Skip to content

Commit

Permalink
feat: override video playback rate
Browse files Browse the repository at this point in the history
  • Loading branch information
rhunk committed Feb 14, 2024
1 parent 60ee368 commit 93e9a67
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 23 deletions.
8 changes: 8 additions & 0 deletions common/src/main/assets/lang/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,14 @@
"name": "Bypass Video Length Restrictions",
"description": "Single: sends a single video\nSplit: split videos after editing"
},
"default_video_playback_rate": {
"name": "Default Video Playback Rate",
"description": "Sets the default speed for the playback of videos\nValue must be between 0.1 and 4.0"
},
"video_playback_rate_slider": {
"name": "Video Playback Rate Slider",
"description": "Adds a slider in opera context menu to change the video playback rate\nNote: Changes only apply to subsequent videos"
},
"disable_google_play_dialogs": {
"name": "Disable Google Play Services Dialogs",
"description": "Prevent Google Play Services availability dialogs from being shown"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class Global : ConfigContainer() {
val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() }
val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(
FeatureNotice.BAN_RISK); requireRestart(); nativeHooks() }
val defaultVideoPlaybackRate = float("default_video_playback_rate", 1.0F) { requireRestart(); inputCheck = { (it.toFloatOrNull() ?: 1.0F) in 0.1F..4.0F} }
val videoPlaybackRateSlider = boolean("video_playback_rate_slider") { requireRestart() }
val disableGooglePlayDialogs = boolean("disable_google_play_dialogs") { requireRestart() }
val forceUploadSourceQuality = boolean("force_upload_source_quality") { requireRestart() }
val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.INTERNAL_BEHAVIOR) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper

class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
var currentPlaybackRate = 1.0F

data class OverrideKey(
val name: String,
val defaultValue: Any?
Expand All @@ -24,6 +26,12 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
overrideMap[key] = Override(filter, value)
}

currentPlaybackRate = context.config.global.defaultVideoPlaybackRate.getNullable()?.takeIf { it > 0 } ?: 1.0F

if (context.config.global.videoPlaybackRateSlider.get() || currentPlaybackRate != 1.0F) {
overrideParam("video_playback_rate", { currentPlaybackRate != 1.0F }, { _, _ -> currentPlaybackRate.toDouble() })
}

if (context.config.messaging.loopMediaPlayback.get()) {
//https://github.com/rodit/SnapMod/blob/master/app/src/main/java/xyz/rodit/snapmod/features/opera/SnapDurationModifier.kt
overrideParam("auto_advance_mode", { true }, { key, _ -> key.defaultValue })
Expand All @@ -37,29 +45,36 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
}

context.mappings.useMapper(OperaViewerParamsMapper::class) {
classReference.get()?.hook(putMethod.get()!!, HookStage.BEFORE) { param ->
val key = param.argNullable<Any>(0)?.let { key ->
val fields = key::class.java.fields
OverrideKey(
name = fields.firstOrNull {
it.type == String::class.java
}?.get(key)?.toString() ?: return@hook,
defaultValue = fields.firstOrNull {
it.type == Object::class.java
}?.get(key)
)
} ?: return@hook
val value = param.argNullable<Any>(1) ?: return@hook
fun overrideParamResult(paramKey: Any, value: Any?): Any? {
val fields = paramKey::class.java.fields
val key = OverrideKey(
name = fields.firstOrNull {
it.type == String::class.java
}?.get(paramKey)?.toString() ?: return value,
defaultValue = fields.firstOrNull {
it.type == Object::class.java
}?.get(paramKey)
)

overrideMap[key.name]?.let { override ->
if (override.filter(value)) {
runCatching {
param.setArg(1, override.value(key, value))
return override.value(key, value)
}.onFailure {
context.log.error("Failed to override param $key", it)
}
}
}

return value
}

classReference.get()?.hook(getMethod.get()!!, HookStage.AFTER) { param ->
param.setResult(overrideParamResult(param.arg(0), param.getResult()))
}

classReference.get()?.hook(getOrDefaultMethod.get()!!, HookStage.AFTER) { param ->
param.setResult(overrideParamResult(param.arg(0), param.getResult()))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,27 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.ui.applyTheme
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
import me.rhunk.snapenhance.core.util.ktx.getId
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress
import me.rhunk.snapenhance.core.wrapper.impl.ScSize
import java.text.DateFormat
Expand Down Expand Up @@ -129,6 +145,45 @@ class OperaContextActionMenu : AbstractMenu() {
}
}

if (context.config.global.videoPlaybackRateSlider.get()) {
val operaViewerParamsOverride = context.feature(OperaViewerParamsOverride::class)

linearLayout.addView(createComposeView(view.context) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
var value by remember { mutableFloatStateOf(operaViewerParamsOverride.currentPlaybackRate) }
Slider(
value = value,
onValueChange = {
value = it
operaViewerParamsOverride.currentPlaybackRate = it
},
valueRange = 0.1F..4.0F,
steps = 0,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "x" + value.toString().take(4),
color = remember {
view.context.theme.obtainStyledAttributes(
intArrayOf(view.context.resources.getIdentifier("sigColorTextPrimary", "attr"))
).getColor(0, 0).let { Color(it) }
},
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}.apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
})
}

if (context.config.downloader.downloadContextMenu.get()) {
linearLayout.addView(Button(view.context).apply {
text = translation["download"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.rhunk.snapenhance.mapper.impl

import com.android.tools.smali.dexlib2.iface.Method
import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName
Expand All @@ -8,25 +9,38 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference

class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") {
val classReference = classReference("class")
val putMethod = string("putMethod")
val getMethod = string("getMethod")
val getOrDefaultMethod = string("getOrDefaultMethod")

private fun Method.hasHashMapReference(methodName: String) = implementation?.instructions?.any {
val instruction = it as? Instruction35c ?: return@any false
val reference = instruction.reference as? MethodReference ?: return@any false
reference.name == methodName && reference.definingClass == "Ljava/util/concurrent/ConcurrentHashMap;"
} == true

init {
mapper {
for (classDef in classes) {
classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue
if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue

val putDexMethod = classDef.methods.firstOrNull { method ->
method.implementation?.instructions?.any {
val instruction = it as? Instruction35c ?: return@any false
val reference = instruction.reference as? MethodReference ?: return@any false
reference.name == "put" && reference.definingClass == "Ljava/util/concurrent/ConcurrentHashMap;"
} == true
val getOrDefaultDexMethod = classDef.methods.firstOrNull { method ->
method.returnType == "Ljava/lang/Object;" &&
method.parameters.size == 2 &&
method.parameterTypes[1] == "Ljava/lang/Object;" &&
method.hasHashMapReference("get")
} ?: return@mapper

classReference.set(classDef.getClassName())
putMethod.set(putDexMethod.name)
val getDexMethod = classDef.methods.firstOrNull { method ->
method.returnType == "Ljava/lang/Object;" &&
method.parameters.size == 1 &&
method.parameterTypes[0] == getOrDefaultDexMethod.parameterTypes[0] &&
method.hasHashMapReference("get")
} ?: return@mapper

getMethod.set(getDexMethod.name)
getOrDefaultMethod.set(getOrDefaultDexMethod.name)
classReference.set(classDef.getClassName())
return@mapper
}
}
Expand Down

0 comments on commit 93e9a67

Please sign in to comment.