Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3cfcd9f
Add seek functionality for playback controls in CustomPlaybackOverlay…
itayavra Jul 11, 2025
74f5a85
Add seek confirmation feature for D-pad navigation in playback controls
itayavra Jul 11, 2025
5eb1af3
Remove seek confirmation feature from playback controls
itayavra Jul 11, 2025
9a2ab3a
Add accelerated seeking functionality to playback controls
itayavra Jul 11, 2025
bbd725f
Refactor seeking functionality in CustomPlaybackOverlayFragment
itayavra Jul 11, 2025
69513d9
Refactor playback control methods in CustomPlaybackOverlayFragment
itayavra Jul 11, 2025
06332c5
Refactor getSafeSeekPosition method in Utils.kt
itayavra Jul 11, 2025
cdd8355
Add settings to enable the enhanced D-pad seeking logic
itayavra Jul 11, 2025
84077ae
fix: Revert unrelated changes and preserve original D-pad behavior
itayavra Jul 11, 2025
94f4ab5
refactor: Rename to natural D-pad seeking and extract preference vari…
itayavra Jul 11, 2025
1838f9a
refactor: Update comments for seeking behavior in CustomPlaybackOverl…
itayavra Jul 11, 2025
4ec8dcf
fix: fixed compile
itayavra Jul 11, 2025
7db8934
refactor: code cleanups and better UX by lowering the seek debounce time
itayavra Jul 11, 2025
ff0d1fe
refactor: code review fixes
itayavra Jul 11, 2025
241431a
refactor: update playback control to use ProgressBar and fix lint errors
itayavra Jul 13, 2025
64d05a1
fix: fixed runtime error when playing video
itayavra Jul 13, 2025
e6d4f58
feat: implement preview seeking functionality in playback controls
itayavra Jul 13, 2025
353a90f
refactor: merged the natural d-pad seeking preference as the only one…
itayavra Jul 13, 2025
d576e90
fix: update seeking methods to use PlayerAdapter again for improved UX
itayavra Jul 13, 2025
0f7214a
feat: add skip back length preference to playback settings
itayavra Jul 13, 2025
690c7fa
chore: removed empty lines and added nullability annotation in Leanba…
itayavra Jul 13, 2025
078e8a8
fix: bugfix and preview-seek key handling improvements
itayavra Jul 13, 2025
db88fd7
refactor: clean up comments
itayavra Jul 13, 2025
c15d53b
refactor: clean up preview seeking code
itayavra Jul 16, 2025
3d30729
refactor: clean up preview seeking code
itayavra Jul 16, 2025
79e5028
refactor: clean up preview seeking code
itayavra Jul 16, 2025
032191f
feat: added a simple thumbnail preview for trickplay when preview see…
itayavra Jul 16, 2025
bd72a74
refactor: moved thumbnails implementation into CustomPlaybackTranspor…
itayavra Jul 16, 2025
eb49c2f
refactor: extracted code to the glue and to a new handler for thumbna…
itayavra Jul 17, 2025
59b7252
refactor: fixed seeking on seekbar focus, simplified the glue code a bit
itayavra Jul 17, 2025
261ddb3
refactor: update thumbnail position and dimensions, enabled thumbnail…
itayavra Jul 17, 2025
19fab58
refactor: adjust thumbnail dimensions and background to hide black ba…
itayavra Jul 18, 2025
bccb3cc
refactor: code cleanup
itayavra Jul 18, 2025
b990ad9
refactor: code cleanup
itayavra Jul 18, 2025
c24be06
refactor: code cleanup
itayavra Jul 18, 2025
42f7ce2
refactor: code cleanup
itayavra Jul 18, 2025
e836bf7
refactor: code cleanup
itayavra Jul 18, 2025
e6cf6fc
refactor: update preview seeking confirmation text for clarity
itayavra Jul 19, 2025
7170ad9
refactor: rename preview seeking to seek confirmation for better term…
itayavra Jul 19, 2025
5217bc0
refactor: make sure the thumbnail preview is removed if exists when p…
itayavra Jul 19, 2025
c574a0b
refactor: shorter seek confirmation text
itayavra Jul 19, 2025
32f2f92
refactor: remove extra newline
itayavra Jul 20, 2025
6280e37
cr fixes - converted ThumbnailPreviewHandler to kt
itayavra Jul 31, 2025
a9b475e
refactor:
itayavra Jul 31, 2025
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
Expand Up @@ -252,6 +252,12 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
* Enable PGS subtitle direct-play.
*/
var pgsDirectPlay = booleanPreference("pgs_enabled", true)

/**
* Require confirmation before seeking to allows users to preview seek positions before confirming them.
* When disabled, D-pad left/right will seek immediately.
*/
var seekConfirmationRequired = booleanPreference("seek_confirmation", true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should choose one behavior for the seek control and stick with that. No preferences to complicate it necessary.

Copy link
Author

@itayavra itayavra Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, the intuitive behavior would be to pause the video when seeking, especially when thumbnail previews can be utilized, this is what I'm used to from netflix, youtube (well, smart tube), etc.
However, I saw that there were some issues open specifically about that by users that prefer the other behavior (#2399, #3154, ...).
I can leave only one option, but I would consider giving the option to choose, since it seems like it would allow more users to feel at home/enjoy the behavior they're used to (especially given supporting both behaviors in the code was pretty easy) 🙏 .
Anyway, let me know what you decide.

}

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@

import kotlin.Lazy;
import timber.log.Timber;
import org.jellyfin.androidtv.preference.UserSettingPreferences;
import org.jellyfin.androidtv.preference.UserPreferences;

public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGuide, View.OnKeyListener {
protected VlcPlayerInterfaceBinding binding;
Expand Down Expand Up @@ -125,6 +127,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
private boolean mIsVisible = false;
private boolean mPopupPanelVisible = false;
private boolean navigating = false;
private boolean mPendingSeekConfirmation = false;

protected LeanbackOverlayFragment leanbackOverlayFragment;

Expand All @@ -136,6 +139,8 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
private final Lazy<NavigationRepository> navigationRepository = inject(NavigationRepository.class);
private final Lazy<BackgroundService> backgroundService = inject(BackgroundService.class);
private final Lazy<ImageHelper> imageHelper = inject(ImageHelper.class);
private final Lazy<UserPreferences> userPreferences = inject(UserPreferences.class);
private final Lazy<UserSettingPreferences> userSettingPreferences = inject(UserSettingPreferences.class);

private final PlaybackOverlayFragmentHelper helper = new PlaybackOverlayFragmentHelper(this);

Expand Down Expand Up @@ -164,6 +169,7 @@ public void onCreate(Bundle savedInstanceState) {

// setup fade task
mHideTask = () -> {
leanbackOverlayFragment.getPlayerGlue().hideThumbnailPreview();
if (mIsVisible) {
leanbackOverlayFragment.hideOverlay();
}
Expand Down Expand Up @@ -499,6 +505,21 @@ public boolean onKey(View v, int keyCode, KeyEvent event) {
}
}

// Handle seek confirmation/cancellation
if (mPendingSeekConfirmation) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER ||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
applyPendingSeek();
return true;
}

if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B ||
keyCode == KeyEvent.KEYCODE_ESCAPE) {
exitSeekConfirmationMode();
return true;
}
}

if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) {
closePlayer();
return true;
Expand Down Expand Up @@ -587,19 +608,56 @@ public boolean onKey(View v, int keyCode, KeyEvent event) {
}
}

if (!mIsVisible) {
if (!playbackControllerContainer.getValue().getPlaybackController().isLiveTv()) {
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if ((!mIsVisible || isSeekBarFocused()) && !playbackControllerContainer.getValue().getPlaybackController().isLiveTv()) {
boolean seekConfirmationRequired = userPreferences.getValue().get(UserPreferences.Companion.getSeekConfirmationRequired());

if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {

if (seekConfirmationRequired) {
if (!mPendingSeekConfirmation) {
enterSeekConfirmationMode();
}

if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
UserSettingPreferences prefs = userSettingPreferences.getValue();
long skipAmount = prefs.get(UserSettingPreferences.Companion.getSkipForwardLength());
leanbackOverlayFragment.getPlayerGlue().previewSeek(skipAmount);
}

setFadingEnabled(true);
return true;
}

if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
// Immediate seeking
leanbackOverlayFragment.getPlayerGlue().fastForward();
setFadingEnabled(true);
return true;
}

if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {

if (seekConfirmationRequired) {
if (!mPendingSeekConfirmation) {
enterSeekConfirmationMode();
}

if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
UserSettingPreferences prefs = userSettingPreferences.getValue();
long skipAmount = prefs.get(UserSettingPreferences.Companion.getSkipBackLength());
leanbackOverlayFragment.getPlayerGlue().previewSeek(-skipAmount);
}
setFadingEnabled(true);
return true;
}

// Immediate seeking
leanbackOverlayFragment.getPlayerGlue().rewind();
setFadingEnabled(true);
return true;
}
}

if (!mIsVisible) {
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER)
&& playbackControllerContainer.getValue().getPlaybackController().canSeek()) {
// if the player is playing and the overlay is hidden, this will pause
Expand All @@ -614,16 +672,41 @@ public boolean onKey(View v, int keyCode, KeyEvent event) {
}
}

switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
leanbackOverlayFragment.getPlayerGlue().setInjectedViewsVisibility();
}

return false;
}
};


private boolean isSeekBarFocused() {
if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
return leanbackOverlayFragment.getPlayerGlue().isSeekBarFocused();
}
return false;
}

private void enterSeekConfirmationMode() {
mPendingSeekConfirmation = true;
if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
leanbackOverlayFragment.getPlayerGlue().enterSeekConfirmationMode();
}
}

private void exitSeekConfirmationMode() {
mPendingSeekConfirmation = false;
if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
leanbackOverlayFragment.getPlayerGlue().exitSeekConfirmationMode();
}
}

private void applyPendingSeek() {
if (mPendingSeekConfirmation) {
if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
leanbackOverlayFragment.getPlayerGlue().applyPendingSeek();
}
mPendingSeekConfirmation = false;
}
}

public LocalDateTime getCurrentLocalStartDate() {
return mCurrentGuideStart;
}
Expand Down Expand Up @@ -729,6 +812,10 @@ public void hide() {
mIsVisible = false;
binding.topPanel.startAnimation(fadeOut);
binding.skipOverlay.setSkipUiEnabled(!mIsVisible && !mGuideVisible && !mPopupPanelVisible);

if (leanbackOverlayFragment != null && leanbackOverlayFragment.getPlayerGlue() != null) {
leanbackOverlayFragment.getPlayerGlue().hideThumbnailPreview();
}
}

private void showChapterPanel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public class PlaybackController implements PlaybackControllerNotifiable {
private long mSeekPosition = -1;
private boolean wasSeeking = false;
private boolean finishedInitialSeek = false;
private boolean mPendingSeekConfirmation = false;

private LocalDateTime mCurrentProgramEnd = null;
private LocalDateTime mCurrentProgramStart = null;
Expand All @@ -104,6 +105,7 @@ public class PlaybackController implements PlaybackControllerNotifiable {
private boolean directStreamLiveTv;
private int playbackRetries = 0;
private long lastPlaybackError = 0;
private long mPendingSeekPosition = -1;

private Display.Mode[] mDisplayModes;
private RefreshRateSwitchingBehavior refreshRateSwitchingBehavior = RefreshRateSwitchingBehavior.DISABLED;
Expand Down Expand Up @@ -1284,9 +1286,15 @@ public long getBufferedPosition() {
return bufferedPosition;
}


public long getCurrentPosition() {
// don't report the real position if seeking
return !isPlaying() && mSeekPosition != -1 ? mSeekPosition : mCurrentPosition;
// Return pending seek position if in pending seek confirmation mode
if (mPendingSeekConfirmation && mPendingSeekPosition != -1) {
return mPendingSeekPosition;
}

refreshCurrentPosition();
return mCurrentPosition;
}

public boolean isPaused() {
Expand All @@ -1302,6 +1310,20 @@ public void setZoom(@NonNull ZoomMode mode) {
mVideoManager.setZoom(mode);
}

public void setPendingSeekPosition(long position) {
mPendingSeekPosition = position;
mPendingSeekConfirmation = true;
}

public void clearPendingSeekPosition() {
mPendingSeekPosition = -1;
mPendingSeekConfirmation = false;
}

public long getPendingSeekPosition() {
return mPendingSeekPosition;
}

/**
* List of various states that we can be in
*/
Expand Down
Loading