Home | History | Annotate | Download | only in subtitle
      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