diff --git a/app/src/main/java/org/jellyfin/androidtv/constant/QualityProfiles.kt b/app/src/main/java/org/jellyfin/androidtv/constant/QualityProfiles.kt new file mode 100644 index 0000000000..2b220b838c --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/constant/QualityProfiles.kt @@ -0,0 +1,27 @@ +package org.jellyfin.androidtv.constant + +import android.content.Context +import org.jellyfin.androidtv.R + +@Suppress("MagicNumber") +private val qualityOptions = setOf( + 0.0, // auto + 120.0, 110.0, 100.0, // 100 >= + 90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 15.0, 10.0, // 10 >= + 5.0, 3.0, 2.0, 1.0, // 1 >= + 0.72, 0.42 // 0 >= +) + +@Suppress("MagicNumber") +fun getQualityProfiles( + context: Context +): Map = qualityOptions.associate { + val value = when { + it == 0.0 -> context.getString(R.string.bitrate_auto) + it >= 1.0 -> context.getString(R.string.bitrate_mbit, it) + else -> context.getString(R.string.bitrate_kbit, it * 1000.0) + } + + it.toString().removeSuffix(".0") to value +} + diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java index 96fae6f6cc..1c802b0038 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java @@ -1139,6 +1139,14 @@ public void stop() { } } + public void refreshStream() { + // get current timestamp first + refreshCurrentPosition(); + + stop(); + play(mCurrentPosition); + } + public void endPlayback(Boolean closeActivity) { if (closeActivity) mFragment.getActivity().finish(); stop(); diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoQualityController.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoQualityController.kt new file mode 100644 index 0000000000..ea57bb233c --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoQualityController.kt @@ -0,0 +1,14 @@ +package org.jellyfin.androidtv.ui.playback + +import org.jellyfin.androidtv.preference.UserPreferences; + +class VideoQualityController( + previousQualitySelection: String, + private val userPreferences: UserPreferences, +) { + var currentQuality = previousQualitySelection + set(value) { + userPreferences[UserPreferences.maxBitrate] = value + field = value + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java index a79accf8d0..bd701fb808 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java @@ -36,6 +36,7 @@ import org.jellyfin.androidtv.ui.playback.overlay.action.PreviousLiveTvChannelAction; import org.jellyfin.androidtv.ui.playback.overlay.action.RecordAction; import org.jellyfin.androidtv.ui.playback.overlay.action.SelectAudioAction; +import org.jellyfin.androidtv.ui.playback.overlay.action.SelectQualityAction; import org.jellyfin.androidtv.ui.playback.overlay.action.SettingAction; import org.jellyfin.androidtv.ui.playback.overlay.action.ZoomAction; import org.koin.java.KoinJavaComponent; @@ -52,6 +53,7 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl private PlaybackControlsRow.SkipNextAction skipNextAction; private SelectAudioAction selectAudioAction; private ClosedCaptionsAction closedCaptionsAction; + private SelectQualityAction selectQualityAction; private SettingAction settingAction; private PlaybackSpeedAction playbackSpeedAction; private ZoomAction zoomAction; @@ -183,6 +185,8 @@ private void initActions(Context context) { closedCaptionsAction.setLabels(new String[]{context.getString(R.string.lbl_subtitle_track)}); settingAction = new SettingAction(context, this); settingAction.setLabels(new String[]{context.getString(R.string.lbl_adjust)}); + selectQualityAction = new SelectQualityAction(context, this, KoinJavaComponent.get(UserPreferences.class)); + selectQualityAction.setLabels(new String[]{context.getString(R.string.lbl_quality_profile)}); playbackSpeedAction = new PlaybackSpeedAction(context, this, playbackController); playbackSpeedAction.setLabels(new String[]{context.getString(R.string.lbl_playback_speed)}); zoomAction = new ZoomAction(context, this); @@ -263,6 +267,7 @@ void addMediaActions() { if (!isLiveTv()) { secondaryActionsAdapter.add(playbackSpeedAction); + secondaryActionsAdapter.add(selectQualityAction); } @@ -314,6 +319,9 @@ public void onCustomActionClicked(Action action, View view) { // This is a hack, we should instead have onPlaybackParametersChanged call out to this // class to notify rather than poll. But communication is unidirectional at the moment: mHandler.postDelayed(mRefreshEndTime, 5000); // 5 seconds + } else if (action == selectQualityAction) { + getPlayerAdapter().getLeanbackOverlayFragment().setFading(false); + selectQualityAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view); } else if (action == zoomAction) { getPlayerAdapter().getLeanbackOverlayFragment().setFading(false); zoomAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view); diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt new file mode 100644 index 0000000000..70b1033c39 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt @@ -0,0 +1,52 @@ +package org.jellyfin.androidtv.ui.playback.overlay.action + +import android.content.Context +import android.view.Gravity +import android.view.View +import android.widget.PopupMenu +import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.constant.getQualityProfiles +import org.jellyfin.androidtv.preference.UserPreferences +import org.jellyfin.androidtv.ui.playback.PlaybackController +import org.jellyfin.androidtv.ui.playback.VideoQualityController +import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue +import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment + +class SelectQualityAction( + context: Context, + customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue, + userPreferences: UserPreferences +) : CustomAction(context, customPlaybackTransportControlGlue) { + private val previousQualitySelection = userPreferences[UserPreferences.maxBitrate] + private val qualityController = VideoQualityController(previousQualitySelection, userPreferences) + private val qualityProfiles = getQualityProfiles(context) + + init { + initializeWithIcon(R.drawable.ic_select_quality) + } + + override fun handleClickAction( + playbackController: PlaybackController, + leanbackOverlayFragment: LeanbackOverlayFragment, + context: Context, view: View + ) { + val qualityMenu = PopupMenu(context, view, Gravity.END).apply { + qualityProfiles.values.forEachIndexed { i, selected -> + menu.add(0, i, i, selected) + } + + menu.setGroupCheckable(0, true, true) + menu.getItem(qualityProfiles.keys.indexOf(qualityController.currentQuality))?.let { item -> + item.isChecked = true + } + + setOnDismissListener { leanbackOverlayFragment.setFading(true) } + setOnMenuItemClickListener { menuItem -> + qualityController.currentQuality = qualityProfiles.keys.elementAt(menuItem.itemId) + playbackController.refreshStream() + dismiss() + true + } + }.show() + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt index b7e7eb594c..96b1916611 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt @@ -2,6 +2,7 @@ package org.jellyfin.androidtv.ui.preference.screen import android.app.AlertDialog import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.constant.getQualityProfiles import org.jellyfin.androidtv.preference.UserPreferences import org.jellyfin.androidtv.preference.constant.AudioBehavior import org.jellyfin.androidtv.preference.constant.NEXTUP_TIMER_DISABLED @@ -96,21 +97,7 @@ class PlaybackPreferencesScreen : OptionsFragment() { @Suppress("MagicNumber") list { setTitle(R.string.pref_max_bitrate_title) - entries = setOf( - 0.0, // auto - 120.0, 110.0, 100.0, // 100 >= - 90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 21.0, 15.0, 10.0, // 10 >= - 5.0, 3.0, 2.0, 1.0, // 1 >= - 0.72, 0.42 // 0 >= - ).associate { - val value = when { - it == 0.0 -> getString(R.string.bitrate_auto) - it >= 1.0 -> getString(R.string.bitrate_mbit, it) - else -> getString(R.string.bitrate_kbit, it * 100.0) - } - - it.toString().removeSuffix(".0") to value - } + entries = getQualityProfiles(context) bind(userPreferences, UserPreferences.maxBitrate) } diff --git a/app/src/main/res/drawable/ic_select_quality.xml b/app/src/main/res/drawable/ic_select_quality.xml new file mode 100644 index 0000000000..3ccde25777 --- /dev/null +++ b/app/src/main/res/drawable/ic_select_quality.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e800018342..3684e8afb6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -190,6 +190,7 @@ Normal Select audio track Playback Speed + Quality profile Select subtitle track This feature will only work if you have properly set up your library on the server with network paths or path substitution and the client you are using can directly access these locations over the network. Got it