Remove custom subtitle renderer

This commit is contained in:
Niels van Velzen 2024-01-02 21:21:12 +01:00 committed by Niels van Velzen
parent 0f38bd1691
commit be06693978
6 changed files with 6 additions and 323 deletions

View File

@ -9,9 +9,6 @@ import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableString;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@ -25,7 +22,6 @@ import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
@ -45,7 +41,6 @@ import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
import org.jellyfin.androidtv.data.service.BackgroundService;
import org.jellyfin.androidtv.databinding.OverlayTvGuideBinding;
import org.jellyfin.androidtv.databinding.VlcPlayerInterfaceBinding;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.ui.GuideChannelHeader;
import org.jellyfin.androidtv.ui.GuidePagingButton;
import org.jellyfin.androidtv.ui.HorizontalScrollViewListener;
@ -67,7 +62,6 @@ import org.jellyfin.androidtv.ui.presentation.CardPresenter;
import org.jellyfin.androidtv.ui.presentation.ChannelCardPresenter;
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter;
import org.jellyfin.androidtv.ui.shared.PaddedLineBackgroundSpan;
import org.jellyfin.androidtv.util.CoroutineUtils;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
import org.jellyfin.androidtv.util.ImageHelper;
@ -77,12 +71,9 @@ import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackEvent;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackInfo;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
import org.jellyfin.sdk.model.api.ChapterInfo;
import org.koin.java.KoinJavaComponent;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@ -133,18 +124,6 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
private LeanbackOverlayFragment leanbackOverlayFragment;
// Subtitle fields
private static final int SUBTITLE_PADDING = 20;
private static final long SUBTITLE_RENDER_INTERVAL_MS = 50;
private SubtitleTrackInfo subtitleTrackInfo;
private int currentSubtitleIndex = 0;
private long lastSubtitlePositionMs = 0;
private final UserPreferences userPreferences = KoinJavaComponent.<UserPreferences>get(UserPreferences.class);
private final int subtitlesSize = userPreferences.get(UserPreferences.Companion.getSubtitlesSize());
private final boolean subtitlesBackgroundEnabled = userPreferences.get(UserPreferences.Companion.getSubtitlesBackgroundEnabled());
private final int subtitlesPosition = userPreferences.get(UserPreferences.Companion.getSubtitlePosition());
private final int subtitlesStrokeWidth = userPreferences.get(UserPreferences.Companion.getSubtitleStrokeSize());
private final Lazy<org.jellyfin.sdk.api.client.ApiClient> api = inject(org.jellyfin.sdk.api.client.ApiClient.class);
private final Lazy<MediaManager> mediaManager = inject(MediaManager.class);
private final Lazy<VideoQueueManager> videoQueueManager = inject(VideoQueueManager.class);
@ -259,26 +238,6 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
prepareOverlayFragment();
//manual subtitles
// This configuration is required for the PaddedLineBackgroundSpan to work
binding.subtitlesText.setShadowLayer(SUBTITLE_PADDING, 0, 0, Color.TRANSPARENT);
binding.subtitlesText.setPadding(SUBTITLE_PADDING, 0, SUBTITLE_PADDING, 0);
// Subtitles font size configuration
binding.subtitlesText.setTextSize(subtitlesSize);
// Subtitles font position (margin bottom)
if (subtitlesPosition > 0) {
ViewGroup.MarginLayoutParams currentLayoutParams = (ViewGroup.MarginLayoutParams) binding.subtitlesText.getLayoutParams();
currentLayoutParams.bottomMargin = (8 + Utils.convertDpToPixel(requireContext(), subtitlesPosition));
binding.subtitlesText.setLayoutParams(currentLayoutParams);
}
// Subtitles stroke width
if (subtitlesStrokeWidth > 0 && !subtitlesBackgroundEnabled) {
binding.subtitlesText.setStrokeWidth(subtitlesStrokeWidth);
}
//pre-load animations
fadeOut = AnimationUtils.loadAnimation(requireContext(), androidx.leanback.R.anim.abc_fade_out);
fadeOut.setAnimationListener(hideAnimationListener);
@ -1313,137 +1272,6 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
navigationRepository.getValue().navigate(Destinations.INSTANCE.nextUp(id), true);
}
public void addManualSubtitles(@Nullable SubtitleTrackInfo info) {
subtitleTrackInfo = info;
currentSubtitleIndex = -1;
lastSubtitlePositionMs = 0;
clearSubtitles();
}
public void showSubLoadingMsg(final boolean show) {
if (show) {
renderSubtitles(requireContext().getString(R.string.msg_subtitles_loading));
} else {
clearSubtitles();
}
}
public void updateSubtitles(long positionMs) {
int iterCount = 1;
final long positionTicks = positionMs * 10000;
final long lastPositionTicks = lastSubtitlePositionMs * 10000;
if (subtitleTrackInfo == null
|| subtitleTrackInfo.getTrackEvents() == null
|| subtitleTrackInfo.getTrackEvents().size() < 1
|| currentSubtitleIndex >= subtitleTrackInfo.getTrackEvents().size()) {
return;
}
if (positionTicks < subtitleTrackInfo.getTrackEvents().get(0).getStartPositionTicks())
return;
// Skip rendering if the interval ms have not passed since last render
if (lastSubtitlePositionMs > 0
&& Math.abs(lastSubtitlePositionMs - positionMs) < SUBTITLE_RENDER_INTERVAL_MS) {
return;
}
// If the user has skipped back, reset the subtitle index
if (lastSubtitlePositionMs > positionMs) {
currentSubtitleIndex = -1;
clearSubtitles();
}
if (currentSubtitleIndex == -1)
Timber.d("subtitle track events size %s", subtitleTrackInfo.getTrackEvents().size());
// Find the next subtitle event that should be rendered
for (int tmpSubtitleIndex = currentSubtitleIndex == -1 ? 0 : currentSubtitleIndex; tmpSubtitleIndex < subtitleTrackInfo.getTrackEvents().size(); tmpSubtitleIndex++) {
SubtitleTrackEvent trackEvent = subtitleTrackInfo.getTrackEvents().get(tmpSubtitleIndex);
if (positionTicks >= trackEvent.getStartPositionTicks()
&& positionTicks < trackEvent.getEndPositionTicks()) {
// This subtitle event should be displayed now
// use lastPositionTicks to ensure it is only rendered once
if (lastPositionTicks < trackEvent.getStartPositionTicks() || lastPositionTicks >= trackEvent.getEndPositionTicks()) {
Timber.d("rendering subtitle event: %s (pos %s start %s end %s)", tmpSubtitleIndex, positionMs, trackEvent.getStartPositionTicks() / 10000, trackEvent.getEndPositionTicks() / 10000);
renderSubtitles(trackEvent.getText());
}
currentSubtitleIndex = tmpSubtitleIndex;
lastSubtitlePositionMs = positionMs;
// rendering should happen on the 2nd iteration
if (iterCount > 2)
Timber.d("subtitles handled in %s iterations", iterCount);
return;
} else if (tmpSubtitleIndex < subtitleTrackInfo.getTrackEvents().size() - 1) {
SubtitleTrackEvent nextTrackEvent = subtitleTrackInfo.getTrackEvents().get(tmpSubtitleIndex + 1);
if (positionTicks >= trackEvent.getEndPositionTicks() && positionTicks < nextTrackEvent.getStartPositionTicks()) {
// clear the subtitles if between events
// use lastPositionTicks to ensure it is only cleared once
if (currentSubtitleIndex > -1 && !(lastPositionTicks >= trackEvent.getEndPositionTicks() && lastPositionTicks < nextTrackEvent.getStartPositionTicks())) {
Timber.d("clearing subtitle event: %s (pos %s - event end %s)", tmpSubtitleIndex, positionMs, trackEvent.getEndPositionTicks() / 10000);
clearSubtitles();
}
// set currentSubtitleIndex in case it was -1
currentSubtitleIndex = tmpSubtitleIndex;
lastSubtitlePositionMs = positionMs;
if (iterCount > 1)
Timber.d("subtitles handled in %s iterations", iterCount);
return;
}
}
iterCount++;
}
// handles clearing the last event
if (iterCount > 1)
Timber.d("subtitles handled in %s iterations", iterCount);
clearSubtitles();
}
private void clearSubtitles() {
requireActivity().runOnUiThread(() -> {
binding.subtitlesText.setVisibility(View.INVISIBLE);
binding.subtitlesText.setText(null);
});
}
private void renderSubtitles(@Nullable final String text) {
if (text == null || text.length() == 0) {
clearSubtitles();
return;
}
requireActivity().runOnUiThread(() -> {
final String htmlText = text
// Encode whitespace as html entities
.replaceAll("\\r\\n", "<br>")
.replaceAll("\\n", "<br>")
.replaceAll("\\\\h", "&ensp;")
// Remove SSA tags
.replaceAll("\\{\\\\.*?\\}", "");
final SpannableString span = new SpannableString(TextUtilsKt.toHtmlSpanned(htmlText));
if (subtitlesBackgroundEnabled) {
// Disable the text outlining when the background is enabled
binding.subtitlesText.setStrokeWidth(0.0f);
// get the alignment gravity of the TextView
// extract the absolute horizontal gravity so the span can draw its background aligned
int gravity = binding.subtitlesText.getGravity();
int horizontalGravity = Gravity.getAbsoluteGravity(gravity, binding.subtitlesText.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
span.setSpan(new PaddedLineBackgroundSpan(ContextCompat.getColor(requireContext(), R.color.black_opaque), SUBTITLE_PADDING, horizontalGravity), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.subtitlesText.setText(span);
binding.subtitlesText.setVisibility(View.VISIBLE);
});
}
@Override
public void onDestroy() {
super.onDestroy();

View File

@ -28,13 +28,11 @@ 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.ModelUtils;
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.mediainfo.SubtitleTrackInfo;
import org.jellyfin.apiclient.model.session.PlayMethod;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
@ -753,9 +751,6 @@ public class PlaybackController implements PlaybackControllerNotifiable {
refreshCurrentPosition();
Timber.d("Setting subtitle index to: %d", index);
// clear subtitles first
if (mFragment != null) mFragment.addManualSubtitles(null);
mVideoManager.disableSubs();
// clear the default in case there's an error loading the subtitles
mDefaultSubIndex = -1;
@ -796,63 +791,16 @@ public class PlaybackController implements PlaybackControllerNotifiable {
// 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
switch (streamInfo.getDeliveryMethod()) {
case Embed:
if (!mVideoManager.setExoPlayerTrack(index, MediaStreamType.SUBTITLE, getCurrentlyPlayingItem().getMediaStreams())) {
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;
}
break;
case External:
if (mFragment != null) mFragment.showSubLoadingMsg(true);
stream = ModelUtils.withDelivery(
stream,
org.jellyfin.sdk.model.api.SubtitleDeliveryMethod.EXTERNAL,
String.format(
"%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON",
apiClient.getValue().getApiUrl(),
mCurrentStreamInfo.getItemId(),
mCurrentStreamInfo.getMediaSourceId(),
String.valueOf(stream.getIndex())
)
);
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
@Override
public void onResponse(final SubtitleTrackInfo info) {
if (info != null) {
Timber.d("Adding json subtitle track to player");
if (mFragment != null) mFragment.addManualSubtitles(info);
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;
} else {
Timber.e("Empty subtitle result");
if (mFragment != null) {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
mFragment.showSubLoadingMsg(false);
}
}
}
@Override
public void onError(Exception ex) {
Timber.e(ex, "Error downloading subtitles");
if (mFragment != null) {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
mFragment.showSubLoadingMsg(false);
}
}
});
break;
case Hls:
break;
}
}
}
@ -1279,9 +1227,6 @@ public class PlaybackController implements PlaybackControllerNotifiable {
stopSpinner();
}
}
if (mFragment != null && finishedInitialSeek)
mFragment.updateSubtitles(mCurrentPosition);
}
if (mFragment != null)
mFragment.setCurrentTime(mCurrentPosition);

View File

@ -37,12 +37,9 @@ import androidx.media3.extractor.ts.TsExtractor;
import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView;
import com.google.common.collect.ImmutableSet;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.sdk.model.api.MediaStream;
import org.jellyfin.sdk.model.api.MediaStreamType;
import org.koin.java.KoinJavaComponent;
import java.util.List;
@ -84,11 +81,6 @@ public class VideoManager {
// Volume normalisation (audio night mode).
if (nightModeEnabled) enableAudioNightMode(mExoPlayer.getAudioSessionId());
mExoPlayer.setTrackSelectionParameters(mExoPlayer.getTrackSelectionParameters()
.buildUpon()
.setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_TEXT))
.build());
mExoPlayerView = view.findViewById(R.id.exoPlayerView);
mExoPlayerView.setPlayer(mExoPlayer);
mExoPlayer.addListener(new Player.Listener() {
@ -268,7 +260,6 @@ public class VideoManager {
public void stopPlayback() {
if (mExoPlayer != null) {
mExoPlayer.stop();
disableSubs();
mExoPlayer.setTrackSelectionParameters(mExoPlayer.getTrackSelectionParameters()
.buildUpon()
@ -312,16 +303,6 @@ public class VideoManager {
}
}
public void disableSubs() {
if (!isInitialized())
return;
mExoPlayer.setTrackSelectionParameters(mExoPlayer.getTrackSelectionParameters()
.buildUpon()
.setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_TEXT))
.build());
}
private int offsetStreamIndex(int index, boolean adjustByAdding, boolean indexStartsAtOne, @Nullable List<org.jellyfin.sdk.model.api.MediaStream> allStreams) {
if (index < 0 || allStreams == null)
return -1;
@ -399,13 +380,9 @@ public class VideoManager {
int chosenTrackType = streamType == org.jellyfin.sdk.model.api.MediaStreamType.SUBTITLE ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
// Make sure the index is present
Optional<MediaStream> candidateOptional = allStreams.stream().filter(stream -> stream.getIndex() == index).findFirst();
Optional<MediaStream> candidateOptional = allStreams.stream().filter(stream -> stream.getIndex() == index && !stream.isExternal() && stream.getType() == streamType).findFirst();
if (!candidateOptional.isPresent()) return false;
org.jellyfin.sdk.model.api.MediaStream candidate = candidateOptional.get();
if (candidate.isExternal() || candidate.getType() != streamType)
return false;
int exoTrackID = offsetStreamIndex(index, false, true, allStreams);
if (exoTrackID < 0)
return false;
@ -471,8 +448,6 @@ public class VideoManager {
try {
TrackSelectionParameters.Builder mExoPlayerSelectionParams = mExoPlayer.getTrackSelectionParameters().buildUpon();
mExoPlayerSelectionParams.setOverrideForType(new TrackSelectionOverride(matchedGroup, 0));
if (streamType == MediaStreamType.SUBTITLE)
mExoPlayerSelectionParams.setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_NONE));
mExoPlayer.setTrackSelectionParameters(mExoPlayerSelectionParams.build());
} catch (Exception e) {
Timber.d("Error setting track selection");

View File

@ -1,51 +0,0 @@
package org.jellyfin.androidtv.ui.shared
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class StrokeTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AppCompatTextView(context, attrs, defStyleAttr), KoinComponent {
var strokeWidth = 0.0f
private var isDrawing: Boolean = false
private val subtitlesTextColor = get<UserPreferences>()[UserPreferences.subtitlesTextColor]
init {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StrokeTextView)
strokeWidth = styledAttrs.getFloat(R.styleable.StrokeTextView_strokeWidth, 0.0f)
styledAttrs.recycle()
}
override fun invalidate() {
// To prevent infinite call of onDraw because setTextColor calls invalidate()
if (isDrawing) return
super.invalidate()
}
override fun onDraw(canvas: Canvas) {
if (strokeWidth <= 0) return super.onDraw(canvas)
isDrawing = true
paint.isAntiAlias = true
paint.strokeWidth = strokeWidth
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
setTextColor(ContextCompat.getColor(context, R.color.black))
super.onDraw(canvas)
paint.style = Paint.Style.FILL
setTextColor(ColorStateList.valueOf(subtitlesTextColor.toInt()))
super.onDraw(canvas)
isDrawing = false
}
}

View File

@ -216,8 +216,8 @@ class ExoPlayerProfile(
}.toTypedArray()
subtitleProfiles = arrayOf(
subtitleProfile(Codec.Subtitle.SRT, SubtitleDeliveryMethod.External),
subtitleProfile(Codec.Subtitle.SUBRIP, SubtitleDeliveryMethod.External),
subtitleProfile(Codec.Subtitle.SRT, SubtitleDeliveryMethod.Embed),
subtitleProfile(Codec.Subtitle.SUBRIP, SubtitleDeliveryMethod.Embed),
subtitleProfile(Codec.Subtitle.ASS, SubtitleDeliveryMethod.Encode),
subtitleProfile(Codec.Subtitle.SSA, SubtitleDeliveryMethod.Encode),
subtitleProfile(Codec.Subtitle.PGS, SubtitleDeliveryMethod.Embed),

View File

@ -38,20 +38,6 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
<org.jellyfin.androidtv.ui.shared.StrokeTextView
android:id="@+id/subtitles_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginHorizontal="120dp"
android:layout_marginBottom="48dp"
android:gravity="center"
android:textColor="@color/white"
android:textSize="28sp"
android:textDirection="ltr"
app:strokeWidth="5.0"
tools:text="Subtitles" />
</FrameLayout>
<FrameLayout