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",
"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"

View File

@ -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) }

View File

@ -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(
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(key)?.toString() ?: return@hook,
}?.get(paramKey)?.toString() ?: return value,
defaultValue = fields.firstOrNull {
it.type == Object::class.java
}?.get(key)
}?.get(paramKey)
)
} ?: return@hook
val value = param.argNullable<Any>(1) ?: return@hook
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()))
}
}
}

View File

@ -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"]

View File

@ -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
}
}