1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.media.subtitle; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Rect; 27 import android.graphics.Typeface; 28 import android.media.MediaFormat; 29 import android.text.Spannable; 30 import android.text.SpannableStringBuilder; 31 import android.text.TextPaint; 32 import android.util.AttributeSet; 33 import android.util.TypedValue; 34 import android.view.Gravity; 35 import android.view.View; 36 import android.view.accessibility.CaptioningManager.CaptionStyle; 37 import android.widget.LinearLayout; 38 import android.widget.TextView; 39 40 import androidx.annotation.RequiresApi; 41 import androidx.annotation.RestrictTo; 42 import androidx.media.R; 43 44 import java.util.ArrayList; 45 46 // Note: This is forked from android.media.ClosedCaptionRenderer since P 47 /** 48 * @hide 49 */ 50 @RequiresApi(28) 51 @RestrictTo(LIBRARY_GROUP) 52 public class ClosedCaptionRenderer extends SubtitleController.Renderer { 53 private final Context mContext; 54 private Cea608CCWidget mCCWidget; 55 56 public ClosedCaptionRenderer(Context context) { 57 mContext = context; 58 } 59 60 @Override 61 public boolean supports(MediaFormat format) { 62 if (format.containsKey(MediaFormat.KEY_MIME)) { 63 String mimeType = format.getString(MediaFormat.KEY_MIME); 64 return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType); 65 } 66 return false; 67 } 68 69 @Override 70 public SubtitleTrack createTrack(MediaFormat format) { 71 String mimeType = format.getString(MediaFormat.KEY_MIME); 72 if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) { 73 if (mCCWidget == null) { 74 mCCWidget = new Cea608CCWidget(mContext); 75 } 76 return new Cea608CaptionTrack(mCCWidget, format); 77 } 78 throw new RuntimeException("No matching format: " + format.toString()); 79 } 80 81 static class Cea608CaptionTrack extends SubtitleTrack { 82 private final Cea608CCParser mCCParser; 83 private final Cea608CCWidget mRenderingWidget; 84 85 Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) { 86 super(format); 87 88 mRenderingWidget = renderingWidget; 89 mCCParser = new Cea608CCParser(mRenderingWidget); 90 } 91 92 @Override 93 public void onData(byte[] data, boolean eos, long runID) { 94 mCCParser.parse(data); 95 } 96 97 @Override 98 public RenderingWidget getRenderingWidget() { 99 return mRenderingWidget; 100 } 101 102 @Override 103 public void updateView(ArrayList<Cue> activeCues) { 104 // Overriding with NO-OP, CC rendering by-passes this 105 } 106 } 107 108 /** 109 * Widget capable of rendering CEA-608 closed captions. 110 */ 111 class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener { 112 private static final String DUMMY_TEXT = "1234567890123456789012345678901234"; 113 private final Rect mTextBounds = new Rect(); 114 115 Cea608CCWidget(Context context) { 116 this(context, null); 117 } 118 119 Cea608CCWidget(Context context, AttributeSet attrs) { 120 this(context, attrs, 0); 121 } 122 123 Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) { 124 this(context, attrs, defStyle, 0); 125 } 126 127 Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 128 super(context, attrs, defStyleAttr, defStyleRes); 129 } 130 131 @Override 132 public ClosedCaptionLayout createCaptionLayout(Context context) { 133 return new CCLayout(context); 134 } 135 136 @Override 137 public void onDisplayChanged(SpannableStringBuilder[] styledTexts) { 138 ((CCLayout) mClosedCaptionLayout).update(styledTexts); 139 140 if (mListener != null) { 141 mListener.onChanged(this); 142 } 143 } 144 145 @Override 146 public CaptionStyle getCaptionStyle() { 147 return mCaptionStyle; 148 } 149 150 private class CCLineBox extends TextView { 151 private static final float FONT_PADDING_RATIO = 0.75f; 152 private static final float EDGE_OUTLINE_RATIO = 0.1f; 153 private static final float EDGE_SHADOW_RATIO = 0.05f; 154 private float mOutlineWidth; 155 private float mShadowRadius; 156 private float mShadowOffset; 157 158 private int mTextColor = Color.WHITE; 159 private int mBgColor = Color.BLACK; 160 private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE; 161 private int mEdgeColor = Color.TRANSPARENT; 162 163 CCLineBox(Context context) { 164 super(context); 165 setGravity(Gravity.CENTER); 166 setBackgroundColor(Color.TRANSPARENT); 167 setTextColor(Color.WHITE); 168 setTypeface(Typeface.MONOSPACE); 169 setVisibility(View.INVISIBLE); 170 171 final Resources res = getContext().getResources(); 172 173 // get the default (will be updated later during measure) 174 mOutlineWidth = res.getDimensionPixelSize( 175 R.dimen.subtitle_outline_width); 176 mShadowRadius = res.getDimensionPixelSize( 177 R.dimen.subtitle_shadow_radius); 178 mShadowOffset = res.getDimensionPixelSize( 179 R.dimen.subtitle_shadow_offset); 180 } 181 182 void setCaptionStyle(CaptionStyle captionStyle) { 183 mTextColor = captionStyle.foregroundColor; 184 mBgColor = captionStyle.backgroundColor; 185 mEdgeType = captionStyle.edgeType; 186 mEdgeColor = captionStyle.edgeColor; 187 188 setTextColor(mTextColor); 189 if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { 190 setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor); 191 } else { 192 setShadowLayer(0, 0, 0, 0); 193 } 194 invalidate(); 195 } 196 197 @Override 198 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 199 float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO; 200 setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); 201 202 mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f; 203 mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f; 204 mShadowOffset = mShadowRadius; 205 206 // set font scale in the X direction to match the required width 207 setScaleX(1.0f); 208 getPaint().getTextBounds(DUMMY_TEXT, 0, DUMMY_TEXT.length(), mTextBounds); 209 float actualTextWidth = mTextBounds.width(); 210 float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec); 211 setScaleX(requiredTextWidth / actualTextWidth); 212 213 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 214 } 215 216 @Override 217 protected void onDraw(Canvas c) { 218 if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED 219 || mEdgeType == CaptionStyle.EDGE_TYPE_NONE 220 || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { 221 // these edge styles don't require a second pass 222 super.onDraw(c); 223 return; 224 } 225 226 if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) { 227 drawEdgeOutline(c); 228 } else { 229 // Raised or depressed 230 drawEdgeRaisedOrDepressed(c); 231 } 232 } 233 234 @SuppressWarnings("WrongCall") 235 private void drawEdgeOutline(Canvas c) { 236 TextPaint textPaint = getPaint(); 237 238 Paint.Style previousStyle = textPaint.getStyle(); 239 Paint.Join previousJoin = textPaint.getStrokeJoin(); 240 float previousWidth = textPaint.getStrokeWidth(); 241 242 setTextColor(mEdgeColor); 243 textPaint.setStyle(Paint.Style.FILL_AND_STROKE); 244 textPaint.setStrokeJoin(Paint.Join.ROUND); 245 textPaint.setStrokeWidth(mOutlineWidth); 246 247 // Draw outline and background only. 248 super.onDraw(c); 249 250 // Restore original settings. 251 setTextColor(mTextColor); 252 textPaint.setStyle(previousStyle); 253 textPaint.setStrokeJoin(previousJoin); 254 textPaint.setStrokeWidth(previousWidth); 255 256 // Remove the background. 257 setBackgroundSpans(Color.TRANSPARENT); 258 // Draw foreground only. 259 super.onDraw(c); 260 // Restore the background. 261 setBackgroundSpans(mBgColor); 262 } 263 264 @SuppressWarnings("WrongCall") 265 private void drawEdgeRaisedOrDepressed(Canvas c) { 266 TextPaint textPaint = getPaint(); 267 268 Paint.Style previousStyle = textPaint.getStyle(); 269 textPaint.setStyle(Paint.Style.FILL); 270 271 final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED; 272 final int colorUp = raised ? Color.WHITE : mEdgeColor; 273 final int colorDown = raised ? mEdgeColor : Color.WHITE; 274 final float offset = mShadowRadius / 2f; 275 276 // Draw background and text with shadow up 277 setShadowLayer(mShadowRadius, -offset, -offset, colorUp); 278 super.onDraw(c); 279 280 // Remove the background. 281 setBackgroundSpans(Color.TRANSPARENT); 282 283 // Draw text with shadow down 284 setShadowLayer(mShadowRadius, +offset, +offset, colorDown); 285 super.onDraw(c); 286 287 // Restore settings 288 textPaint.setStyle(previousStyle); 289 290 // Restore the background. 291 setBackgroundSpans(mBgColor); 292 } 293 294 private void setBackgroundSpans(int color) { 295 CharSequence text = getText(); 296 if (text instanceof Spannable) { 297 Spannable spannable = (Spannable) text; 298 Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans( 299 0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class); 300 for (int i = 0; i < bgSpans.length; i++) { 301 bgSpans[i].setBackgroundColor(color); 302 } 303 } 304 } 305 } 306 307 private class CCLayout extends LinearLayout implements ClosedCaptionLayout { 308 private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS; 309 private static final float SAFE_AREA_RATIO = 0.9f; 310 311 private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS]; 312 313 CCLayout(Context context) { 314 super(context); 315 setGravity(Gravity.START); 316 setOrientation(LinearLayout.VERTICAL); 317 for (int i = 0; i < MAX_ROWS; i++) { 318 mLineBoxes[i] = new CCLineBox(getContext()); 319 addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 320 } 321 } 322 323 @Override 324 public void setCaptionStyle(CaptionStyle captionStyle) { 325 for (int i = 0; i < MAX_ROWS; i++) { 326 mLineBoxes[i].setCaptionStyle(captionStyle); 327 } 328 } 329 330 @Override 331 public void setFontScale(float fontScale) { 332 // Ignores the font scale changes of the system wide CC preference. 333 } 334 335 void update(SpannableStringBuilder[] textBuffer) { 336 for (int i = 0; i < MAX_ROWS; i++) { 337 if (textBuffer[i] != null) { 338 mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE); 339 mLineBoxes[i].setVisibility(View.VISIBLE); 340 } else { 341 mLineBoxes[i].setVisibility(View.INVISIBLE); 342 } 343 } 344 } 345 346 @Override 347 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 348 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 349 350 int safeWidth = getMeasuredWidth(); 351 int safeHeight = getMeasuredHeight(); 352 353 // CEA-608 assumes 4:3 video 354 if (safeWidth * 3 >= safeHeight * 4) { 355 safeWidth = safeHeight * 4 / 3; 356 } else { 357 safeHeight = safeWidth * 3 / 4; 358 } 359 safeWidth = (int) (safeWidth * SAFE_AREA_RATIO); 360 safeHeight = (int) (safeHeight * SAFE_AREA_RATIO); 361 362 int lineHeight = safeHeight / MAX_ROWS; 363 int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 364 lineHeight, MeasureSpec.EXACTLY); 365 int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 366 safeWidth, MeasureSpec.EXACTLY); 367 368 for (int i = 0; i < MAX_ROWS; i++) { 369 mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec); 370 } 371 } 372 373 @Override 374 protected void onLayout(boolean changed, int l, int t, int r, int b) { 375 // safe caption area 376 int viewPortWidth = r - l; 377 int viewPortHeight = b - t; 378 int safeWidth, safeHeight; 379 // CEA-608 assumes 4:3 video 380 if (viewPortWidth * 3 >= viewPortHeight * 4) { 381 safeWidth = viewPortHeight * 4 / 3; 382 safeHeight = viewPortHeight; 383 } else { 384 safeWidth = viewPortWidth; 385 safeHeight = viewPortWidth * 3 / 4; 386 } 387 safeWidth = (int) (safeWidth * SAFE_AREA_RATIO); 388 safeHeight = (int) (safeHeight * SAFE_AREA_RATIO); 389 int left = (viewPortWidth - safeWidth) / 2; 390 int top = (viewPortHeight - safeHeight) / 2; 391 392 for (int i = 0; i < MAX_ROWS; i++) { 393 mLineBoxes[i].layout( 394 left, 395 top + safeHeight * i / MAX_ROWS, 396 left + safeWidth, 397 top + safeHeight * (i + 1) / MAX_ROWS); 398 } 399 } 400 } 401 } 402 } 403