From 04d2edbfafbc646b4efd67f364422051aa392036 Mon Sep 17 00:00:00 2001 From: vksp2k7 Date: Fri, 30 Oct 2026 03:45:01 +0000 Subject: [PATCH] Add playback speed settings --- .../ui/player/FullScreenPlayer.kt | 15 ++- .../cloudstream3/ui/player/PlaybackSpeeds.kt | 44 +++++++ .../ui/player/PlayerGestureHelper.kt | 27 ++++- .../ui/settings/SettingsPlayer.kt | 26 +++- .../ui/settings/SettingsPlayerScreen.kt | 9 ++ .../main/res/layout/player_custom_layout.xml | 10 +- .../res/layout/player_custom_layout_tv.xml | 11 +- app/src/main/res/layout/speed_dialog.xml | 112 +++++++++++++++--- .../main/res/layout/trailer_custom_layout.xml | 10 +- app/src/main/res/values/array.xml | 19 +++ .../res/values/donottranslate-strings.xml | 1 + app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/settings_player.xml | 9 +- .../cloudstream4/DevicePreferenceStore.kt | 6 +- 14 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/PlaybackSpeeds.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index d337d53a6c9..49780d25e58 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -96,7 +96,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( protected var androidTVInterfaceOffSeekTime = 10000L protected var androidTVInterfaceOnSeekTime = 30000L - protected var playBackSpeedEnabled = false + protected var playBackSpeedEnabled = true protected var playerResizeEnabled = false protected var playerRotateEnabled = false protected var rotatedManually = false @@ -639,7 +639,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( binding.speedText.text = "%.2fx".format(speed).replace(".0x", "x") // Android crashes if you don't round to an exact step size binding.speedBar.value = - (speed.coerceIn(0.1f, 2.0f) / binding.speedBar.stepSize).roundToInt() + (speed.coerceIn(PlaybackSpeeds.MIN_SPEED, PlaybackSpeeds.MAX_DIALOG_SPEED) / binding.speedBar.stepSize).roundToInt() .toFloat() * binding.speedBar.stepSize } @@ -655,10 +655,15 @@ open class FullScreenPlayer : AbstractPlayerFragment( updateSpeedDialogBinding(binding) for ((view, speed) in arrayOf( binding.speed25 to 0.25f, + binding.speed50 to 0.5f, + binding.speed75 to 0.75f, binding.speed100 to 1.0f, binding.speed125 to 1.25f, binding.speed150 to 1.5f, + binding.speed175 to 1.75f, binding.speed200 to 2.0f, + binding.speed300 to 3.0f, + binding.speed400 to 4.0f, )) { view.setOnClickListener { setPlayBackSpeed(speed) @@ -667,12 +672,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( } binding.speedMinus.setOnClickListener { - setPlayBackSpeed(maxOf((player.getPlaybackSpeed() - 0.1f), 0.1f)) + setPlayBackSpeed(maxOf(player.getPlaybackSpeed() - 0.1f, PlaybackSpeeds.MIN_SPEED)) updateSpeedDialogBinding(binding) } binding.speedPlus.setOnClickListener { - setPlayBackSpeed(minOf((player.getPlaybackSpeed() + 0.1f), 2.0f)) + setPlayBackSpeed(minOf(player.getPlaybackSpeed() + 0.1f, PlaybackSpeeds.MAX_DIALOG_SPEED)) updateSpeedDialogBinding(binding) } @@ -1410,7 +1415,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( playBackSpeedEnabled = settingsManager.getBoolean( ctx.getString(R.string.playback_speed_enabled_key), - false + true ) playBackCompressorEnabled = settingsManager.getBoolean( ctx.getString(R.string.compressor_enabled_key), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlaybackSpeeds.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlaybackSpeeds.kt new file mode 100644 index 00000000000..3b0b2997236 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlaybackSpeeds.kt @@ -0,0 +1,44 @@ +package com.lagradost.cloudstream3.ui.player + +import java.text.DecimalFormat + +/** + * Shared constants and helpers for playback speed, used by the speed dialog + * ([FullScreenPlayer]) and the hold-to-speed-up gesture ([PlayerGestureHelper]). + */ +object PlaybackSpeeds { + const val MIN_SPEED = 0.1f + + /** Upper bound of the speed dialog (slider, +/- buttons and presets). */ + const val MAX_DIALOG_SPEED = 4.0f + + /** Upper bound for the long-press speed, including the automatic escalation. */ + const val MAX_HOLD_SPEED = 8.0f + + /** Default long-press speed, stored in percent in the settings (200 = 2x). */ + const val DEFAULT_HOLD_SPEED_PERCENT = 200 + + private const val EPSILON = 0.001f + + /** Converts the stored percent setting (200 = 2x) to a speed multiplier. */ + fun holdSpeedFromPercent(percent: Int): Float = + (percent / 100f).coerceIn(MIN_SPEED, MAX_HOLD_SPEED) + + /** + * Speed to use while the screen is held. + * + * Starts at the [configured] long-press speed, but if playback is already running at that + * speed (or faster) the speed is doubled until it is actually faster than [current], + * so with a 2x setting: 1x -> 2x, 2x -> 4x, 4x -> 8x. Never exceeds [MAX_HOLD_SPEED]. + */ + fun getHoldSpeed(current: Float, configured: Float): Float { + var target = configured.coerceIn(MIN_SPEED, MAX_HOLD_SPEED) + while (target <= current + EPSILON && target < MAX_HOLD_SPEED) { + target *= 2f + } + return target.coerceAtMost(MAX_HOLD_SPEED) + } + + /** Formats a speed for display without trailing zeroes, e.g. 2.0 -> "2x", 1.25 -> "1.25x". */ + fun format(speed: Float): String = DecimalFormat("0.##").format(speed.toDouble()) + "x" +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt index d3340cb57cd..926eba00b47 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt @@ -22,6 +22,7 @@ import android.view.WindowInsets import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import android.widget.TextView import androidx.annotation.OptIn import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -40,7 +41,6 @@ import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight import com.lagradost.cloudstream3.utils.Vector2 import kotlin.math.abs @@ -128,12 +128,20 @@ class PlayerGestureHelper(private val playerView: PlayerView) { var swipeHorizontalEnabled: Boolean = false var extraBrightnessEnabled: Boolean = false var speedupEnabled: Boolean = false + /** Configured long-press speed (see [PlaybackSpeeds.getHoldSpeed] for the escalation). */ + var speedupSpeed: Float = PlaybackSpeeds.holdSpeedFromPercent(PlaybackSpeeds.DEFAULT_HOLD_SPEED_PERCENT) /** Hold / speed-up */ val holdHandler = Handler(Looper.getMainLooper()) var hasTriggeredSpeedUp = false + /** Speed that was active before the hold started, restored on release. */ + private var speedBeforeHold = 1.0f val holdRunnable = Runnable { - playerView.player.setPlaybackSpeed(2.0f) + // If we are already at (or above) the long-press speed, go faster: 2x -> 4x -> 8x... + speedBeforeHold = playerView.player.getPlaybackSpeed() + val holdSpeed = PlaybackSpeeds.getHoldSpeed(speedBeforeHold, speedupSpeed) + playerView.player.setPlaybackSpeed(holdSpeed) + updateSpeedUpText(holdSpeed) showOrHideSpeedUp(true) playerView.callbacks?.onHoldSpeedUp(true) hasTriggeredSpeedUp = true @@ -219,7 +227,13 @@ class PlayerGestureHelper(private val playerView: PlayerView) { swipeVerticalEnabled = sm.getBoolean(context.getString(R.string.swipe_vertical_enabled_key), true) swipeHorizontalEnabled = sm.getBoolean(context.getString(R.string.swipe_enabled_key), true) extraBrightnessEnabled = sm.getBoolean(context.getString(R.string.extra_brightness_key), false) - speedupEnabled = sm.getBoolean(context.getString(R.string.speedup_key), false) + speedupEnabled = sm.getBoolean(context.getString(R.string.speedup_key), true) + speedupSpeed = PlaybackSpeeds.holdSpeedFromPercent( + sm.getInt( + context.getString(R.string.speedup_speed_key), + PlaybackSpeeds.DEFAULT_HOLD_SPEED_PERCENT + ) + ) doubleTapEnabled = sm.getBoolean(context.getString(R.string.double_tap_enabled_key), false) doubleTapPauseEnabled = sm.getBoolean(context.getString(R.string.double_tap_pause_enabled_key), false) fastForwardTime = sm.getInt(context.getString(R.string.double_tap_seek_time_key), 10).toLong() * 1000L @@ -282,6 +296,11 @@ class PlayerGestureHelper(private val playerView: PlayerView) { /** Speed-up */ + /** Shows the current hold speed (e.g. "4x") next to the icon on the speed-up indicator. */ + private fun updateSpeedUpText(speed: Float) { + (playerView.playerSpeedupButton as? TextView)?.text = PlaybackSpeeds.format(speed) + } + fun showOrHideSpeedUp(show: Boolean) { playerView.playerSpeedupButton?.let { btn -> btn.clearAnimation() @@ -1164,7 +1183,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) { MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { holdHandler.removeCallbacks(holdRunnable) if (hasTriggeredSpeedUp) { - playerView.player.setPlaybackSpeed(DataStoreHelper.playBackSpeed) + playerView.player.setPlaybackSpeed(speedBeforeHold) showOrHideSpeedUp(false) playerView.callbacks?.onHoldSpeedUp(false) hasTriggeredSpeedUp = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 2949281153f..29db31f350c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -9,6 +9,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.actions.VideoClickActionHolder import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat +import com.lagradost.cloudstream3.ui.player.PlaybackSpeeds import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE @@ -48,7 +49,8 @@ class SettingsPlayer : BasePreferenceFragmentCompat() { R.string.pref_category_gestures_key, R.string.rotate_video_key, R.string.auto_rotate_video_key, - R.string.speedup_key + R.string.speedup_key, + R.string.speedup_speed_key ), TV or EMULATOR ) @@ -96,6 +98,28 @@ class SettingsPlayer : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.speedup_speed_key)?.setOnPreferenceClickListener { + val prefNames = resources.getStringArray(R.array.speedup_speed_names) + val prefValues = resources.getIntArray(R.array.speedup_speed_values) + val current = settingsManager.getInt( + getString(R.string.speedup_speed_key), + PlaybackSpeeds.DEFAULT_HOLD_SPEED_PERCENT + ) + + activity?.showBottomDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.speedup_speed_title), + true, + {} + ) { + settingsManager.edit { + putInt(getString(R.string.speedup_speed_key), prefValues[it]) + } + } + return@setOnPreferenceClickListener true + } + getPref(R.string.software_decoding_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.software_decoding_switch) val prefValues = resources.getIntArray(R.array.software_decoding_switch_values) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt index 8f6f5bf4f12..01313a4d553 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt @@ -142,6 +142,15 @@ object SettingsPlayerScreen : SearchableSettings { subtitle = stringResource(R.string.speedup_summary), icon = painterResource(R.drawable.speedup), ), + Preference.PreferenceItem.ListPreference( + preference = settings.player.tiktokSpeed, + title = stringResource(R.string.speedup_speed_title), + subtitle = stringResource(R.string.speedup_speed_summary) + "\n%s", + icon = painterResource(R.drawable.speedup), + entries = integerArrayResource(R.array.speedup_speed_values).zip( + stringArrayResource(R.array.speedup_speed_names) + ).toMap() + ), Preference.PreferenceItem.SwitchPreference( preference = settings.player.autoPlayEnabled, title = stringResource(R.string.autoplay_next_settings), diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 9a79e6590fc..98494059830 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -1103,15 +1103,21 @@ diff --git a/app/src/main/res/layout/speed_dialog.xml b/app/src/main/res/layout/speed_dialog.xml index bb97914f1c8..ec30cdd5b76 100644 --- a/app/src/main/res/layout/speed_dialog.xml +++ b/app/src/main/res/layout/speed_dialog.xml @@ -43,13 +43,13 @@ android:nextFocusLeft="@id/speed_minus" android:nextFocusRight="@id/speed_plus" android:nextFocusUp="@id/speed_bar" - android:nextFocusDown="@id/speed_125" + android:nextFocusDown="@id/speed_100" android:progressTint="?attr/white" android:stepSize="0.05" android:thumbTint="?attr/white" android:value="1.0" android:valueFrom="0.1" - android:valueTo="2.0" + android:valueTo="4.0" app:labelStyle="@style/BlackLabel" app:thumbColor="?attr/white" app:tickVisible="false" @@ -66,7 +66,7 @@ android:foreground="@drawable/outline_drawable" android:nextFocusLeft="@id/speed_bar" android:nextFocusRight="@id/speed_minus" - android:nextFocusDown="@id/speed_200" + android:nextFocusDown="@id/speed_125" android:src="@drawable/media3_icon_plus" app:tint="?attr/textColor" /> @@ -74,7 +74,10 @@ + android:layout_marginStart="14dp" + android:layout_marginEnd="14dp" + android:layout_marginTop="14dp" + android:layout_marginBottom="0dp"> + + + + @@ -109,11 +138,19 @@ android:layout_height="wrap_content" android:layout_weight="1" android:nextFocusLeft="@id/speed_100" - android:nextFocusRight="@id/speed_150" - android:nextFocusUp="@id/speed_bar" - android:nextFocusDown="@id/speed_125" + android:nextFocusRight="@id/speed_25" + android:nextFocusUp="@id/speed_plus" + android:nextFocusDown="@id/speed_400" android:text="1.25x" tools:ignore="HardcodedText" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index b1dc964797a..941afc161ba 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -1063,15 +1063,21 @@ 2 + + + 1.5x + 2x + 2.5x + 3x + 4x + 5x + + + + 150 + 200 + 250 + 300 + 400 + 500 + + @string/automatic HW+SW diff --git a/app/src/main/res/values/donottranslate-strings.xml b/app/src/main/res/values/donottranslate-strings.xml index bf1566cdb98..e6e178f7ab2 100644 --- a/app/src/main/res/values/donottranslate-strings.xml +++ b/app/src/main/res/values/donottranslate-strings.xml @@ -95,6 +95,7 @@ manual_update_plugins legal_notice_key speedup_key + speedup_speed_key anilist_key simkl_key mal_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c429519967..26ae80d79e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -774,7 +774,9 @@ Changes size of posters Poster size LongPress Speed Toggle - Hold to get 2x speed + Hold the screen to speed up playback. If already at that speed, it doubles (2x → 4x → 8x) + LongPress speed + Playback speed while holding the screen Edit Profile Image Url Image picker diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index f370fbcc9c2..7ece02f880a 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -78,7 +78,7 @@ android:icon="@drawable/ic_baseline_speed_24" android:summary="@string/speed_setting_summary" android:title="@string/eigengraumode_settings" - app:defaultValue="false" + app:defaultValue="true" app:key="@string/playback_speed_enabled_key" /> +