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:
Mcommon/src/main/assets/lang/en_US.json | 8++++++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt | 2++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt | 43+++++++++++++++++++++++++++++--------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt | 32+++++++++++++++++++++++---------
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 } }