From 6c3ae7f1757789936294b6ba79586dbf7ca43f31 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Sep 2014 16:30:39 +0100 Subject: [PATCH] Add SubtitleView and CaptionStyleCompat to ExoPlayer. --- .../demo/full/FullPlayerActivity.java | 57 +++- .../main/res/layout/player_activity_full.xml | 14 +- demo/src/main/res/values/constants.xml | 22 ++ .../exoplayer/parser/mp4/TrackFragment.java | 4 +- .../exoplayer/text/CaptionStyleCompat.java | 142 +++++++++ .../android/exoplayer/text/SubtitleView.java | 295 ++++++++++++++++++ 6 files changed, 519 insertions(+), 15 deletions(-) create mode 100644 demo/src/main/res/values/constants.xml create mode 100644 library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java create mode 100644 library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 55ef6f56ee..366118877f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -24,13 +24,20 @@ import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.text.CaptionStyleCompat; +import com.google.android.exoplayer.text.SubtitleView; +import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.VerboseLogUtil; +import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; +import android.view.Display; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; @@ -38,6 +45,8 @@ import android.view.SurfaceHolder; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; +import android.view.WindowManager; +import android.view.accessibility.CaptioningManager; import android.widget.Button; import android.widget.MediaController; import android.widget.PopupMenu; @@ -50,6 +59,7 @@ import android.widget.TextView; public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, DemoPlayer.Listener, DemoPlayer.TextListener { + private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int MENU_GROUP_TRACKS = 1; private static final int ID_OFFSET = 2; @@ -60,7 +70,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private VideoSurfaceView surfaceView; private TextView debugTextView; private TextView playerStateTextView; - private TextView subtitlesTextView; + private SubtitleView subtitleView; private Button videoButton; private Button audioButton; private Button textButton; @@ -108,7 +118,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba debugTextView = (TextView) findViewById(R.id.debug_text_view); playerStateTextView = (TextView) findViewById(R.id.player_state_view); - subtitlesTextView = (TextView) findViewById(R.id.subtitles); + subtitleView = (SubtitleView) findViewById(R.id.subtitles); mediaController = new MediaController(this); mediaController.setAnchorView(root); @@ -122,6 +132,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba @Override public void onResume() { super.onResume(); + configureSubtitleView(); preparePlayer(); } @@ -380,10 +391,10 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba @Override public void onText(String text) { if (TextUtils.isEmpty(text)) { - subtitlesTextView.setVisibility(View.INVISIBLE); + subtitleView.setVisibility(View.INVISIBLE); } else { - subtitlesTextView.setVisibility(View.VISIBLE); - subtitlesTextView.setText(text); + subtitleView.setVisibility(View.VISIBLE); + subtitleView.setText(text); } } @@ -409,4 +420,40 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } + private void configureSubtitleView() { + CaptionStyleCompat captionStyle; + float captionTextSize = getCaptionFontSize(); + if (Util.SDK_INT >= 19) { + captionStyle = getUserCaptionStyleV19(); + captionTextSize *= getUserCaptionFontScaleV19(); + } else { + captionStyle = CaptionStyleCompat.DEFAULT; + } + subtitleView.setStyle(captionStyle); + subtitleView.setTextSize(captionTextSize); + } + + private float getCaptionFontSize() { + Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + Point displaySize = new Point(); + display.getSize(displaySize); + return Math.max(getResources().getDimension(R.dimen.subtitle_minimum_font_size), + CAPTION_LINE_HEIGHT_RATIO * Math.min(displaySize.x, displaySize.y)); + } + + @TargetApi(19) + private float getUserCaptionFontScaleV19() { + CaptioningManager captioningManager = + (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); + return captioningManager.getFontScale(); + } + + @TargetApi(19) + private CaptionStyleCompat getUserCaptionStyleV19() { + CaptioningManager captioningManager = + (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); + return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); + } + } diff --git a/demo/src/main/res/layout/player_activity_full.xml b/demo/src/main/res/layout/player_activity_full.xml index 8d3e132995..d2e069620f 100644 --- a/demo/src/main/res/layout/player_activity_full.xml +++ b/demo/src/main/res/layout/player_activity_full.xml @@ -24,15 +24,13 @@ android:layout_height="match_parent" android:layout_gravity="center"/> - + + + + + + 13sp + + diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java index a25dfa4505..4291f5cad4 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java @@ -95,8 +95,8 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; // likely. The choice of 25% is relatively arbitrary. int tableSize = (sampleCount * 125) / 100; sampleSizeTable = new int[tableSize]; - sampleDecodingTimeTable = new int[tableSize]; sampleCompositionTimeOffsetTable = new int[tableSize]; + sampleDecodingTimeTable = new long[tableSize]; sampleIsSyncFrameTable = new boolean[tableSize]; sampleHasSubsampleEncryptionTable = new boolean[tableSize]; } @@ -147,7 +147,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; return true; } - public int getSamplePresentationTime(int index) { + public long getSamplePresentationTime(int index) { return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java new file mode 100644 index 0000000000..60cc8ab129 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/CaptionStyleCompat.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text; + +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.accessibility.CaptioningManager; +import android.view.accessibility.CaptioningManager.CaptionStyle; + +/** + * A compatibility wrapper for {@link CaptionStyle}. + */ +public final class CaptionStyleCompat { + + /** + * Edge type value specifying no character edges. + */ + public static final int EDGE_TYPE_NONE = 0; + + /** + * Edge type value specifying uniformly outlined character edges. + */ + public static final int EDGE_TYPE_OUTLINE = 1; + + /** + * Edge type value specifying drop-shadowed character edges. + */ + public static final int EDGE_TYPE_DROP_SHADOW = 2; + + /** + * Edge type value specifying raised bevel character edges. + */ + public static final int EDGE_TYPE_RAISED = 3; + + /** + * Edge type value specifying depressed bevel character edges. + */ + public static final int EDGE_TYPE_DEPRESSED = 4; + + /** + * Use color setting specified by the track and fallback to default caption style. + */ + public static final int USE_TRACK_COLOR_SETTINGS = 1; + + /** + * Default caption style. + */ + public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat( + Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null); + + /** + * The preferred foreground color. + */ + public final int foregroundColor; + + /** + * The preferred background color. + */ + public final int backgroundColor; + + /** + * The preferred window color. + */ + public final int windowColor; + + /** + * The preferred edge type. One of: + *
    + *
  • {@link #EDGE_TYPE_NONE} + *
  • {@link #EDGE_TYPE_OUTLINE} + *
  • {@link #EDGE_TYPE_DROP_SHADOW} + *
  • {@link #EDGE_TYPE_RAISED} + *
  • {@link #EDGE_TYPE_DEPRESSED} + *
+ */ + public final int edgeType; + + /** + * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. + */ + public final int edgeColor; + + /** + * The preferred typeface. + */ + public final Typeface typeface; + + /** + * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. + * + * @param style A {@link CaptionStyle}. + * @return The equivalent {@link CaptionStyleCompat}. + */ + @TargetApi(19) + public static CaptionStyleCompat createFromCaptionStyle(CaptionStyle style) { + int windowColor = Util.SDK_INT >= 21 ? getWindowColorV21(style) : Color.TRANSPARENT; + return new CaptionStyleCompat(style.foregroundColor, style.backgroundColor, windowColor, + style.edgeType, style.edgeColor, style.getTypeface()); + } + + /** + * @param foregroundColor See {@link #foregroundColor}. + * @param backgroundColor See {@link #backgroundColor}. + * @param windowColor See {@link #windowColor}. + * @param edgeType See {@link #edgeType}. + * @param edgeColor See {@link #edgeColor}. + * @param typeface See {@link #typeface}. + */ + public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, int edgeType, + int edgeColor, Typeface typeface) { + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + this.windowColor = windowColor; + this.edgeType = edgeType; + this.edgeColor = edgeColor; + this.typeface = typeface; + } + + @SuppressWarnings("unused") + @TargetApi(21) + private static int getWindowColorV21(CaptioningManager.CaptionStyle captionStyle) { + // TODO: Uncomment when building against API level 21. + return Color.TRANSPARENT; //captionStyle.windowColor; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java new file mode 100644 index 0000000000..7b2ecf5494 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text; + +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Join; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.View; + +/** + * A view for rendering captions. + *

+ * The caption style and text size can be configured using {@link #setStyle(CaptionStyleCompat)} and + * {@link #setTextSize(float)} respectively. + */ +public class SubtitleView extends View { + + /** + * Ratio of inner padding to font size. + */ + private static final float INNER_PADDING_RATIO = 0.125f; + + /** + * Temporary rectangle used for computing line bounds. + */ + private final RectF lineBounds = new RectF(); + + /** + * Reusable string builder used for holding text. + */ + private final StringBuilder textBuilder = new StringBuilder(); + + // Styled dimensions. + private final float cornerRadius; + private final float outlineWidth; + private final float shadowRadius; + private final float shadowOffset; + + private TextPaint textPaint; + private Paint paint; + + private int foregroundColor; + private int backgroundColor; + private int edgeColor; + private int edgeType; + + private boolean hasMeasurements; + private int lastMeasuredWidth; + private StaticLayout layout; + + private float spacingMult; + private float spacingAdd; + private int innerPaddingX; + + public SubtitleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + int[] viewAttr = {android.R.attr.text, android.R.attr.textSize, + android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier}; + TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0); + CharSequence text = a.getText(0); + int textSize = a.getDimensionPixelSize(1, 15); + spacingAdd = a.getDimensionPixelSize(2, 0); + spacingMult = a.getFloat(3, 1); + a.recycle(); + + Resources resources = getContext().getResources(); + DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + int twoDpInPx = Math.round((2 * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); + cornerRadius = twoDpInPx; + outlineWidth = twoDpInPx; + shadowRadius = twoDpInPx; + shadowOffset = twoDpInPx; + + textPaint = new TextPaint(); + textPaint.setAntiAlias(true); + textPaint.setSubpixelText(true); + + paint = new Paint(); + paint.setAntiAlias(true); + + innerPaddingX = 0; + setText(text); + setTextSize(textSize); + setStyle(CaptionStyleCompat.DEFAULT); + } + + public SubtitleView(Context context) { + this(context, null); + } + + @Override + public void setBackgroundColor(int color) { + backgroundColor = color; + invalidate(); + } + + /** + * Sets the text to be displayed by the view. + * + * @param text The text to display. + */ + public void setText(CharSequence text) { + textBuilder.setLength(0); + textBuilder.append(text); + hasMeasurements = false; + requestLayout(); + } + + /** + * Sets the text size in pixels. + * + * @param size The text size in pixels. + */ + public void setTextSize(float size) { + if (textPaint.getTextSize() != size) { + textPaint.setTextSize(size); + innerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f); + hasMeasurements = false; + requestLayout(); + invalidate(); + } + } + + /** + * Configures the view according to the given style. + * + * @param style A style for the view. + */ + public void setStyle(CaptionStyleCompat style) { + foregroundColor = style.foregroundColor; + backgroundColor = style.backgroundColor; + edgeType = style.edgeType; + edgeColor = style.edgeColor; + setTypeface(style.typeface); + super.setBackgroundColor(style.windowColor); + hasMeasurements = false; + requestLayout(); + } + + private void setTypeface(Typeface typeface) { + if (textPaint.getTypeface() != typeface) { + textPaint.setTypeface(typeface); + hasMeasurements = false; + requestLayout(); + invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSpec = MeasureSpec.getSize(widthMeasureSpec); + + if (computeMeasurements(widthSpec)) { + final StaticLayout layout = this.layout; + final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2; + final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom(); + int width = 0; + int lineCount = layout.getLineCount(); + for (int i = 0; i < lineCount; i++) { + width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width); + } + width += paddingX; + setMeasuredDimension(width, height); + } else if (Util.SDK_INT >= 11) { + setTooSmallMeasureDimensionV11(); + } else { + setMeasuredDimension(0, 0); + } + } + + @TargetApi(11) + private void setTooSmallMeasureDimensionV11() { + setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = r - l; + computeMeasurements(width); + } + + private boolean computeMeasurements(int maxWidth) { + if (hasMeasurements && maxWidth == lastMeasuredWidth) { + return true; + } + + // Account for padding. + final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2; + maxWidth -= paddingX; + if (maxWidth <= 0) { + return false; + } + + hasMeasurements = true; + lastMeasuredWidth = maxWidth; + layout = new StaticLayout(textBuilder, textPaint, maxWidth, null, spacingMult, spacingAdd, + true); + return true; + } + + @Override + protected void onDraw(Canvas c) { + final StaticLayout layout = this.layout; + if (layout == null) { + return; + } + + final int saveCount = c.save(); + final int innerPaddingX = this.innerPaddingX; + c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop()); + + final int lineCount = layout.getLineCount(); + final Paint textPaint = this.textPaint; + final Paint paint = this.paint; + final RectF bounds = lineBounds; + + if (Color.alpha(backgroundColor) > 0) { + final float cornerRadius = this.cornerRadius; + float previousBottom = layout.getLineTop(0); + + paint.setColor(backgroundColor); + paint.setStyle(Style.FILL); + + for (int i = 0; i < lineCount; i++) { + bounds.left = layout.getLineLeft(i) - innerPaddingX; + bounds.right = layout.getLineRight(i) + innerPaddingX; + bounds.top = previousBottom; + bounds.bottom = layout.getLineBottom(i); + previousBottom = bounds.bottom; + + c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint); + } + } + + if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { + textPaint.setStrokeJoin(Join.ROUND); + textPaint.setStrokeWidth(outlineWidth); + textPaint.setColor(edgeColor); + textPaint.setStyle(Style.FILL_AND_STROKE); + layout.draw(c); + } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { + textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); + } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED + || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { + boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; + int colorUp = raised ? Color.WHITE : edgeColor; + int colorDown = raised ? edgeColor : Color.WHITE; + float offset = shadowRadius / 2f; + textPaint.setColor(foregroundColor); + textPaint.setStyle(Style.FILL); + textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp); + layout.draw(c); + textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown); + } + + textPaint.setColor(foregroundColor); + textPaint.setStyle(Style.FILL); + layout.draw(c); + textPaint.setShadowLayer(0, 0, 0, 0); + c.restoreToCount(saveCount); + } + +}