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

Rewrite all subtitle behavior #4094

Merged
merged 3 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -1,13 +1,11 @@
package org.jellyfin.androidtv.data.compat;

import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dlna.EncodingContext;
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
import org.jellyfin.apiclient.model.dlna.SubtitleProfile;
import org.jellyfin.apiclient.model.dlna.TranscodeSeekInfo;
import org.jellyfin.apiclient.model.session.PlayMethod;
import org.jellyfin.sdk.model.api.MediaProtocol;
import org.jellyfin.sdk.model.api.MediaSourceInfo;
import org.jellyfin.sdk.model.api.MediaStream;
import org.jellyfin.sdk.model.api.MediaStreamType;
Expand Down Expand Up @@ -66,16 +64,6 @@ public final void setContainer(String value) {
Container = value;
}

private long StartPositionTicks;

public final long getStartPositionTicks() {
return StartPositionTicks;
}

public final void setStartPositionTicks(long value) {
StartPositionTicks = value;
}

private DeviceProfile DeviceProfile;

public final DeviceProfile getDeviceProfile() {
Expand Down Expand Up @@ -139,59 +127,29 @@ public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSe
public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSelectedTrackOnly, boolean enableAllProfiles, String baseUrl, String accessToken) {
ArrayList<SubtitleStreamInfo> list = new ArrayList<SubtitleStreamInfo>();

// HLS will preserve timestamps so we can just grab the full subtitle stream
long startPositionTicks = getPlayMethod() == PlayMethod.Transcode ? getStartPositionTicks() : 0;

if (!includeSelectedTrackOnly) {
if (getMediaSource() == null) return list;

for (org.jellyfin.sdk.model.api.MediaStream stream : getMediaSource().getMediaStreams()) {
if (stream.getType() == org.jellyfin.sdk.model.api.MediaStreamType.SUBTITLE) {
addSubtitleProfiles(list, stream, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
SubtitleStreamInfo info = getSubtitleStreamInfo(stream, getDeviceProfile().getSubtitleProfiles());
list.add(info);
}
}
}

return list;
}

private void addSubtitleProfiles(ArrayList<SubtitleStreamInfo> list, org.jellyfin.sdk.model.api.MediaStream stream, boolean enableAllProfiles, String baseUrl, String accessToken, long startPositionTicks) {
if (enableAllProfiles) {
for (SubtitleProfile profile : getDeviceProfile().getSubtitleProfiles()) {
SubtitleStreamInfo info = getSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new SubtitleProfile[]{profile});

list.add(info);
}
} else {
SubtitleStreamInfo info = getSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, getDeviceProfile().getSubtitleProfiles());

list.add(info);
}
}

private SubtitleStreamInfo getSubtitleStreamInfo(org.jellyfin.sdk.model.api.MediaStream stream, String baseUrl, String accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles) {
private SubtitleStreamInfo getSubtitleStreamInfo(org.jellyfin.sdk.model.api.MediaStream stream, SubtitleProfile[] subtitleProfiles) {
SubtitleProfile subtitleProfile = StreamBuilder.getSubtitleProfile(stream, subtitleProfiles, getPlayMethod());
SubtitleStreamInfo tempVar = new SubtitleStreamInfo();
SubtitleStreamInfo info = new SubtitleStreamInfo();
String tempVar2 = stream.getLanguage();
tempVar.setName((tempVar2 != null) ? tempVar2 : "Unknown");
tempVar.setFormat(subtitleProfile.getFormat());
tempVar.setIndex(stream.getIndex());
tempVar.setDeliveryMethod(subtitleProfile.getMethod());
tempVar.setDisplayTitle(stream.getDisplayTitle());
SubtitleStreamInfo info = tempVar;

if (info.getDeliveryMethod() == SubtitleDeliveryMethod.External) {
if (getMediaSource().getProtocol() == MediaProtocol.FILE || !stream.getCodec().equalsIgnoreCase(subtitleProfile.getFormat())) {
info.setUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/%5$s/Stream.%6$s", baseUrl, getItemId(), getMediaSourceId(), String.valueOf(stream.getIndex()), String.valueOf(startPositionTicks), subtitleProfile.getFormat()));

if (!Utils.isEmpty(accessToken)) {
info.setUrl(info.getUrl() + "?api_key=" + accessToken);
}
} else {
info.setUrl(stream.getPath());
}
}

info.setName((tempVar2 != null) ? tempVar2 : "Unknown");
info.setFormat(subtitleProfile.getFormat());
info.setIndex(stream.getIndex());
info.setDeliveryMethod(subtitleProfile.getMethod());
info.setDisplayTitle(stream.getDisplayTitle());
return info;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer
import org.jellyfin.androidtv.ui.playback.setSubtitleIndex
import org.jellyfin.androidtv.util.PlaybackHelper
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
Expand Down Expand Up @@ -104,7 +105,7 @@ class SocketHandler(
val index = message["index"]?.toIntOrNull() ?: return@onEach

withContext(Dispatchers.Main) {
playbackControllerContainer.playbackController?.switchSubtitleStream(index)
playbackControllerContainer.playbackController?.setSubtitleIndex(index)
}
}
.launchIn(coroutineScope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@
private AudioOptions options;
private Response<StreamInfo> response;
private boolean isVideo;
private Long startPositionTicks;

public GetPlaybackInfoResponse(PlaybackManager playbackManager, DeviceInfo deviceInfo, ApiClient apiClient, AudioOptions options, Response<StreamInfo> response, boolean isVideo, Long startPositionTicks) {
public GetPlaybackInfoResponse(PlaybackManager playbackManager, DeviceInfo deviceInfo, ApiClient apiClient, AudioOptions options, Response<StreamInfo> response, boolean isVideo) {

Check notice

Code scanning / Android Lint

Unknown nullness Note

Unknown nullability; explicitly declare as @Nullable or @NonNull to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations

Check notice

Code scanning / Android Lint

Unknown nullness Note

Unknown nullability; explicitly declare as @Nullable or @NonNull to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations

Check notice

Code scanning / Android Lint

Unknown nullness Note

Unknown nullability; explicitly declare as @Nullable or @NonNull to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations

Check notice

Code scanning / Android Lint

Unknown nullness Note

Unknown nullability; explicitly declare as @Nullable or @NonNull to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations

Check notice

Code scanning / Android Lint

Unknown nullness Note

Unknown nullability; explicitly declare as @Nullable or @NonNull to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations
super(response);
this.playbackManager = playbackManager;
this.deviceInfo = deviceInfo;
this.apiClient = apiClient;
this.options = options;
this.response = response;
this.isVideo = isVideo;
this.startPositionTicks = startPositionTicks;
}

@Override
Expand Down Expand Up @@ -96,7 +94,6 @@
streamInfo.setItemId(options.getItemId());
streamInfo.setDeviceProfile(options.getProfile());
streamInfo.setPlaySessionId(playbackInfo.getPlaySessionId());
streamInfo.setStartPositionTicks(startPositionTicks);

if (options.getEnableDirectPlay() && mediaSourceInfo.getSupportsDirectPlay()){
if (canDirectPlay(mediaSourceInfo)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.data.compat.PlaybackException;
import org.jellyfin.androidtv.data.compat.StreamInfo;
import org.jellyfin.androidtv.data.compat.SubtitleStreamInfo;
import org.jellyfin.androidtv.data.compat.VideoOptions;
import org.jellyfin.androidtv.data.model.DataRefreshService;
import org.jellyfin.androidtv.preference.UserPreferences;
Expand All @@ -28,13 +27,11 @@
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import org.jellyfin.androidtv.util.profile.ExoPlayerProfile;
import org.jellyfin.androidtv.util.sdk.compat.JavaCompat;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
import org.jellyfin.apiclient.model.session.PlayMethod;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
Expand Down Expand Up @@ -70,20 +67,18 @@
List<BaseItemDto> mItems;
VideoManager mVideoManager;
int mCurrentIndex;
private long mCurrentPosition = 0;
protected long mCurrentPosition = 0;
private PlaybackState mPlaybackState = PlaybackState.IDLE;

private StreamInfo mCurrentStreamInfo;
private List<SubtitleStreamInfo> mSubtitleStreams;

@Nullable
private CustomPlaybackOverlayFragment mFragment;
private Boolean spinnerOff = false;

private VideoOptions mCurrentOptions;
private int mDefaultSubIndex = -1;
protected VideoOptions mCurrentOptions;

Check notice

Code scanning / Android Lint

Unknown nullness Note

Unknown nullability; explicitly declare as @Nullable or @NonNull to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations
private int mDefaultAudioIndex = -1;
private boolean burningSubs = false;
protected boolean burningSubs = false;
private float mRequestedPlaybackSpeed = -1.0f;

private Runnable mReportLoop;
Expand Down Expand Up @@ -211,18 +206,6 @@
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : -1;
}

public List<SubtitleStreamInfo> getSubtitleStreams() {
return mSubtitleStreams;
}

public SubtitleStreamInfo getSubtitleStreamInfo(int index) {
for (SubtitleStreamInfo info : mSubtitleStreams) {
if (info.getIndex() == index) return info;
}

return null;
}

public boolean isTranscoding() {
// use or here so that true is the default since
// this method is used to exclude features that may break unless we are sure playback is direct
Expand Down Expand Up @@ -401,7 +384,7 @@
play(position, null);
}

private void play(long position, @Nullable Integer forcedSubtitleIndex) {
protected void play(long position, @Nullable Integer forcedSubtitleIndex) {
Timber.i("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, forcedSubtitleIndex);

if (mFragment == null) {
Expand Down Expand Up @@ -607,22 +590,10 @@
mCurrentOptions.setMediaSourceId(response.getMediaSource().getId());

// get subtitle info
mSubtitleStreams = response.getSubtitleProfiles(false, apiClient.getValue().getApiUrl(), apiClient.getValue().getAccessToken());
mDefaultSubIndex = response.getMediaSource().getDefaultSubtitleStreamIndex() != null ? response.getMediaSource().getDefaultSubtitleStreamIndex() : mDefaultSubIndex;
mCurrentOptions.setSubtitleStreamIndex(response.getMediaSource().getDefaultSubtitleStreamIndex() != null ? response.getMediaSource().getDefaultSubtitleStreamIndex() : null);
setDefaultAudioIndex(response);
Timber.d("default audio index set to %s remote default %s", mDefaultAudioIndex, response.getMediaSource().getDefaultAudioStreamIndex());
Timber.d("default sub index set to %s remote default %s", mDefaultSubIndex, response.getMediaSource().getDefaultSubtitleStreamIndex());

// if burning in, set the subtitle index and the burningSubs flag so that onPrepared and switchSubtitleStream will know that we already have subtitles enabled
burningSubs = false;
if (mCurrentStreamInfo.getPlayMethod() == PlayMethod.Transcode && getSubtitleStreamInfo(mDefaultSubIndex) != null &&
getSubtitleStreamInfo(mDefaultSubIndex).getDeliveryMethod() == SubtitleDeliveryMethod.Encode) {
mCurrentOptions.setSubtitleStreamIndex(mDefaultSubIndex);
Timber.d("stream started with burnt in subs");
burningSubs = true;
} else {
mCurrentOptions.setSubtitleStreamIndex(null);
}
Timber.d("default sub index set to %s remote default %s", mCurrentOptions.getSubtitleStreamIndex(), response.getMediaSource().getDefaultSubtitleStreamIndex());

Long mbPos = position * 10000;

Expand All @@ -638,7 +609,7 @@
if (mFragment != null) mFragment.updateDisplay();

if (mVideoManager != null) {
mVideoManager.setVideoPath(response.getMediaUrl());
mVideoManager.setMediaStreamInfo(api.getValue(), response);
}

PlaybackControllerHelperKt.applyMediaSegments(this, item, () -> {
Expand Down Expand Up @@ -752,67 +723,6 @@
}
}


public void switchSubtitleStream(int index) {
if (!hasInitializedVideoManager())
return;
// get current timestamp first
refreshCurrentPosition();
Timber.d("Setting subtitle index to: %d", index);

// clear the default in case there's an error loading the subtitles
mDefaultSubIndex = -1;

// handle setting subtitles as disabled
// restart playback if turning off burnt-in subtitles
if (index < 0) {
mCurrentOptions.setSubtitleStreamIndex(-1);
if (burningSubs) {
stop();
play(mCurrentPosition, -1);
}
return;
}

org.jellyfin.sdk.model.api.MediaStream stream = StreamHelper.getMediaStream(getCurrentMediaSource(), index);
if (stream == null) {
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.subtitle_error));
return;
}
SubtitleStreamInfo streamInfo = getSubtitleStreamInfo(index);
if (streamInfo == null) {
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
return;
}

// handle switching on or between burnt-in subtitles, or switching to non-burnt subtitles
// if switching from burnt-in subtitles to another type, playback still needs to be restarted
if (burningSubs || streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode) {
stop();
if (mFragment != null && streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
play(mCurrentPosition, index);
return;
}

// when burnt-in subtitles are selected, mCurrentOptions SubtitleStreamIndex is set in startItem() as soon as playback starts
// otherwise mCurrentOptions SubtitleStreamIndex is kept null until now so we knew subtitles needed to be enabled but weren't already

if (streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Embed) {
if (!mVideoManager.setExoPlayerTrack(index, MediaStreamType.SUBTITLE, getCurrentlyPlayingItem().getMediaStreams())) {
// error selecting internal subs
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
} else {
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;

}
}
}

public void pause() {
Timber.d("pause called at %s", mCurrentPosition);
// if playback is paused and the seekbar is scrubbed, it will call pause even if already paused
Expand Down Expand Up @@ -887,7 +797,6 @@
}

private void clearPlaybackSessionOptions() {
mDefaultSubIndex = -1;
mDefaultAudioIndex = -1;
mSeekPosition = -1;
finishedInitialSeek = false;
Expand Down Expand Up @@ -980,7 +889,7 @@
public void onResponse(StreamInfo response) {
mCurrentStreamInfo = response;
if (mVideoManager != null) {
mVideoManager.setVideoPath(response.getMediaUrl());
mVideoManager.setMediaStreamInfo(api.getValue(), response);
mVideoManager.start();
}
}
Expand Down Expand Up @@ -1186,16 +1095,13 @@
if (mPlaybackState == PlaybackState.PAUSED) {
mPlaybackState = PlaybackState.PLAYING;
} else {
// select or disable subtitles
Integer currentSubtitleIndex = mCurrentOptions.getSubtitleStreamIndex();
if (mDefaultSubIndex >= 0 && currentSubtitleIndex != null && currentSubtitleIndex == mDefaultSubIndex) {
Timber.i("subtitle stream %s is already selected", mDefaultSubIndex);
} else {
if (mDefaultSubIndex < 0)
Timber.i("Turning off subs");
else
Timber.i("Enabling default sub stream: %d", mDefaultSubIndex);
switchSubtitleStream(mDefaultSubIndex);
if (!burningSubs) {
// Make sure the requested subtitles are enabled when external/embedded
Integer currentSubtitleIndex = mCurrentOptions.getSubtitleStreamIndex();

if (currentSubtitleIndex != null && currentSubtitleIndex != -1) {
PlaybackControllerHelperKt.setSubtitleIndex(this, currentSubtitleIndex, true);
}
}

// select an audio track
Expand Down
Loading
Loading