Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
@@ -0,0 +1,149 @@
package org.jellyfin.androidtv.ui.playback;

import android.content.Context;
import android.graphics.Color;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.ui.CaptionStyleCompat;

import org.jellyfin.androidtv.preference.UserPreferences;

import java.util.ArrayList;
import java.util.List;

import timber.log.Timber;

/**
* Helper class to manage custom subtitle rendering with proper stacking of multiple cues.
*/
public class CustomSubtitleHelper {
private final Context context;
private final LinearLayout subtitleContainer;
private final List<TextView> subtitleViews = new ArrayList<>();
private CaptionStyleCompat captionStyle;
private float textSize = 18f;

public CustomSubtitleHelper(@NonNull Context context, @NonNull LinearLayout subtitleContainer, @NonNull UserPreferences userPreferences) {
this.context = context;
this.subtitleContainer = subtitleContainer;

// Configure the subtitle style based on user preferences
int strokeColor = userPreferences.get(UserPreferences.Companion.getSubtitleTextStrokeColor()).intValue();
captionStyle = new CaptionStyleCompat(
userPreferences.get(UserPreferences.Companion.getSubtitlesTextColor()).intValue(),
userPreferences.get(UserPreferences.Companion.getSubtitlesBackgroundColor()).intValue(),
Color.TRANSPARENT,
Color.alpha(strokeColor) == 0 ? CaptionStyleCompat.EDGE_TYPE_NONE : CaptionStyleCompat.EDGE_TYPE_OUTLINE,
strokeColor,
null
);

// Set text size based on user preferences, with a multiplier to make it larger
float userSizePreference = userPreferences.get(UserPreferences.Companion.getSubtitlesTextSize());
textSize = 0.0533f * userSizePreference * 500;

Timber.d("Setting subtitle text size to %f (user preference: %f)", textSize, userSizePreference);
}

/**
* Process and display subtitle cues.
* @param cueGroup The cue group containing subtitle cues
*/
public void onCues(CueGroup cueGroup) {
List<Cue> cues = cueGroup != null ? cueGroup.cues : null;

if (cues == null || cues.isEmpty()) {
subtitleContainer.setVisibility(ViewGroup.GONE);
return;
}

// Make sure the container is visible
subtitleContainer.setVisibility(ViewGroup.VISIBLE);

// Clear previous subtitles
subtitleContainer.removeAllViews();

Timber.d("Displaying %d subtitle cues", cues.size());

// Ensure we have enough TextView instances
while (subtitleViews.size() < cues.size()) {
TextView textView = new TextView(context);
textView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
subtitleViews.add(textView);
}

// Add and configure each cue
for (int i = 0; i < cues.size(); i++) {
Cue cue = cues.get(i);
TextView cueView = subtitleViews.get(i);

// Set the text from the cue
if (cue.text != null) {
cueView.setText(cue.text);

// Apply styling
applyTextStyle(cueView);

// Add to the container
subtitleContainer.addView(cueView);
}
}
}

/**
* Apply text styling to a subtitle TextView.
* @param textView The TextView to style
*/
private void applyTextStyle(TextView textView) {
// Use a different unit for better TV display
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
textView.setTextColor(captionStyle.foregroundColor);

// Make text bold for better visibility on TV
textView.setTypeface(textView.getTypeface(), android.graphics.Typeface.BOLD);

// Apply edge type (outline, drop shadow, etc.)
switch (captionStyle.edgeType) {
case CaptionStyleCompat.EDGE_TYPE_OUTLINE:
// Increase outline thickness for better visibility
textView.setShadowLayer(4, 0, 0, captionStyle.edgeColor);
break;
case CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW:
// Increase shadow size for better visibility
textView.setShadowLayer(4, 2, 2, captionStyle.edgeColor);
break;
case CaptionStyleCompat.EDGE_TYPE_NONE:
default:
textView.setShadowLayer(0, 0, 0, 0);
break;
}

// Center the text
textView.setTextAlignment(TextView.TEXT_ALIGNMENT_CENTER);
textView.setGravity(Gravity.CENTER);

// Set the background color
textView.setBackgroundColor(captionStyle.backgroundColor);

// Add some padding
int padding = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4,
context.getResources().getDisplayMetrics());
textView.setPadding(padding, padding, padding, padding);

// Add some margin between subtitle lines
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) textView.getLayoutParams();
params.setMargins(0, 0, 0, padding / 2);
textView.setLayoutParams(params);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import android.os.Handler;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
Expand Down Expand Up @@ -69,6 +72,8 @@ public class VideoManager {
public ExoPlayer mExoPlayer;
private PlayerView mExoPlayerView;
private Handler mHandler = new Handler();
private CustomSubtitleHelper mCustomSubtitleHelper;
private LinearLayout mCustomSubtitleContainer;

private long mMetaDuration = -1;
private long lastExoPlayerPosition = -1;
Expand All @@ -94,17 +99,25 @@ public VideoManager(@NonNull Activity activity, @NonNull View view, @NonNull Pla

mExoPlayerView = view.findViewById(R.id.exoPlayerView);
mExoPlayerView.setPlayer(mExoPlayer);
int strokeColor = userPreferences.get(UserPreferences.Companion.getSubtitleTextStrokeColor()).intValue();
CaptionStyleCompat subtitleStyle = new CaptionStyleCompat(
userPreferences.get(UserPreferences.Companion.getSubtitlesTextColor()).intValue(),
userPreferences.get(UserPreferences.Companion.getSubtitlesBackgroundColor()).intValue(),
Color.TRANSPARENT,
Color.alpha(strokeColor) == 0 ? CaptionStyleCompat.EDGE_TYPE_NONE : CaptionStyleCompat.EDGE_TYPE_OUTLINE,
strokeColor,
null
);
mExoPlayerView.getSubtitleView().setFractionalTextSize(0.0533f * userPreferences.get(UserPreferences.Companion.getSubtitlesTextSize()));
mExoPlayerView.getSubtitleView().setStyle(subtitleStyle);

// Get the custom subtitle container
mCustomSubtitleContainer = view.findViewById(R.id.custom_subtitle_container);

// Initialize the custom subtitle helper
mCustomSubtitleHelper = new CustomSubtitleHelper(mActivity, mCustomSubtitleContainer, userPreferences);

// Hide the default subtitle view
mExoPlayerView.getSubtitleView().setVisibility(View.GONE);

// Add a listener for subtitle cues
mExoPlayer.addListener(new Player.Listener() {
@Override
public void onCues(@NonNull List<Cue> cues) {
// Process subtitle cues in our custom helper
mCustomSubtitleHelper.onCues(new CueGroup(cues, 0));
}
});

mExoPlayer.addListener(new Player.Listener() {
@Override
public void onPlayerError(@NonNull PlaybackException error) {
Expand Down Expand Up @@ -550,6 +563,7 @@ public void destroy() {
mPlaybackControllerNotifiable = null;
stopPlayback();
releasePlayer();
mCustomSubtitleHelper = null;
}

private void releasePlayer() {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/res/layout/vlc_player_interface.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false" />

<!-- Custom subtitle container for stacked subtitles -->
<LinearLayout
android:id="@+id/custom_subtitle_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="50dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/leanback_fragment"
Expand Down