Skip to content

Commit

Permalink
Add preference to delay start of video (jellyfin#4025)
Browse files Browse the repository at this point in the history
* Add setting to adjust video player start delay

* Update PlaybackController to skip video start delay if set to zero.
Update default for video start delay.
Update video start delay string.

* Update mVideoManager null check for delayed start logic.

* Final changes

---------

Co-authored-by: Niels van Velzen <[email protected]>
  • Loading branch information
2 people authored and jumoog committed Oct 4, 2024
1 parent 39ad527 commit 4d321e2
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
* Whether items shown in the screensaver are required to have an age rating set.
*/
var screensaverAgeRatingRequired = booleanPreference("screensaver_agerating_required", true)

/**
* Delay when starting video playback after loading the video player.
*/
var videoStartDelay = longPreference("video_start_delay", 0)
}

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

import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Handler;
import android.view.Display;
import android.view.WindowManager;
Expand Down Expand Up @@ -57,13 +58,13 @@ public class PlaybackController implements PlaybackControllerNotifiable {
// Frequency to report paused state
private static final long PROGRESS_REPORTING_PAUSE_INTERVAL = TimeUtils.secondsToMillis(15);

private final Lazy<ApiClient> apiClient = inject(ApiClient.class);
private final Lazy<PlaybackManager> playbackManager = inject(PlaybackManager.class);
private final Lazy<UserPreferences> userPreferences = inject(UserPreferences.class);
private final Lazy<VideoQueueManager> videoQueueManager = inject(VideoQueueManager.class);
private final Lazy<org.jellyfin.sdk.api.client.ApiClient> api = inject(org.jellyfin.sdk.api.client.ApiClient.class);
private final Lazy<DataRefreshService> dataRefreshService = inject(DataRefreshService.class);
private final Lazy<ReportingHelper> reportingHelper = inject(ReportingHelper.class);
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
private Lazy<PlaybackManager> playbackManager = inject(PlaybackManager.class);
private Lazy<UserPreferences> userPreferences = inject(UserPreferences.class);
private Lazy<VideoQueueManager> videoQueueManager = inject(VideoQueueManager.class);
private Lazy<org.jellyfin.sdk.api.client.ApiClient> api = inject(org.jellyfin.sdk.api.client.ApiClient.class);
private Lazy<DataRefreshService> dataRefreshService = inject(DataRefreshService.class);
private Lazy<ReportingHelper> reportingHelper = inject(ReportingHelper.class);

List<BaseItemDto> mItems;
VideoManager mVideoManager;
Expand Down Expand Up @@ -103,13 +104,13 @@ public class PlaybackController implements PlaybackControllerNotifiable {
private long lastPlaybackError = 0;

private Display.Mode[] mDisplayModes;
private RefreshRateSwitchingBehavior refreshRateSwitchingBehavior;
private RefreshRateSwitchingBehavior refreshRateSwitchingBehavior = RefreshRateSwitchingBehavior.DISABLED;

public PlaybackController(List<BaseItemDto> items, CustomPlaybackOverlayFragment fragment) {
this(items, fragment, 0);
}

public PlaybackController(List<BaseItemDto> items, @Nullable CustomPlaybackOverlayFragment fragment, int startIndex) {
public PlaybackController(List<BaseItemDto> items, CustomPlaybackOverlayFragment fragment, int startIndex) {
mItems = items;
mCurrentIndex = 0;
if (items != null && startIndex > 0 && startIndex < items.size()) {
Expand Down Expand Up @@ -255,14 +256,14 @@ public void playerErrorEncountered() {

if (playbackRetries < 3) {
if (mFragment != null)
Utils.showToast(mFragment.requireContext(), R.string.player_error);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.player_error));
Timber.i("Player error encountered - retrying");
stop();
play(mCurrentPosition);
} else {
mPlaybackState = PlaybackState.ERROR;
if (mFragment != null) {
Utils.showToast(mFragment.requireContext(), R.string.too_many_errors);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.too_many_errors));
mFragment.closePlayer();
}
}
Expand Down Expand Up @@ -441,26 +442,41 @@ private void play(long position, @Nullable Integer forcedSubtitleIndex) {

if (item == null) {
Timber.d("item is null - aborting play");
Utils.showToast(mFragment.requireContext(), mFragment.getString(R.string.msg_cannot_play));
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_cannot_play));
mFragment.closePlayer();
return;
}

// make sure item isn't missing
if (item.getLocationType() == LocationType.VIRTUAL) {
if (hasNextItem()) {
new AlertDialog.Builder(mFragment.requireContext())
new AlertDialog.Builder(mFragment.getContext())
.setTitle(R.string.episode_missing)
.setMessage(R.string.episode_missing_message)
.setPositiveButton(R.string.lbl_yes, (dialog, which) -> next())
.setNegativeButton(R.string.lbl_no, (dialog, which) -> mFragment.closePlayer())
.setPositiveButton(R.string.lbl_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
next();
}
})
.setNegativeButton(R.string.lbl_no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mFragment.closePlayer();
}
})
.create()
.show();
} else {
new AlertDialog.Builder(mFragment.requireContext())
new AlertDialog.Builder(mFragment.getContext())
.setTitle(R.string.episode_missing)
.setMessage(R.string.episode_missing_message_2)
.setPositiveButton(R.string.lbl_ok, (dialog, which) -> mFragment.closePlayer())
.setPositiveButton(R.string.lbl_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mFragment.closePlayer();
}
})
.create()
.show();
}
Expand Down Expand Up @@ -560,19 +576,19 @@ private void handlePlaybackInfoError(Exception exception) {
PlaybackException ex = (PlaybackException) exception;
switch (ex.getErrorCode()) {
case NotAllowed:
Utils.showToast(mFragment.requireContext(), R.string.msg_playback_not_allowed);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_playback_not_allowed));
break;
case NoCompatibleStream:
Utils.showToast(mFragment.requireContext(), R.string.msg_playback_incompatible);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_playback_incompatible));
break;
case RateLimitExceeded:
Utils.showToast(mFragment.requireContext(), R.string.msg_playback_restricted);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_playback_restricted));
break;
}
} else {
Utils.showToast(mFragment.requireContext(), R.string.msg_cannot_play);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_cannot_play));
}
mFragment.closePlayer();
if (mFragment != null) mFragment.closePlayer();
}

private void startItem(BaseItemDto item, long position, StreamInfo response) {
Expand Down Expand Up @@ -606,7 +622,7 @@ private void startItem(BaseItemDto item, long position, StreamInfo response) {
mCurrentOptions.setSubtitleStreamIndex(null);
}

long mbPos = position * 10000;
Long mbPos = position * 10000;

// set refresh rate
if (refreshRateSwitchingBehavior != RefreshRateSwitchingBehavior.DISABLED) {
Expand All @@ -624,11 +640,20 @@ private void startItem(BaseItemDto item, long position, StreamInfo response) {
mFragment.onStartItem(item);
}

//wait a beat before attempting to start so the player surface is fully initialized and video is ready
mHandler.postDelayed(() -> {
if (mVideoManager != null)
mVideoManager.start();
}, 750);
// Set video start delay
long videoStartDelay = userPreferences.getValue().get(UserPreferences.Companion.getVideoStartDelay());
if (videoStartDelay > 0) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mVideoManager != null) {
mVideoManager.start();
}
}
}, videoStartDelay);
} else {
mVideoManager.start();
}

dataRefreshService.getValue().setLastPlayedItem(item);
reportingHelper.getValue().reportStart(item, mbPos);
Expand Down Expand Up @@ -747,13 +772,13 @@ public void switchSubtitleStream(int index) {
org.jellyfin.sdk.model.api.MediaStream stream = StreamHelper.getMediaStream(getCurrentMediaSource(), index);
if (stream == null) {
if (mFragment != null)
Utils.showToast(mFragment.requireContext(), R.string.subtitle_error);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.subtitle_error));
return;
}
SubtitleStreamInfo streamInfo = getSubtitleStreamInfo(index);
if (streamInfo == null) {
if (mFragment != null)
Utils.showToast(mFragment.requireContext(), R.string.msg_unable_load_subs);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
return;
}

Expand All @@ -762,7 +787,7 @@ public void switchSubtitleStream(int index) {
if (burningSubs || streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode) {
stop();
if (mFragment != null && streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode)
Utils.showToast(mFragment.requireContext(), R.string.msg_burn_sub_warning);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
play(mCurrentPosition, index);
return;
}
Expand All @@ -774,7 +799,7 @@ public void switchSubtitleStream(int index) {
if (!mVideoManager.setExoPlayerTrack(index, MediaStreamType.SUBTITLE, getCurrentlyPlayingItem().getMediaStreams())) {
// error selecting internal subs
if (mFragment != null)
Utils.showToast(mFragment.requireContext(), R.string.msg_unable_load_subs);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
} else {
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;
Expand Down Expand Up @@ -824,7 +849,7 @@ public void stop() {
mPlaybackState = PlaybackState.IDLE;

if (mVideoManager != null && mVideoManager.isPlaying()) mVideoManager.stopPlayback();
long mbPos = mCurrentPosition * 10000;
Long mbPos = mCurrentPosition * 10000;
reportingHelper.getValue().reportStopped(getCurrentlyPlayingItem(), getCurrentStreamInfo(), mbPos);
clearPlaybackSessionOptions();
}
Expand Down Expand Up @@ -881,7 +906,7 @@ public void next() {

public void prev() {
Timber.d("Prev called.");
if (mCurrentIndex > 0 && !mItems.isEmpty()) {
if (mCurrentIndex > 0 && mItems.size() > 0) {
stop();
resetPlayerErrors();
mCurrentIndex--;
Expand Down Expand Up @@ -946,7 +971,7 @@ public void onResponse(StreamInfo response) {
@Override
public void onError(Exception exception) {
if (mFragment != null)
Utils.showToast(mFragment.requireContext(), R.string.msg_video_playback_error);
Utils.showToast(mFragment.getContext(), R.string.msg_video_playback_error);
Timber.e(exception, "Error trying to seek transcoded stream");
// call stop so playback can be retried by the user
stop();
Expand All @@ -961,7 +986,7 @@ public void onError(Exception exception) {
mPlaybackState = PlaybackState.SEEKING;
if (mVideoManager.seekTo(pos) < 0) {
if (mFragment != null)
Utils.showToast(mFragment.requireContext(), R.string.seek_error);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.seek_error));
pause();
} else {
mVideoManager.play();
Expand Down Expand Up @@ -1157,24 +1182,19 @@ public void onPrepared() {
}

// select an audio track
int eligibleAudioTrack = getEligibleAudioTrack();
int eligibleAudioTrack = mDefaultAudioIndex;

// if track switching is done without rebuilding the stream, mCurrentOptions is updated
// otherwise, use the server default
if (mCurrentOptions.getAudioStreamIndex() != null) {
eligibleAudioTrack = mCurrentOptions.getAudioStreamIndex();
} else if (getCurrentMediaSource().getDefaultAudioStreamIndex() != null) {
eligibleAudioTrack = getCurrentMediaSource().getDefaultAudioStreamIndex();
}
switchAudioStream(eligibleAudioTrack);
}
}

private int getEligibleAudioTrack() {
int eligibleAudioTrack = mDefaultAudioIndex;

// if track switching is done without rebuilding the stream, mCurrentOptions is updated
// otherwise, use the server default
if (mCurrentOptions.getAudioStreamIndex() != null) {
eligibleAudioTrack = mCurrentOptions.getAudioStreamIndex();
} else if (getCurrentMediaSource().getDefaultAudioStreamIndex() != null) {
eligibleAudioTrack = getCurrentMediaSource().getDefaultAudioStreamIndex();
}
return eligibleAudioTrack;
}

@Override
public void onError() {
if (mFragment == null) {
Expand All @@ -1183,7 +1203,7 @@ public void onError() {
}

if (isLiveTv && directStreamLiveTv) {
Utils.showToast(mFragment.requireContext(), R.string.msg_error_live_stream);
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_error_live_stream));
directStreamLiveTv = false;
} else {
String msg = mFragment.getString(R.string.video_error_unknown_error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.getQualityProfiles
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.ui.preference.custom.DurationSeekBarPreference
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.enum
import org.jellyfin.androidtv.ui.preference.dsl.list
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.jellyfin.androidtv.ui.preference.dsl.seekbar
import org.jellyfin.androidtv.util.TimeUtils
import org.koin.android.ext.android.inject

Expand Down Expand Up @@ -60,6 +62,22 @@ class PlaybackAdvancedPreferencesScreen : OptionsFragment() {
bind(userPreferences, UserPreferences.refreshRateSwitchingBehavior)
}

@Suppress("MagicNumber")
seekbar {
setTitle(R.string.video_start_delay)
min = 0
max = 5_000
increment = 250
valueFormatter = object : DurationSeekBarPreference.ValueFormatter() {
override fun display(value: Int): String = "${value.toDouble() / 1000}s"
}
bind {
get { userPreferences[UserPreferences.videoStartDelay].toInt() }
set { value -> userPreferences[UserPreferences.videoStartDelay] = value.toLong() }
default { UserPreferences.videoStartDelay.defaultValue.toInt() }
}
}

checkbox{
setTitle(R.string.pref_external_player)
bind(userPreferences, UserPreferences.useExternalPlayer)
Expand Down
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 @@ -516,6 +516,7 @@
<string name="past_24_hours">Past 24 hours</string>
<string name="prefer_exoplayer_ffmpeg">Prefer FFmpeg for audio playback</string>
<string name="prefer_exoplayer_ffmpeg_content">Use FFmpeg to decode audio, even if platform codecs are available.</string>
<string name="video_start_delay">Video start delay</string>
<plurals name="seconds">
<item quantity="one">%1$s second</item>
<item quantity="other">%1$s seconds</item>
Expand Down

0 comments on commit 4d321e2

Please sign in to comment.