1+ package org .jellyfin .androidtv .ui .playback ;
2+
3+ import android .content .Context ;
4+ import android .graphics .Color ;
5+ import android .util .TypedValue ;
6+ import android .view .Gravity ;
7+ import android .view .ViewGroup ;
8+ import android .widget .LinearLayout ;
9+ import android .widget .TextView ;
10+
11+ import androidx .annotation .NonNull ;
12+ import androidx .media3 .common .text .Cue ;
13+ import androidx .media3 .common .text .CueGroup ;
14+ import androidx .media3 .ui .CaptionStyleCompat ;
15+
16+ import org .jellyfin .androidtv .preference .UserPreferences ;
17+
18+ import java .util .ArrayList ;
19+ import java .util .List ;
20+
21+ import timber .log .Timber ;
22+
23+ /**
24+ * Helper class to manage custom subtitle rendering with proper stacking of multiple cues.
25+ */
26+ public class CustomSubtitleHelper {
27+ private final Context context ;
28+ private final LinearLayout subtitleContainer ;
29+ private final List <TextView > subtitleViews = new ArrayList <>();
30+ private CaptionStyleCompat captionStyle ;
31+ private float textSize = 18f ;
32+
33+ public CustomSubtitleHelper (@ NonNull Context context , @ NonNull LinearLayout subtitleContainer , @ NonNull UserPreferences userPreferences ) {
34+ this .context = context ;
35+ this .subtitleContainer = subtitleContainer ;
36+
37+ // Configure the subtitle style based on user preferences
38+ int strokeColor = userPreferences .get (UserPreferences .Companion .getSubtitleTextStrokeColor ()).intValue ();
39+ captionStyle = new CaptionStyleCompat (
40+ userPreferences .get (UserPreferences .Companion .getSubtitlesTextColor ()).intValue (),
41+ userPreferences .get (UserPreferences .Companion .getSubtitlesBackgroundColor ()).intValue (),
42+ Color .TRANSPARENT ,
43+ Color .alpha (strokeColor ) == 0 ? CaptionStyleCompat .EDGE_TYPE_NONE : CaptionStyleCompat .EDGE_TYPE_OUTLINE ,
44+ strokeColor ,
45+ null
46+ );
47+
48+ // Set text size based on user preferences, with a multiplier to make it larger
49+ // The multiplier (1.8) makes the text significantly larger than the default
50+ float userSizePreference = userPreferences .get (UserPreferences .Companion .getSubtitlesTextSize ());
51+ textSize = 0.0533f * userSizePreference * 500 ;
52+
53+ Timber .d ("Setting subtitle text size to %f (user preference: %f)" , textSize , userSizePreference );
54+ }
55+
56+ /**
57+ * Process and display subtitle cues.
58+ * @param cueGroup The cue group containing subtitle cues
59+ */
60+ public void onCues (CueGroup cueGroup ) {
61+ List <Cue > cues = cueGroup != null ? cueGroup .cues : null ;
62+
63+ if (cues == null || cues .isEmpty ()) {
64+ subtitleContainer .setVisibility (ViewGroup .GONE );
65+ return ;
66+ }
67+
68+ // Make sure the container is visible
69+ subtitleContainer .setVisibility (ViewGroup .VISIBLE );
70+
71+ // Clear previous subtitles
72+ subtitleContainer .removeAllViews ();
73+
74+ Timber .d ("Displaying %d subtitle cues" , cues .size ());
75+
76+ // Ensure we have enough TextView instances
77+ while (subtitleViews .size () < cues .size ()) {
78+ TextView textView = new TextView (context );
79+ textView .setLayoutParams (new LinearLayout .LayoutParams (
80+ LinearLayout .LayoutParams .WRAP_CONTENT ,
81+ LinearLayout .LayoutParams .WRAP_CONTENT ));
82+ subtitleViews .add (textView );
83+ }
84+
85+ // Add and configure each cue
86+ for (int i = 0 ; i < cues .size (); i ++) {
87+ Cue cue = cues .get (i );
88+ TextView cueView = subtitleViews .get (i );
89+
90+ // Set the text from the cue
91+ if (cue .text != null ) {
92+ cueView .setText (cue .text );
93+
94+ // Apply styling
95+ applyTextStyle (cueView );
96+
97+ // Add to the container
98+ subtitleContainer .addView (cueView );
99+ }
100+ }
101+ }
102+
103+ /**
104+ * Apply text styling to a subtitle TextView.
105+ * @param textView The TextView to style
106+ */
107+ private void applyTextStyle (TextView textView ) {
108+ // Use a different unit for better TV display
109+ textView .setTextSize (TypedValue .COMPLEX_UNIT_SP , textSize );
110+ textView .setTextColor (captionStyle .foregroundColor );
111+
112+ // Make text bold for better visibility on TV
113+ textView .setTypeface (textView .getTypeface (), android .graphics .Typeface .BOLD );
114+
115+ // Apply edge type (outline, drop shadow, etc.)
116+ switch (captionStyle .edgeType ) {
117+ case CaptionStyleCompat .EDGE_TYPE_OUTLINE :
118+ // Increase outline thickness for better visibility
119+ textView .setShadowLayer (4 , 0 , 0 , captionStyle .edgeColor );
120+ break ;
121+ case CaptionStyleCompat .EDGE_TYPE_DROP_SHADOW :
122+ // Increase shadow size for better visibility
123+ textView .setShadowLayer (4 , 2 , 2 , captionStyle .edgeColor );
124+ break ;
125+ case CaptionStyleCompat .EDGE_TYPE_NONE :
126+ default :
127+ textView .setShadowLayer (0 , 0 , 0 , 0 );
128+ break ;
129+ }
130+
131+ // Center the text
132+ textView .setTextAlignment (TextView .TEXT_ALIGNMENT_CENTER );
133+ textView .setGravity (Gravity .CENTER );
134+
135+ // Set the background color
136+ textView .setBackgroundColor (captionStyle .backgroundColor );
137+
138+ // Add some padding
139+ int padding = (int ) TypedValue .applyDimension (
140+ TypedValue .COMPLEX_UNIT_DIP ,
141+ 4 ,
142+ context .getResources ().getDisplayMetrics ());
143+ textView .setPadding (padding , padding , padding , padding );
144+
145+ // Add some margin between subtitle lines
146+ LinearLayout .LayoutParams params = (LinearLayout .LayoutParams ) textView .getLayoutParams ();
147+ params .setMargins (0 , 0 , 0 , padding / 2 );
148+ textView .setLayoutParams (params );
149+ }
150+ }
0 commit comments