feat: override video playback rate

This commit is contained in:
rhunk
2024-02-14 17:40:55 +01:00
parent 60ee3680a4
commit 93e9a67daf
5 changed files with 117 additions and 23 deletions

View File

@ -522,6 +522,14 @@
"name": "Bypass Video Length Restrictions", "name": "Bypass Video Length Restrictions",
"description": "Single: sends a single video\nSplit: split videos after editing" "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": { "disable_google_play_dialogs": {
"name": "Disable Google Play Services Dialogs", "name": "Disable Google Play Services Dialogs",
"description": "Prevent Google Play Services availability dialogs from being shown" "description": "Prevent Google Play Services availability dialogs from being shown"

View File

@ -33,6 +33,8 @@ class Global : ConfigContainer() {
val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() } val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() }
val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices( val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(
FeatureNotice.BAN_RISK); requireRestart(); nativeHooks() } 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 disableGooglePlayDialogs = boolean("disable_google_play_dialogs") { requireRestart() }
val forceUploadSourceQuality = boolean("force_upload_source_quality") { requireRestart() } val forceUploadSourceQuality = boolean("force_upload_source_quality") { requireRestart() }
val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.INTERNAL_BEHAVIOR) } val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.INTERNAL_BEHAVIOR) }

View File

@ -7,6 +7,8 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper
class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
var currentPlaybackRate = 1.0F
data class OverrideKey( data class OverrideKey(
val name: String, val name: String,
val defaultValue: Any? val defaultValue: Any?
@ -24,6 +26,12 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
overrideMap[key] = Override(filter, value) 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()) { if (context.config.messaging.loopMediaPlayback.get()) {
//https://github.com/rodit/SnapMod/blob/master/app/src/main/java/xyz/rodit/snapmod/features/opera/SnapDurationModifier.kt //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 }) overrideParam("auto_advance_mode", { true }, { key, _ -> key.defaultValue })
@ -37,29 +45,36 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
} }
context.mappings.useMapper(OperaViewerParamsMapper::class) { context.mappings.useMapper(OperaViewerParamsMapper::class) {
classReference.get()?.hook(putMethod.get()!!, HookStage.BEFORE) { param -> fun overrideParamResult(paramKey: Any, value: Any?): Any? {
val key = param.argNullable<Any>(0)?.let { key -> val fields = paramKey::class.java.fields
val fields = key::class.java.fields val key = OverrideKey(
OverrideKey( name = fields.firstOrNull {
name = fields.firstOrNull { it.type == String::class.java
it.type == String::class.java }?.get(paramKey)?.toString() ?: return value,
}?.get(key)?.toString() ?: return@hook, defaultValue = fields.firstOrNull {
defaultValue = fields.firstOrNull { it.type == Object::class.java
it.type == Object::class.java }?.get(paramKey)
}?.get(key) )
)
} ?: return@hook
val value = param.argNullable<Any>(1) ?: return@hook
overrideMap[key.name]?.let { override -> overrideMap[key.name]?.let { override ->
if (override.filter(value)) { if (override.filter(value)) {
runCatching { runCatching {
param.setArg(1, override.value(key, value)) return override.value(key, value)
}.onFailure { }.onFailure {
context.log.error("Failed to override param $key", it) 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()))
} }
} }
} }

View File

@ -8,11 +8,27 @@ import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView 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.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.ui.applyTheme import me.rhunk.snapenhance.core.ui.applyTheme
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
import me.rhunk.snapenhance.core.util.ktx.getId 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.util.ktx.vibrateLongPress
import me.rhunk.snapenhance.core.wrapper.impl.ScSize import me.rhunk.snapenhance.core.wrapper.impl.ScSize
import java.text.DateFormat 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()) { if (context.config.downloader.downloadContextMenu.get()) {
linearLayout.addView(Button(view.context).apply { linearLayout.addView(Button(view.context).apply {
text = translation["download"] text = translation["download"]

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.mapper.impl 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.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
@ -8,7 +9,14 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference
class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") { class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") {
val classReference = classReference("class") 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 { init {
mapper { mapper {
@ -16,17 +24,23 @@ class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") {
classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue
if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue
val putDexMethod = classDef.methods.firstOrNull { method -> val getOrDefaultDexMethod = classDef.methods.firstOrNull { method ->
method.implementation?.instructions?.any { method.returnType == "Ljava/lang/Object;" &&
val instruction = it as? Instruction35c ?: return@any false method.parameters.size == 2 &&
val reference = instruction.reference as? MethodReference ?: return@any false method.parameterTypes[1] == "Ljava/lang/Object;" &&
reference.name == "put" && reference.definingClass == "Ljava/util/concurrent/ConcurrentHashMap;" method.hasHashMapReference("get")
} == true
} ?: return@mapper } ?: return@mapper
classReference.set(classDef.getClassName()) val getDexMethod = classDef.methods.firstOrNull { method ->
putMethod.set(putDexMethod.name) 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 return@mapper
} }
} }