commit 93e9a67daf4a20682023560ff2b4e441e227a465
parent 60ee3680a484a1de4fc305a6ce64834c6dde3ed4
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Wed, 14 Feb 2024 17:40:55 +0100
feat: override video playback rate
Diffstat:
5 files changed, 117 insertions(+), 23 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -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"
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt
@@ -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) }
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt
@@ -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?
@@ -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 })
@@ -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()))
}
}
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt
@@ -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
@@ -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"]
diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt
@@ -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
@@ -8,7 +9,14 @@ 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 {
@@ -16,17 +24,23 @@ class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") {
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
}
}