add seperate button to adjust subtitle delay (if subtitles are present). (#1731)
* add seperate button to adjust subtitle delay (if subtitles are present). * merge subtitle delay action and audio delay action to single button. * Remove "m" prefix Fix magic numbers Fix formatting Remove superfluous member variables * Changes after review Move all logic from `init` to constructor. * Minor changes to SettingsPopup Co-authored-by: Tim van Katwijk <tim.van.katwijk@moba.net>
This commit is contained in:
@@ -127,6 +127,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
*/
|
||||
var libVLCAudioDelay = intPreference("libvlc_audio_delay", 0)
|
||||
|
||||
/**
|
||||
* Default audio subtitle in milliseconds for libVLC
|
||||
*/
|
||||
var libVLCSubtitleDelay = intPreference("libvlc_subtitle_delay", 0)
|
||||
|
||||
/* Live TV */
|
||||
/**
|
||||
* Use direct play
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jellyfin.androidtv.databinding.AudioDelayPopupBinding;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
|
||||
public class AudioDelayPopup {
|
||||
private PopupWindow mPopup;
|
||||
private View mAnchor;
|
||||
private NumberSpinnerView mDelaySpinner;
|
||||
|
||||
public AudioDelayPopup(Context context, View anchor, ValueChangedListener<Long> listener) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
AudioDelayPopupBinding binding = AudioDelayPopupBinding.inflate(inflater, null, false);
|
||||
|
||||
int width = Utils.convertDpToPixel(context, 240);
|
||||
int height = Utils.convertDpToPixel(context, 130);
|
||||
|
||||
mPopup = new PopupWindow(binding.getRoot(), width, height);
|
||||
mPopup.setFocusable(true);
|
||||
mPopup.setOutsideTouchable(true);
|
||||
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
|
||||
mAnchor = anchor;
|
||||
|
||||
mDelaySpinner = binding.numberSpinner;
|
||||
mDelaySpinner.setValueChangedListener(listener);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PopupWindow getPopupWindow() {
|
||||
return mPopup;
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return (mPopup != null && mPopup.isShowing());
|
||||
}
|
||||
|
||||
public void show(long value) {
|
||||
mDelaySpinner.setValue(value);
|
||||
mPopup.showAsDropDown(mAnchor, 0, 0, Gravity.END);
|
||||
}
|
||||
}
|
||||
55
app/src/main/java/org/jellyfin/androidtv/ui/SettingsPopup.kt
Normal file
55
app/src/main/java/org/jellyfin/androidtv/ui/SettingsPopup.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.view.isVisible
|
||||
import org.jellyfin.androidtv.databinding.SettingsPopupBinding
|
||||
import org.jellyfin.androidtv.util.dp
|
||||
|
||||
class SettingsPopup(
|
||||
private val context: Context,
|
||||
private val anchor: View,
|
||||
audioChangedListener: ValueChangedListener<Long>?,
|
||||
subtitleChangedListener: ValueChangedListener<Long>?
|
||||
) {
|
||||
private val binding = SettingsPopupBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
.apply {
|
||||
subtitleDelay.increment = SUBTITLE_DELAY_INCREMENT
|
||||
subtitleDelay.valueChangedListener = subtitleChangedListener
|
||||
audioDelay.valueChangedListener = audioChangedListener
|
||||
}
|
||||
|
||||
val popupWindow = PopupWindow(binding.root, WIDTH.dp(context), FULL_HEIGHT.dp(context))
|
||||
.apply {
|
||||
isFocusable = true
|
||||
isOutsideTouchable = true
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
|
||||
fun show(
|
||||
hasSubtitles: Boolean,
|
||||
audioDelay: Long,
|
||||
subtitleDelay: Long
|
||||
) {
|
||||
binding.audioDelay.value = audioDelay
|
||||
|
||||
binding.subtitleDelayTxt.isVisible = hasSubtitles
|
||||
binding.subtitleDelay.isVisible = hasSubtitles
|
||||
binding.subtitleDelay.value = subtitleDelay
|
||||
|
||||
popupWindow.height = (if (hasSubtitles) FULL_HEIGHT else SINGLE_HEIGHT).dp(context)
|
||||
popupWindow.showAsDropDown(anchor, 0, 0, Gravity.END)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WIDTH = 240
|
||||
private const val FULL_HEIGHT = 230
|
||||
private const val SINGLE_HEIGHT = 130
|
||||
private const val SUBTITLE_DELAY_INCREMENT = 500L
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,14 @@ public class PlaybackController implements PlaybackControllerNotifiable {
|
||||
return hasInitializedVideoManager() ? mVideoManager.getAudioDelay() : 0;
|
||||
}
|
||||
|
||||
public void setSubtitleDelay(long value) {
|
||||
if (hasInitializedVideoManager()) mVideoManager.setSubtitleDelay(value);
|
||||
}
|
||||
|
||||
public long getSubtitleDelay() {
|
||||
return hasInitializedVideoManager() ? mVideoManager.getSubtitleDelay() : 0;
|
||||
}
|
||||
|
||||
public void playerErrorEncountered() {
|
||||
if (isNativeMode()) exoErrorEncountered = true;
|
||||
else vlcErrorEncountered = true;
|
||||
|
||||
@@ -680,6 +680,20 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
}
|
||||
}
|
||||
|
||||
public void setSubtitleDelay(long value) {
|
||||
if (!nativeMode && mVlcPlayer != null) {
|
||||
if (!mVlcPlayer.setSpuDelay(value * 1000)) {
|
||||
Timber.e("Error setting subtitle offset");
|
||||
} else {
|
||||
Timber.i("Subtitle offset set to %d", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long getSubtitleDelay() {
|
||||
return mVlcPlayer != null ? mVlcPlayer.getSpuDelay() / 1000 : 0;
|
||||
}
|
||||
|
||||
public void setAudioDelay(long value) {
|
||||
if (!nativeMode && mVlcPlayer != null) {
|
||||
if (!mVlcPlayer.setAudioDelay(value * 1000)) {
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior;
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.action.AdjustAudioDelayAction;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.action.ChannelBarChannelAction;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.action.ChapterAction;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.action.ClosedCaptionsAction;
|
||||
@@ -37,6 +36,7 @@ import org.jellyfin.androidtv.ui.playback.overlay.action.PlaybackSpeedAction;
|
||||
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.SettingAction;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.action.ZoomAction;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
@@ -51,7 +51,7 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
private PlaybackControlsRow.SkipNextAction skipNextAction;
|
||||
private SelectAudioAction selectAudioAction;
|
||||
private ClosedCaptionsAction closedCaptionsAction;
|
||||
private AdjustAudioDelayAction adjustAudioDelayAction;
|
||||
private SettingAction settingAction;
|
||||
private PlaybackSpeedAction playbackSpeedAction;
|
||||
private ZoomAction zoomAction;
|
||||
private ChapterAction chapterAction;
|
||||
@@ -179,8 +179,8 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
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)});
|
||||
adjustAudioDelayAction = new AdjustAudioDelayAction(context, this);
|
||||
adjustAudioDelayAction.setLabels(new String[]{context.getString(R.string.lbl_audio_delay)});
|
||||
settingAction = new SettingAction(context, this);
|
||||
settingAction.setLabels(new String[]{context.getString(R.string.lbl_adjust)});
|
||||
playbackSpeedAction = new PlaybackSpeedAction(context, this, playbackController);
|
||||
playbackSpeedAction.setLabels(new String[]{context.getString(R.string.lbl_playback_speed)});
|
||||
zoomAction = new ZoomAction(context, this);
|
||||
@@ -262,7 +262,7 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
|
||||
|
||||
if (!isNativeMode()) {
|
||||
secondaryActionsAdapter.add(adjustAudioDelayAction);
|
||||
secondaryActionsAdapter.add(settingAction);
|
||||
} else {
|
||||
secondaryActionsAdapter.add(zoomAction);
|
||||
}
|
||||
@@ -296,6 +296,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
} else if (action == closedCaptionsAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
closedCaptionsAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
} else if (action == settingAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
settingAction.setSubtitlesPresent(hasSubs());
|
||||
settingAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
} else if (action == playbackSpeedAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
playbackSpeedAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
@@ -303,9 +307,6 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
// 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 == adjustAudioDelayAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
adjustAudioDelayAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
} else if (action == zoomAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
zoomAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.playback.overlay.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.ui.AudioDelayPopup;
|
||||
import org.jellyfin.androidtv.ui.ValueChangedListener;
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue;
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment;
|
||||
|
||||
public class AdjustAudioDelayAction extends CustomAction {
|
||||
|
||||
public AdjustAudioDelayAction(Context context, CustomPlaybackTransportControlGlue customPlaybackTransportControlGlue) {
|
||||
super(context, customPlaybackTransportControlGlue);
|
||||
initializeWithIcon(R.drawable.ic_adjust);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClickAction(PlaybackController playbackController, LeanbackOverlayFragment leanbackOverlayFragment, Context context, View view) {
|
||||
AudioDelayPopup audioDelayPopup = new AudioDelayPopup(context, view, new ValueChangedListener<Long>() {
|
||||
@Override
|
||||
public void onValueChanged(Long value) {
|
||||
playbackController.setAudioDelay(value);
|
||||
}
|
||||
});
|
||||
|
||||
PopupWindow popupWindow = audioDelayPopup.getPopupWindow();
|
||||
if (popupWindow != null) {
|
||||
popupWindow.setOnDismissListener(() -> {
|
||||
leanbackOverlayFragment.setFading(true);
|
||||
});
|
||||
}
|
||||
|
||||
audioDelayPopup.show(playbackController.getAudioDelay());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.jellyfin.androidtv.ui.playback.overlay.action
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.SettingsPopup
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue
|
||||
import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment
|
||||
|
||||
class SettingAction(
|
||||
context: Context,
|
||||
customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue
|
||||
) : CustomAction(context, customPlaybackTransportControlGlue) {
|
||||
var subtitlesPresent = false
|
||||
|
||||
init {
|
||||
initializeWithIcon(R.drawable.ic_adjust)
|
||||
}
|
||||
|
||||
override fun handleClickAction(
|
||||
playbackController: PlaybackController,
|
||||
leanbackOverlayFragment: LeanbackOverlayFragment,
|
||||
context: Context,
|
||||
view: View
|
||||
) = SettingsPopup(context, view,
|
||||
{ value -> playbackController.audioDelay = value },
|
||||
{ value -> playbackController.subtitleDelay = value }
|
||||
).apply {
|
||||
popupWindow.setOnDismissListener { leanbackOverlayFragment.setFading(true) }
|
||||
}.show(
|
||||
subtitlesPresent,
|
||||
playbackController.audioDelay,
|
||||
playbackController.subtitleDelay
|
||||
)
|
||||
}
|
||||
@@ -151,6 +151,17 @@ class PlaybackPreferencesScreen : OptionsFragment() {
|
||||
bind(userPreferences, UserPreferences.subtitlesBackgroundEnabled)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
seekbar {
|
||||
setTitle(R.string.pref_libvlc_subtitle_delay_title)
|
||||
min = -50_000
|
||||
max = 50_000
|
||||
valueFormatter = object : DurationSeekBarPreference.ValueFormatter() {
|
||||
override fun display(value: Int) = "${value}ms"
|
||||
}
|
||||
bind(userPreferences, UserPreferences.libVLCSubtitleDelay)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
seekbar {
|
||||
setTitle(R.string.pref_subtitles_size)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/popupWindowBackground"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/lbl_audio_delay"
|
||||
android:id="@+id/textView18"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true" />
|
||||
|
||||
<org.jellyfin.androidtv.ui.NumberSpinnerView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/numberSpinner"
|
||||
android:layout_below="@+id/textView18"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="15sp" />
|
||||
</RelativeLayout>
|
||||
42
app/src/main/res/layout/settings_popup.xml
Normal file
42
app/src/main/res/layout/settings_popup.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/popupWindowBackground"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/audioDelaytxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/lbl_audio_delay"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<org.jellyfin.androidtv.ui.NumberSpinnerView
|
||||
android:id="@+id/audioDelay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/audioDelaytxt"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitleDelayTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="15sp"
|
||||
android:layout_below="@+id/audioDelay"
|
||||
android:text="@string/lbl_subtitle_delay"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<org.jellyfin.androidtv.ui.NumberSpinnerView
|
||||
android:id="@+id/subtitleDelay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/subtitleDelayTxt"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="15sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -261,6 +261,8 @@
|
||||
<string name="lbl_news">News</string>
|
||||
<string name="lbl_new_only">New Only</string>
|
||||
<string name="lbl_audio_delay">Audio Delay</string>
|
||||
<string name="lbl_adjust">Adjust</string>
|
||||
<string name="lbl_subtitle_delay">Subtitle Delay</string>
|
||||
<string name="lbl_new_episodes">New Episodes</string>
|
||||
<string name="lbl_premieres">Premieres</string>
|
||||
<string name="lbl_repeat_episodes">Repeat Episodes</string>
|
||||
@@ -285,7 +287,8 @@
|
||||
<string name="pref_next_up_timeout_disabled">∞</string>
|
||||
<string name="watch_now">Watch now</string>
|
||||
<string name="pref_next_up_behavior_title">Show next video info</string>
|
||||
<string name="pref_libvlc_audio_delay_title">Default Audio Delay for LibVLC</string>
|
||||
<string name="pref_libvlc_audio_delay_title">Default audio delay for LibVLC</string>
|
||||
<string name="pref_libvlc_subtitle_delay_title">Default subtitle delay for LibVLC</string>
|
||||
<string name="pref_app_theme">App theme</string>
|
||||
<string name="pref_device_model">Device model</string>
|
||||
<string name="grid_direction">Grid Direction</string>
|
||||
|
||||
Reference in New Issue
Block a user