Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Quality Selection and Audio Output Controls to Playback Overlay #1406

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.jellyfin.androidtv.ui.playback

import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.koin.java.KoinJavaComponent

class AudioOutputController(
private val parentController: PlaybackController
) {

enum class AudioOutputs(val output: AudioBehavior) {
DIRECT_STREAM(output = AudioBehavior.DIRECT_STREAM),
DOWNMIX_TO_STEREO(output = AudioBehavior.DOWNMIX_TO_STEREO);

companion object {
private val mapping = values().associateBy(AudioOutputs::output)
fun fromPreference(behavior: AudioBehavior) = mapping[behavior]
}
}

companion object {
private var previousAudioOutputSelectioin = AudioOutputs.fromPreference(
KoinJavaComponent.get<UserPreferences>(
UserPreferences::class.java
).get(UserPreferences.audioBehaviour))
}

var currentAudioOutput = previousAudioOutputSelectioin
set(value) {
val checkedVal = AudioOutputs.fromPreference(
KoinJavaComponent.get<UserPreferences>(
UserPreferences::class.java
).get(UserPreferences.audioBehaviour))

previousAudioOutputSelectioin = checkedVal
field = checkedVal
}

init {
currentAudioOutput = previousAudioOutputSelectioin
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,14 @@ public void stop() {
}
}

public void refreshStream() {
// get current timestamp first
refreshCurrentPosition();

stop();
play(mCurrentPosition);
}

zkhcohen marked this conversation as resolved.
Show resolved Hide resolved
public void endPlayback() {
stop();
removePreviousQueueItems();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.jellyfin.androidtv.ui.playback

import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.UserPreferences.Companion.maxBitrate
import org.koin.java.KoinJavaComponent.get

class VideoQualityController(
private val parentController: PlaybackController
) {

enum class QualityProfiles(val quality: String) {
Quality_0(quality = "0"),
Quality_120(quality = "120"),
Quality_110(quality = "110"),
Quality_100(quality = "100"),
Quality_90(quality = "90"),
Quality_80(quality = "80"),
Quality_70(quality = "70"),
Quality_60(quality = "60"),
Quality_50(quality = "50"),
Quality_40(quality = "40"),
Quality_30(quality = "30"),
Quality_20(quality = "20"),
Quality_15(quality = "15"),
Quality_10(quality = "10"),
Quality_5(quality = "5"),
Quality_3(quality = "3"),
Quality_2(quality = "2"),
Quality_1(quality = "1"),
Quality_072(quality = "0.72"),
Quality_042(quality = "0.42");


companion object {
private val mapping = values().associateBy(QualityProfiles::quality)
fun fromPreference(quality: String) = mapping[quality]
}
}

companion object {
private var previousQualitySelection = QualityProfiles.fromPreference(get<UserPreferences>(UserPreferences::class.java).get(maxBitrate))
}

var currentQuality = previousQualitySelection
set(value) {
val checkedVal = QualityProfiles.fromPreference(get<UserPreferences>(UserPreferences::class.java).get(maxBitrate))

previousQualitySelection = checkedVal
field = checkedVal
}

init {
currentQuality = previousQualitySelection
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
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.SelectAudioOutputAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.ZoomAction;
import org.koin.java.KoinJavaComponent;

Expand All @@ -49,6 +51,8 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
private PlaybackControlsRow.SkipNextAction skipNextAction;
private SelectAudioAction selectAudioAction;
private ClosedCaptionsAction closedCaptionsAction;
private SelectQualityAction selectQualityAction;
private SelectAudioOutputAction selectAudioOutputAction;
private AdjustAudioDelayAction adjustAudioDelayAction;
private PlaybackSpeedAction playbackSpeedAction;
private ZoomAction zoomAction;
Expand Down Expand Up @@ -178,6 +182,10 @@ private void initActions(Context context) {
selectAudioAction.setLabels(new String[]{context.getString(R.string.lbl_audio_track)});
closedCaptionsAction = new ClosedCaptionsAction(context, this);
closedCaptionsAction.setLabels(new String[]{context.getString(R.string.lbl_subtitle_track)});
selectQualityAction = new SelectQualityAction(context, this, playbackController);
selectQualityAction.setLabels(new String[]{context.getString(R.string.lbl_quality_profile)});
selectAudioOutputAction = new SelectAudioOutputAction(context, this, playbackController);
selectAudioOutputAction.setLabels(new String[]{context.getString(R.string.lbl_audio_output)});
adjustAudioDelayAction = new AdjustAudioDelayAction(context, this);
adjustAudioDelayAction.setLabels(new String[]{context.getString(R.string.lbl_audio_delay)});
playbackSpeedAction = new PlaybackSpeedAction(context, this, playbackController);
Expand Down Expand Up @@ -255,6 +263,8 @@ void addMediaActions() {

if (!isLiveTv()) {
secondaryActionsAdapter.add(playbackSpeedAction);
secondaryActionsAdapter.add(selectQualityAction);
secondaryActionsAdapter.add(selectAudioOutputAction);
}


Expand All @@ -263,6 +273,7 @@ void addMediaActions() {
} else {
secondaryActionsAdapter.add(zoomAction);
}

}

@Override
Expand Down Expand Up @@ -296,7 +307,13 @@ public void onCustomActionClicked(Action action, View view) {
} else if (action == playbackSpeedAction) {
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
playbackSpeedAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
} else if (action == adjustAudioDelayAction) {
} else if (action == selectQualityAction) {
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
selectQualityAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
} else if (action == selectAudioOutputAction) {
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
selectAudioOutputAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
} else if (action == adjustAudioDelayAction) {
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
adjustAudioDelayAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
} else if (action == zoomAction) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.ui.playback.AudioOutputController
import org.jellyfin.androidtv.ui.playback.PlaybackController
import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue
import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment
import org.koin.java.KoinJavaComponent

class SelectAudioOutputAction (
context: Context,
customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue,
playbackController: PlaybackController
) : CustomAction(context, customPlaybackTransportControlGlue) {
private val audioOutputController = AudioOutputController(playbackController)
private val audioOutputs = AudioOutputController.AudioOutputs.values()

init {
initializeWithIcon(R.drawable.ic_select_audio_output)
}

override fun handleClickAction(
playbackController: PlaybackController,
leanbackOverlayFragment: LeanbackOverlayFragment,
context: Context, view: View
) {
val audioOutputMenu = populateMenu(context, view, audioOutputController)

audioOutputMenu.setOnDismissListener { leanbackOverlayFragment.setFading(true) }

audioOutputMenu.setOnMenuItemClickListener { menuItem ->
KoinJavaComponent.get<UserPreferences>(UserPreferences::class.java).set(UserPreferences.audioBehaviour, audioOutputs[menuItem.itemId].output)
audioOutputController.currentAudioOutput = AudioOutputController.AudioOutputs.fromPreference(
KoinJavaComponent.get<UserPreferences>(UserPreferences::class.java)
.get(UserPreferences.audioBehaviour))
playbackController.refreshStream()
audioOutputMenu.dismiss()
true
}

audioOutputMenu.show()
}

private fun populateMenu(
context: Context,
view: View,
audioOutputController: AudioOutputController
) = PopupMenu(context, view, Gravity.END).apply {
audioOutputs.forEachIndexed { i, selected ->
menu.add(0, i, i, if (selected.output == AudioBehavior.DIRECT_STREAM) "Direct Stream" else "Downmix to Stereo")
}

menu.setGroupCheckable(0, true, true)
menu.getItem(audioOutputs.indexOf(audioOutputController.currentAudioOutput)).isChecked = true
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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.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
import org.koin.java.KoinJavaComponent

class SelectQualityAction (
context: Context,
customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue,
playbackController: PlaybackController
) : CustomAction(context, customPlaybackTransportControlGlue) {
private val qualityController = VideoQualityController(playbackController)
private val qualityProfiles = VideoQualityController.QualityProfiles.values()

init {
initializeWithIcon(R.drawable.ic_select_quality)
}

override fun handleClickAction(
playbackController: PlaybackController,
leanbackOverlayFragment: LeanbackOverlayFragment,
context: Context, view: View
) {
val qualityMenu = populateMenu(context, view, qualityController)

qualityMenu.setOnDismissListener { leanbackOverlayFragment.setFading(true) }

qualityMenu.setOnMenuItemClickListener { menuItem ->
KoinJavaComponent.get<UserPreferences>(UserPreferences::class.java).set(UserPreferences.maxBitrate, qualityProfiles[menuItem.itemId].quality)
qualityController.currentQuality = VideoQualityController.QualityProfiles.fromPreference(
KoinJavaComponent.get<UserPreferences>(UserPreferences::class.java)
.get(UserPreferences.maxBitrate))
playbackController.refreshStream()
qualityMenu.dismiss()
true
}

qualityMenu.show()
}

private fun formatQuality(quality: String, context: Context): String {

val conv = quality.toDouble()

val value = when {
conv == 0.0 -> context.getString(R.string.bitrate_auto)
conv >= 1.0 -> context.getString(R.string.bitrate_mbit, conv)
else -> context.getString(R.string.bitrate_kbit, conv * 100.0)
}

conv.toString().removeSuffix(".0") to value

return value
}

private fun populateMenu(
context: Context,
view: View,
qualityController: VideoQualityController
) = PopupMenu(context, view, Gravity.END).apply {
qualityProfiles.forEachIndexed { i, selected ->
// Since this is purely numeric data, coerce to en_us to keep the linter happy
menu.add(0, i, i, formatQuality(selected.quality, context))
}

menu.setGroupCheckable(0, true, true)
menu.getItem(qualityProfiles.indexOf(qualityController.currentQuality)).isChecked = true
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ class PlaybackPreferencesScreen : OptionsFragment() {
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 >=
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 >=
).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)
else -> getString(R.string.bitrate_kbit, it * 1000.0)
}

it.toString().removeSuffix(".0") to value
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/res/drawable/ic_select_audio_output.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,4.62c0.34,0,0.61,0.28,0.61,0.61v13.53c0,0.34-0.28,0.61-0.61,0.61s-0.61-0.28-0.61-0.61V5.24
C11.39,4.9,11.66,4.62,12,4.62z M9.54,7.08c0.34,0,0.61,0.28,0.61,0.61v8.61c0,0.34-0.28,0.61-0.61,0.61
c-0.34,0-0.61-0.28-0.61-0.61V7.7C8.93,7.36,9.2,7.08,9.54,7.08z M14.46,7.08c0.34,0,0.61,0.28,0.61,0.61v8.61
c0,0.34-0.28,0.61-0.61,0.61c-0.34,0-0.61-0.28-0.61-0.61V7.7C13.84,7.36,14.12,7.08,14.46,7.08z M7.08,8.93
c0.34,0,0.61,0.28,0.61,0.61v4.92c0,0.34-0.28,0.61-0.61,0.61s-0.61-0.28-0.61-0.61V9.54C6.47,9.2,6.74,8.93,7.08,8.93z M16.92,8.93
c0.34,0,0.61,0.28,0.61,0.61v4.92c0,0.34-0.28,0.61-0.61,0.61c-0.34,0-0.61-0.28-0.61-0.61V9.54C16.3,9.2,16.58,8.93,16.92,8.93z
M4.62,10.16c0.34,0,0.61,0.28,0.61,0.61v2.46c0,0.34-0.28,0.61-0.61,0.61s-0.61-0.28-0.61-0.61v-2.46
C4.01,10.43,4.28,10.16,4.62,10.16z M19.38,10.16c0.34,0,0.61,0.28,0.61,0.61v2.46c0,0.34-0.28,0.61-0.61,0.61
c-0.34,0-0.61-0.28-0.61-0.61v-2.46C18.76,10.43,19.04,10.16,19.38,10.16z"
android:fillColor="#fff" />
</vector>
14 changes: 14 additions & 0 deletions app/src/main/res/drawable/ic_select_quality.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2.99,7.21c0,0,0-2.25,2.25-2.25h13.52c0,0,2.25,0,2.25,2.25v6.76c0,0,0,2.25-2.25,2.25h-4.51c0,0.75,0.09,1.31,0.28,1.69
h0.84c0.31,0,0.56,0.25,0.56,0.56c0,0.31-0.25,0.56-0.56,0.56H8.62c-0.31,0-0.56-0.25-0.56-0.56c0-0.31,0.25-0.56,0.56-0.56h0.84
c0.19-0.38,0.28-0.94,0.28-1.69H5.24c0,0-2.25,0-2.25-2.25V7.21z M4.56,6.25c-0.12,0.09-0.22,0.2-0.29,0.34
C4.18,6.79,4.12,7,4.11,7.22v6.75c0,0.37,0.09,0.57,0.16,0.68c0.08,0.12,0.19,0.21,0.34,0.29c0.19,0.09,0.4,0.15,0.61,0.16l0.03,0
h13.51c0.37,0,0.57-0.09,0.68-0.16c0.12-0.09,0.22-0.2,0.29-0.34c0.09-0.19,0.15-0.4,0.16-0.61l0-0.03V7.21
c0-0.37-0.09-0.57-0.16-0.68c-0.09-0.12-0.2-0.22-0.34-0.29c-0.2-0.1-0.41-0.15-0.63-0.16H5.24C4.87,6.09,4.67,6.17,4.56,6.25z"
android:fillColor="#fff" />
</vector>
2 changes: 1 addition & 1 deletion app/src/main/res/values-en-rGB/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,4 @@
<string name="pref_customization">Customisation</string>
<string name="pref_refresh_switching_description">Monitor refresh rate is changed to match video</string>
<string name="pref_show_backdrop_description">Change background image to selected items</string>
</resources>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
<string name="lbl_fit">Normal</string>
<string name="lbl_audio_track">Select audio track</string>
<string name="lbl_playback_speed">Playback Speed</string>
<string name="lbl_quality_profile">Quality Profile</string>
<string name="lbl_subtitle_track">Select subtitle track</string>
<string name="msg_external_path">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.</string>
<string name="btn_got_it">Got it</string>
Expand Down