diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 14b30937ed..11cdcd7ef1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,7 @@ * Add option to TsExtractor to allow non-IDR keyframes. * Added MulticastDataSource for connecting to multicast streams. * (WorkInProgress) - First steps to supporting seeking in DASH DVR window. +* (WorkInProgress) - First steps to supporting styled + positioned subtitles. ### r1.3.2 (from r1.3.1) ### diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 5dd1eb8d32..822c85ec4e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -35,7 +35,8 @@ import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.PrivMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; -import com.google.android.exoplayer.text.SubtitleView; +import com.google.android.exoplayer.text.Cue; +import com.google.android.exoplayer.text.SubtitleLayout; import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.VerboseLogUtil; @@ -43,12 +44,10 @@ 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.util.Log; -import android.view.Display; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -58,7 +57,6 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.View.OnTouchListener; -import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import android.widget.Button; import android.widget.MediaController; @@ -67,13 +65,14 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; import android.widget.Toast; +import java.util.List; import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. */ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener, + DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener, AudioCapabilitiesReceiver.Listener { public static final String CONTENT_TYPE_EXTRA = "content_type"; @@ -81,7 +80,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private static final String TAG = "PlayerActivity"; - 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; @@ -92,7 +90,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private VideoSurfaceView surfaceView; private TextView debugTextView; private TextView playerStateTextView; - private SubtitleView subtitleView; + private SubtitleLayout subtitleLayout; private Button videoButton; private Button audioButton; private Button textButton; @@ -154,7 +152,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, debugTextView = (TextView) findViewById(R.id.debug_text_view); playerStateTextView = (TextView) findViewById(R.id.player_state_view); - subtitleView = (SubtitleView) findViewById(R.id.subtitles); + subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles); mediaController = new MediaController(this); mediaController.setAnchorView(root); @@ -256,7 +254,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (player == null) { player = new DemoPlayer(getRendererBuilder()); player.addListener(this); - player.setTextListener(this); + player.setCaptionListener(this); player.setMetadataListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; @@ -464,16 +462,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, debugRootView.setVisibility(View.VISIBLE); } - // DemoPlayer.TextListener implementation + // DemoPlayer.CaptionListener implementation @Override - public void onText(String text) { - if (TextUtils.isEmpty(text)) { - subtitleView.setVisibility(View.INVISIBLE); - } else { - subtitleView.setVisibility(View.VISIBLE); - subtitleView.setText(text); - } + public void onCues(List cues) { + subtitleLayout.setCues(cues); } // DemoPlayer.MetadataListener implementation @@ -523,24 +516,16 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private void configureSubtitleView() { CaptionStyleCompat captionStyle; - float captionTextSize = getCaptionFontSize(); + float captionFontScale; if (Util.SDK_INT >= 19) { captionStyle = getUserCaptionStyleV19(); - captionTextSize *= getUserCaptionFontScaleV19(); + captionFontScale = getUserCaptionFontScaleV19(); } else { captionStyle = CaptionStyleCompat.DEFAULT; + captionFontScale = 1.0f; } - 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)); + subtitleLayout.setStyle(captionStyle); + subtitleLayout.setFontScale(captionFontScale); } @TargetApi(19) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 2d34222ced..95ab7293ba 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.TextRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.util.PlayerControl; @@ -41,6 +42,8 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -140,8 +143,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi /** * A listener for receiving notifications of timed text. */ - public interface TextListener { - void onText(String text); + public interface CaptionListener { + void onCues(List cues); } /** @@ -193,7 +196,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private int[] selectedTracks; private boolean backgrounded; - private TextListener textListener; + private CaptionListener captionListener; private Id3MetadataListener id3MetadataListener; private InternalErrorListener internalErrorListener; private InfoListener infoListener; @@ -232,8 +235,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi infoListener = listener; } - public void setTextListener(TextListener listener) { - textListener = listener; + public void setCaptionListener(CaptionListener listener) { + captionListener = listener; } public void setMetadataListener(Id3MetadataListener listener) { @@ -268,8 +271,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } selectedTracks[type] = index; pushTrackSelection(type, true); - if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { - textListener.onText(null); + if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) { + captionListener.onCues(Collections.emptyList()); } } @@ -509,8 +512,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onText(String text) { - processText(text); + public void onCues(List cues) { + processCues(cues); } @Override @@ -617,11 +620,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - /* package */ void processText(String text) { - if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { + /* package */ void processCues(List cues) { + if (captionListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { return; } - textListener.onText(text); + captionListener.onCues(cues); } private class InternalRendererBuilderCallback implements RendererBuilderCallback { diff --git a/demo/src/main/res/layout/player_activity.xml b/demo/src/main/res/layout/player_activity.xml index 2480897ca0..2f0cce32a9 100644 --- a/demo/src/main/res/layout/player_activity.xml +++ b/demo/src/main/res/layout/player_activity.xml @@ -26,14 +26,12 @@ android:layout_height="match_parent" android:layout_gravity="center"/> - + android:layout_marginBottom="32dp"/> getCues(long timeUs); } diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleLayout.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleLayout.java new file mode 100644 index 0000000000..e82b8f6121 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleLayout.java @@ -0,0 +1,189 @@ +/* + * 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 android.content.Context; +import android.text.Layout.Alignment; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * A view for rendering rich-formatted captions. + */ +public final class SubtitleLayout extends ViewGroup { + + /** + * Use the same line height ratio as WebVtt to match the display with the preview. + * WebVtt specifies line height as 5.3% of the viewport height. + */ + private static final float LINE_HEIGHT_RATIO = 0.0533f; + + private final List subtitleViews; + + private List subtitleCues; + private int viewsInUse; + + private float fontScale; + private float textSize; + private CaptionStyleCompat captionStyle; + + public SubtitleLayout(Context context) { + this(context, null); + } + + public SubtitleLayout(Context context, AttributeSet attrs) { + super(context, attrs); + subtitleViews = new ArrayList(); + fontScale = 1; + captionStyle = CaptionStyleCompat.DEFAULT; + } + + /** + * Sets the cues to be displayed by the view. + * + * @param cues The cues to display. + */ + public void setCues(List cues) { + subtitleCues = cues; + int size = (cues == null) ? 0 : cues.size(); + + // create new subtitle views if necessary + if (size > subtitleViews.size()) { + for (int i = subtitleViews.size(); i < size; i++) { + SubtitleView newView = createSubtitleView(); + subtitleViews.add(newView); + } + } + + // add the views we currently need, if necessary + for (int i = viewsInUse; i < size; i++) { + addView(subtitleViews.get(i)); + } + + // remove the views we don't currently need, if necessary + for (int i = size; i < viewsInUse; i++) { + removeView(subtitleViews.get(i)); + } + + viewsInUse = size; + + for (int i = 0; i < size; i++) { + subtitleViews.get(i).setText(cues.get(i).text); + } + + requestLayout(); + } + + /** + * Sets the scale of the font. + * + * @param scale The scale of the font. + */ + public void setFontScale(float scale) { + fontScale = scale; + updateSubtitlesTextSize(); + + for (SubtitleView subtitleView : subtitleViews) { + subtitleView.setTextSize(textSize); + } + requestLayout(); + } + + /** + * Configures the view according to the given style. + * + * @param captionStyle A style for the view. + */ + public void setStyle(CaptionStyleCompat captionStyle) { + this.captionStyle = captionStyle; + + for (SubtitleView subtitleView : subtitleViews) { + subtitleView.setStyle(captionStyle); + } + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + + updateSubtitlesTextSize(); + + for (int i = 0; i < viewsInUse; i++) { + subtitleViews.get(i).setTextSize(textSize); + subtitleViews.get(i).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + + for (int i = 0; i < viewsInUse; i++) { + SubtitleView subtitleView = subtitleViews.get(i); + Cue subtitleCue = subtitleCues.get(i); + + int viewLeft = (width - subtitleView.getMeasuredWidth()) / 2; + int viewRight = viewLeft + subtitleView.getMeasuredWidth(); + int viewTop = bottom - subtitleView.getMeasuredHeight(); + int viewBottom = bottom; + + if (subtitleCue.alignment != null) { + subtitleView.setTextAlignment(subtitleCue.alignment); + } else { + subtitleView.setTextAlignment(Alignment.ALIGN_CENTER); + } + if (subtitleCue.position != Cue.UNSET_VALUE) { + if (subtitleCue.alignment == Alignment.ALIGN_OPPOSITE) { + viewRight = (int) ((width * (double) subtitleCue.position) / 100) + left; + viewLeft = Math.max(viewRight - subtitleView.getMeasuredWidth(), left); + } else { + viewLeft = (int) ((width * (double) subtitleCue.position) / 100) + left; + viewRight = Math.min(viewLeft + subtitleView.getMeasuredWidth(), right); + } + } + if (subtitleCue.line != Cue.UNSET_VALUE) { + viewTop = (int) (height * (double) subtitleCue.line / 100) + top; + viewBottom = viewTop + subtitleView.getMeasuredHeight(); + if (viewBottom > bottom) { + viewTop = bottom - subtitleView.getMeasuredHeight(); + viewBottom = bottom; + } + } + + subtitleView.layout(viewLeft, viewTop, viewRight, viewBottom); + } + } + + private void updateSubtitlesTextSize() { + textSize = LINE_HEIGHT_RATIO * getHeight() * fontScale; + } + + private SubtitleView createSubtitleView() { + SubtitleView view = new SubtitleView(getContext()); + view.setStyle(captionStyle); + view.setTextSize(textSize); + return view; + } + +} 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 index 7b977aa7cc..c317508dd4 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java @@ -28,6 +28,7 @@ import android.graphics.Paint.Join; import android.graphics.Paint.Style; import android.graphics.RectF; import android.graphics.Typeface; +import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; @@ -35,10 +36,7 @@ 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. + * A view for rendering a single caption. */ public class SubtitleView extends View { @@ -52,11 +50,6 @@ public class SubtitleView extends View { */ 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; @@ -66,6 +59,8 @@ public class SubtitleView extends View { private TextPaint textPaint; private Paint paint; + private CharSequence text; + private int foregroundColor; private int backgroundColor; private int edgeColor; @@ -75,10 +70,15 @@ public class SubtitleView extends View { private int lastMeasuredWidth; private StaticLayout layout; + private Alignment alignment; private float spacingMult; private float spacingAdd; private int innerPaddingX; + public SubtitleView(Context context) { + this(context, null); + } + public SubtitleView(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -107,6 +107,8 @@ public class SubtitleView extends View { textPaint.setAntiAlias(true); textPaint.setSubpixelText(true); + alignment = Alignment.ALIGN_CENTER; + paint = new Paint(); paint.setAntiAlias(true); @@ -116,10 +118,6 @@ public class SubtitleView extends View { setStyle(CaptionStyleCompat.DEFAULT); } - public SubtitleView(Context context) { - this(context, null); - } - @Override public void setBackgroundColor(int color) { backgroundColor = color; @@ -132,8 +130,7 @@ public class SubtitleView extends View { * @param text The text to display. */ public void setText(CharSequence text) { - textBuilder.setLength(0); - textBuilder.append(text); + this.text = text; forceUpdate(true); } @@ -150,6 +147,15 @@ public class SubtitleView extends View { } } + /** + * Sets the text alignment. + * + * @param textAlignment The text alignment. + */ + public void setTextAlignment(Alignment textAlignment) { + alignment = textAlignment; + } + /** * Configures the view according to the given style. * @@ -227,8 +233,7 @@ public class SubtitleView extends View { hasMeasurements = true; lastMeasuredWidth = maxWidth; - layout = new StaticLayout(textBuilder, textPaint, maxWidth, null, spacingMult, spacingAdd, - true); + layout = new StaticLayout(text, textPaint, maxWidth, alignment, spacingMult, spacingAdd, true); return true; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java index 8b0b1ae6dc..03899c0b0c 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java @@ -15,16 +15,18 @@ */ package com.google.android.exoplayer.text; +import java.util.List; + /** * An interface for components that render text. */ public interface TextRenderer { /** - * Invoked each time there is a change in the text to be rendered. + * Invoked each time there is a change in the {@link Cue}s to be rendered. * - * @param text The text to render, or null if no text is to be rendered. + * @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered. */ - void onText(String text); + void onCues(List cues); } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 3827e88a26..4a12b09777 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -30,6 +30,8 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; +import java.util.Collections; +import java.util.List; /** * A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a @@ -255,34 +257,36 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } private void updateTextRenderer(long positionUs) { - String text = subtitle.getText(positionUs); + List cues = subtitle.getCues(positionUs); if (textRendererHandler != null) { - textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget(); + textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, cues).sendToTarget(); } else { - invokeRendererInternal(text); + invokeRendererInternalCues(cues); } } private void clearTextRenderer() { if (textRendererHandler != null) { - textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget(); + textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, Collections.emptyList()) + .sendToTarget(); } else { - invokeRendererInternal(null); + invokeRendererInternalCues(Collections.emptyList()); } } + @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_OVERLAY: - invokeRendererInternal((String) msg.obj); + invokeRendererInternalCues((List) msg.obj); return true; } return false; } - private void invokeRendererInternal(String text) { - textRenderer.onText(text); + private void invokeRendererInternalCues(List cues) { + textRenderer.onCues(cues); } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index 48438ce1b4..d2f49d4038 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.TextRenderer; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; @@ -31,6 +32,7 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; +import java.util.Collections; import java.util.TreeSet; /** @@ -227,8 +229,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { return false; } - private void invokeRendererInternal(String text) { - textRenderer.onText(text); + private void invokeRendererInternal(String cueText) { + Cue cue = new Cue(cueText); + textRenderer.onCues(Collections.singletonList(cue)); } private void maybeParsePendingSample() { diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java index a0c4da091e..f010a747c3 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java @@ -15,9 +15,13 @@ */ package com.google.android.exoplayer.text.ttml; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.util.Util; +import java.util.Collections; +import java.util.List; + /** * A representation of a TTML subtitle. */ @@ -60,8 +64,14 @@ public final class TtmlSubtitle implements Subtitle { } @Override - public String getText(long timeUs) { - return root.getText(timeUs - startTimeUs); + public List getCues(long timeUs) { + String cueText = root.getText(timeUs - startTimeUs); + if (cueText == null) { + return Collections.emptyList(); + } else { + Cue cue = new Cue(cueText); + return Collections.singletonList(cue); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java new file mode 100644 index 0000000000..1d6d3c554a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java @@ -0,0 +1,55 @@ +/* + * 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.webvtt; + +import com.google.android.exoplayer.text.Cue; + +import android.text.Layout.Alignment; + +/** + * A representation of a WebVTT cue. + */ +/* package */ final class WebvttCue extends Cue { + + public final long startTime; + public final long endTime; + + public WebvttCue(CharSequence text) { + this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text); + } + + public WebvttCue(long startTime, long endTime, CharSequence text) { + this(startTime, endTime, text, Cue.UNSET_VALUE, Cue.UNSET_VALUE, null, Cue.UNSET_VALUE); + } + + public WebvttCue(long startTime, long endTime, CharSequence text, int line, int position, + Alignment alignment, int size) { + super(text, line, position, alignment, size); + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Returns whether or not this cue should be placed in the default position and rolled-up with + * the other "normal" cues. + * + * @return True if this cue should be placed in the default position; false otherwise. + */ + public boolean isNormalCue() { + return (line == UNSET_VALUE && position == UNSET_VALUE); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java index 5d331a78b2..93b52c48aa 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java @@ -17,9 +17,14 @@ package com.google.android.exoplayer.text.webvtt; import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.SubtitleParser; import com.google.android.exoplayer.util.MimeTypes; +import android.text.Html; +import android.text.Layout.Alignment; +import android.util.Log; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -35,6 +40,8 @@ import java.util.regex.Pattern; */ public class WebvttParser implements SubtitleParser { + static final String TAG = "WebvttParser"; + /** * This parser allows a custom header to be prepended to the WebVTT data, in the form of a text * line starting with this string. @@ -63,21 +70,26 @@ public class WebvttParser implements SubtitleParser { private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}"; private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING); + private static final String WEBVTT_CUE_SETTING_STRING = "\\S*:\\S*"; + private static final Pattern WEBVTT_CUE_SETTING = Pattern.compile(WEBVTT_CUE_SETTING_STRING); + private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+"); private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+"); - private static final String WEBVTT_CUE_TAG_STRING = "\\<.*?>"; + private static final String NON_NUMERIC_STRING = ".*[^0-9].*"; + + private final StringBuilder textBuilder; private final boolean strictParsing; - private final boolean filterTags; public WebvttParser() { - this(true, true); + this(true); } - public WebvttParser(boolean strictParsing, boolean filterTags) { + public WebvttParser(boolean strictParsing) { this.strictParsing = strictParsing; - this.filterTags = filterTags; + + textBuilder = new StringBuilder(); } @Override @@ -145,6 +157,7 @@ public class WebvttParser implements SubtitleParser { // process the cues and text while ((line = webvttData.readLine()) != null) { + // parse the cue identifier (if present) { Matcher matcher = WEBVTT_CUE_IDENTIFIER.matcher(line); if (matcher.find()) { @@ -152,11 +165,16 @@ public class WebvttParser implements SubtitleParser { line = webvttData.readLine(); } + long startTime = Cue.UNSET_VALUE; + long endTime = Cue.UNSET_VALUE; + CharSequence text = null; + int lineNum = Cue.UNSET_VALUE; + int position = Cue.UNSET_VALUE; + Alignment alignment = null; + int size = Cue.UNSET_VALUE; + // parse the cue timestamps matcher = WEBVTT_TIMESTAMP.matcher(line); - long startTime; - long endTime; - String text = ""; // parse start timestamp if (!matcher.find()) { @@ -166,36 +184,76 @@ public class WebvttParser implements SubtitleParser { } // parse end timestamp + String endTimeString; if (!matcher.find()) { throw new ParserException("Expected cue end time: " + line); } else { - endTime = parseTimestampUs(matcher.group()) + mediaTimestampUs; + endTimeString = matcher.group(); + endTime = parseTimestampUs(endTimeString) + mediaTimestampUs; + } + + // parse the (optional) cue setting list + line = line.substring(line.indexOf(endTimeString) + endTimeString.length()); + matcher = WEBVTT_CUE_SETTING.matcher(line); + while (matcher.find()) { + String match = matcher.group(); + String[] parts = match.split(":", 2); + String name = parts[0]; + String value = parts[1]; + + try { + if ("line".equals(name)) { + if (value.endsWith("%")) { + lineNum = parseIntPercentage(value); + } else if (value.matches(NON_NUMERIC_STRING)) { + Log.w(TAG, "Invalid line value: " + value); + } else { + lineNum = Integer.parseInt(value); + } + } else if ("align".equals(name)) { + // TODO: handle for RTL languages + if ("start".equals(value)) { + alignment = Alignment.ALIGN_NORMAL; + } else if ("middle".equals(value)) { + alignment = Alignment.ALIGN_CENTER; + } else if ("end".equals(value)) { + alignment = Alignment.ALIGN_OPPOSITE; + } else if ("left".equals(value)) { + alignment = Alignment.ALIGN_NORMAL; + } else if ("right".equals(value)) { + alignment = Alignment.ALIGN_OPPOSITE; + } else { + Log.w(TAG, "Invalid align value: " + value); + } + } else if ("position".equals(name)) { + position = parseIntPercentage(value); + } else if ("size".equals(name)) { + size = parseIntPercentage(value); + } else { + Log.w(TAG, "Unknown cue setting " + name + ":" + value); + } + } catch (NumberFormatException e) { + Log.w(TAG, name + " contains an invalid value " + value, e); + } } // parse text + textBuilder.setLength(0); while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) { - text += processCueText(line.trim()) + "\n"; + if (textBuilder.length() > 0) { + textBuilder.append("
"); + } + textBuilder.append(line.trim()); } + text = Html.fromHtml(textBuilder.toString()); - WebvttCue cue = new WebvttCue(startTime, endTime, text); + WebvttCue cue = new WebvttCue(startTime, endTime, text, lineNum, position, alignment, size); subtitles.add(cue); } webvttData.close(); inputStream.close(); - - // copy WebvttCue data into arrays for WebvttSubtitle constructor - String[] cueText = new String[subtitles.size()]; - long[] cueTimesUs = new long[2 * subtitles.size()]; - for (int subtitleIndex = 0; subtitleIndex < subtitles.size(); subtitleIndex++) { - int arrayIndex = subtitleIndex * 2; - WebvttCue cue = subtitles.get(subtitleIndex); - cueTimesUs[arrayIndex] = cue.startTime; - cueTimesUs[arrayIndex + 1] = cue.endTime; - cueText[subtitleIndex] = cue.text; - } - - WebvttSubtitle subtitle = new WebvttSubtitle(cueText, mediaTimestampUs, cueTimesUs); + WebvttSubtitle subtitle = new WebvttSubtitle(subtitles, mediaTimestampUs); return subtitle; } @@ -208,25 +266,29 @@ public class WebvttParser implements SubtitleParser { return startTimeUs; } - protected String processCueText(String line) { - if (filterTags) { - line = line.replaceAll(WEBVTT_CUE_TAG_STRING, ""); - line = line.replaceAll("<", "<"); - line = line.replaceAll(">", ">"); - line = line.replaceAll(" ", " "); - line = line.replaceAll("&", "&"); - return line; - } else { - return line; - } - } - protected void handleNoncompliantLine(String line) throws ParserException { if (strictParsing) { throw new ParserException("Unexpected line: " + line); } } + private static int parseIntPercentage(String s) throws NumberFormatException { + if (!s.endsWith("%")) { + throw new NumberFormatException(s + " doesn't end with '%'"); + } + + s = s.substring(0, s.length() - 1); + if (s.matches(NON_NUMERIC_STRING)) { + throw new NumberFormatException(s + " contains an invalid character"); + } + + int value = Integer.parseInt(s); + if (value < 0 || value > 100) { + throw new NumberFormatException(value + " is out of range [0-100]"); + } + return value; + } + private static long parseTimestampUs(String s) throws NumberFormatException { if (!s.matches(WEBVTT_TIMESTAMP_STRING)) { throw new NumberFormatException("has invalid format"); @@ -240,16 +302,4 @@ public class WebvttParser implements SubtitleParser { return (value * 1000 + Long.parseLong(parts[1])) * 1000; } - private static class WebvttCue { - public final long startTime; - public final long endTime; - public final String text; - - public WebvttCue(long startTime, long endTime, String text) { - this.startTime = startTime; - this.endTime = endTime; - this.text = text; - } - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java index cc6bdc4ef4..e339782ab2 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java @@ -15,32 +15,46 @@ */ package com.google.android.exoplayer.text.webvtt; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; +import android.text.SpannableStringBuilder; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * A representation of a WebVTT subtitle. */ public class WebvttSubtitle implements Subtitle { - private final String[] cueText; + private final List cues; + private final int numCues; private final long startTimeUs; private final long[] cueTimesUs; private final long[] sortedCueTimesUs; /** - * @param cueText Text to be displayed during each cue. + * @param cues A list of the cues in this subtitle. * @param startTimeUs The start time of the subtitle. - * @param cueTimesUs Cue event times, where cueTimesUs[2 * i] and cueTimesUs[(2 * i) + 1] are - * the start and end times, respectively, corresponding to cueText[i]. */ - public WebvttSubtitle(String[] cueText, long startTimeUs, long[] cueTimesUs) { - this.cueText = cueText; + public WebvttSubtitle(List cues, long startTimeUs) { + this.cues = cues; + numCues = cues.size(); this.startTimeUs = startTimeUs; - this.cueTimesUs = cueTimesUs; + + this.cueTimesUs = new long[2 * numCues]; + for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { + WebvttCue cue = cues.get(cueIndex); + int arrayIndex = cueIndex * 2; + cueTimesUs[arrayIndex] = cue.startTime; + cueTimesUs[arrayIndex + 1] = cue.endTime; + } + this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); } @@ -78,22 +92,47 @@ public class WebvttSubtitle implements Subtitle { } @Override - public String getText(long timeUs) { - StringBuilder stringBuilder = new StringBuilder(); + public List getCues(long timeUs) { + ArrayList list = null; + WebvttCue firstNormalCue = null; + SpannableStringBuilder normalCueTextBuilder = null; - for (int i = 0; i < cueTimesUs.length; i += 2) { - if ((cueTimesUs[i] <= timeUs) && (timeUs < cueTimesUs[i + 1])) { - stringBuilder.append(cueText[i / 2]); + for (int i = 0; i < numCues; i++) { + if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { + if (list == null) { + list = new ArrayList(); + } + WebvttCue cue = cues.get(i); + if (cue.isNormalCue()) { + // we want to merge all of the normal cues into a single cue to ensure they are drawn + // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple + // normal cues, otherwise we can just append the single normal cue + if (firstNormalCue == null) { + firstNormalCue = cue; + } else if (normalCueTextBuilder == null) { + normalCueTextBuilder = new SpannableStringBuilder(); + normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); + } else { + normalCueTextBuilder.append("\n").append(cue.text); + } + } else { + list.add(cue); + } } } - - int stringLength = stringBuilder.length(); - if (stringLength > 0 && stringBuilder.charAt(stringLength - 1) == '\n') { - // Adjust the length to remove the trailing newline character. - stringLength -= 1; + if (normalCueTextBuilder != null) { + // there were multiple normal cues, so create a new cue with all of the text + list.add(new WebvttCue(normalCueTextBuilder)); + } else if (firstNormalCue != null) { + // there was only a single normal cue, so just add it to the list + list.add(firstNormalCue); } - return stringLength == 0 ? null : stringBuilder.substring(0, stringLength); + if (list != null) { + return list; + } else { + return Collections.emptyList(); + } } } diff --git a/library/src/test/assets/webvtt/typical_with_tags b/library/src/test/assets/webvtt/typical_with_tags index 36e630e240..aecf1cb2b7 100644 --- a/library/src/test/assets/webvtt/typical_with_tags +++ b/library/src/test/assets/webvtt/typical_with_tags @@ -11,4 +11,4 @@ This is the second subtitle. This is the third subtitle. 00:06.000 --> 00:07.000 -This is the <fourth> &subtitle. +This is the <fourth> &subtitle. diff --git a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java index 426c5152df..e5bbf7b331 100644 --- a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java +++ b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java @@ -59,13 +59,13 @@ public class WebvttParserTest extends InstrumentationTestCase { // test first cue assertEquals(startTimeUs, subtitle.getEventTime(0)); assertEquals("This is the first subtitle.", - subtitle.getText(subtitle.getEventTime(0))); + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); // test second cue assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); assertEquals("This is the second subtitle.", - subtitle.getText(subtitle.getEventTime(2))); + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); } @@ -84,13 +84,13 @@ public class WebvttParserTest extends InstrumentationTestCase { // test first cue assertEquals(startTimeUs, subtitle.getEventTime(0)); assertEquals("This is the first subtitle.", - subtitle.getText(subtitle.getEventTime(0))); + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); // test second cue assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); assertEquals("This is the second subtitle.", - subtitle.getText(subtitle.getEventTime(2))); + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); } @@ -109,25 +109,25 @@ public class WebvttParserTest extends InstrumentationTestCase { // test first cue assertEquals(startTimeUs, subtitle.getEventTime(0)); assertEquals("This is the first subtitle.", - subtitle.getText(subtitle.getEventTime(0))); + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); // test second cue assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); assertEquals("This is the second subtitle.", - subtitle.getText(subtitle.getEventTime(2))); + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); // test third cue assertEquals(startTimeUs + 4000000, subtitle.getEventTime(4)); assertEquals("This is the third subtitle.", - subtitle.getText(subtitle.getEventTime(4))); + subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString()); assertEquals(startTimeUs + 5000000, subtitle.getEventTime(5)); // test fourth cue assertEquals(startTimeUs + 6000000, subtitle.getEventTime(6)); assertEquals("This is the &subtitle.", - subtitle.getText(subtitle.getEventTime(6))); + subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString()); assertEquals(startTimeUs + 7000000, subtitle.getEventTime(7)); } diff --git a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java index e95482f0fb..fc2ac13de7 100644 --- a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java +++ b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java @@ -15,8 +15,13 @@ */ package com.google.android.exoplayer.text.webvtt; +import com.google.android.exoplayer.text.Cue; + import junit.framework.TestCase; +import java.util.ArrayList; +import java.util.List; + /** * Unit test for {@link WebvttSubtitle}. */ @@ -25,21 +30,39 @@ public class WebvttSubtitleTest extends TestCase { private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle."; private static final String FIRST_AND_SECOND_SUBTITLE_STRING = - FIRST_SUBTITLE_STRING + SECOND_SUBTITLE_STRING; + FIRST_SUBTITLE_STRING + "\n" + SECOND_SUBTITLE_STRING; - private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new String[] {}, 0, new long[] {}); + private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new ArrayList(), 0); - private WebvttSubtitle simpleSubtitle = new WebvttSubtitle( - new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, - new long[] {1000000, 2000000, 3000000, 4000000}); + private ArrayList simpleSubtitleCues = new ArrayList(); + { + WebvttCue firstCue = new WebvttCue(1000000, 2000000, FIRST_SUBTITLE_STRING); + simpleSubtitleCues.add(firstCue); - private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle( - new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, - new long[] {1000000, 3000000, 2000000, 4000000}); + WebvttCue secondCue = new WebvttCue(3000000, 4000000, SECOND_SUBTITLE_STRING); + simpleSubtitleCues.add(secondCue); + } + private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues, 0); - private WebvttSubtitle nestedSubtitle = new WebvttSubtitle( - new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, - new long[] {1000000, 4000000, 2000000, 3000000}); + private ArrayList overlappingSubtitleCues = new ArrayList(); + { + WebvttCue firstCue = new WebvttCue(1000000, 3000000, FIRST_SUBTITLE_STRING); + overlappingSubtitleCues.add(firstCue); + + WebvttCue secondCue = new WebvttCue(2000000, 4000000, SECOND_SUBTITLE_STRING); + overlappingSubtitleCues.add(secondCue); + } + private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues, 0); + + private ArrayList nestedSubtitleCues = new ArrayList(); + { + WebvttCue firstCue = new WebvttCue(1000000, 4000000, FIRST_SUBTITLE_STRING); + nestedSubtitleCues.add(firstCue); + + WebvttCue secondCue = new WebvttCue(2000000, 3000000, SECOND_SUBTITLE_STRING); + nestedSubtitleCues.add(secondCue); + } + private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues, 0); public void testEventCount() { assertEquals(0, emptySubtitle.getEventTimeCount()); @@ -72,29 +95,29 @@ public class WebvttSubtitleTest extends TestCase { public void testSimpleSubtitleText() { // Test before first subtitle - assertNull(simpleSubtitle.getText(0)); - assertNull(simpleSubtitle.getText(500000)); - assertNull(simpleSubtitle.getText(999999)); + assertSingleCueEmpty(simpleSubtitle.getCues(0)); + assertSingleCueEmpty(simpleSubtitle.getCues(500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1000000)); - assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1500000)); - assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999)); // Test after first subtitle, before second subtitle - assertNull(simpleSubtitle.getText(2000000)); - assertNull(simpleSubtitle.getText(2500000)); - assertNull(simpleSubtitle.getText(2999999)); + assertSingleCueEmpty(simpleSubtitle.getCues(2000000)); + assertSingleCueEmpty(simpleSubtitle.getCues(2500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(2999999)); // Test second subtitle - assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3000000)); - assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3500000)); - assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3999999)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999)); // Test after second subtitle - assertNull(simpleSubtitle.getText(4000000)); - assertNull(simpleSubtitle.getText(4500000)); - assertNull(simpleSubtitle.getText(Long.MAX_VALUE)); + assertSingleCueEmpty(simpleSubtitle.getCues(4000000)); + assertSingleCueEmpty(simpleSubtitle.getCues(4500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE)); } public void testOverlappingSubtitleEventTimes() { @@ -107,29 +130,32 @@ public class WebvttSubtitleTest extends TestCase { public void testOverlappingSubtitleText() { // Test before first subtitle - assertNull(overlappingSubtitle.getText(0)); - assertNull(overlappingSubtitle.getText(500000)); - assertNull(overlappingSubtitle.getText(999999)); + assertSingleCueEmpty(overlappingSubtitle.getCues(0)); + assertSingleCueEmpty(overlappingSubtitle.getCues(500000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1000000)); - assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1500000)); - assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999)); // Test after first and second subtitle - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2000000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2500000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2999999)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, + overlappingSubtitle.getCues(2000000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, + overlappingSubtitle.getCues(2500000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, + overlappingSubtitle.getCues(2999999)); // Test second subtitle - assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3000000)); - assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3500000)); - assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3999999)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999)); // Test after second subtitle - assertNull(overlappingSubtitle.getText(4000000)); - assertNull(overlappingSubtitle.getText(4500000)); - assertNull(overlappingSubtitle.getText(Long.MAX_VALUE)); + assertSingleCueEmpty(overlappingSubtitle.getCues(4000000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(4500000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE)); } public void testNestedSubtitleEventTimes() { @@ -142,29 +168,29 @@ public class WebvttSubtitleTest extends TestCase { public void testNestedSubtitleText() { // Test before first subtitle - assertNull(nestedSubtitle.getText(0)); - assertNull(nestedSubtitle.getText(500000)); - assertNull(nestedSubtitle.getText(999999)); + assertSingleCueEmpty(nestedSubtitle.getCues(0)); + assertSingleCueEmpty(nestedSubtitle.getCues(500000)); + assertSingleCueEmpty(nestedSubtitle.getCues(999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1000000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1500000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999)); // Test after first and second subtitle - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2000000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2500000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2999999)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3000000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3500000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999)); // Test after second subtitle - assertNull(nestedSubtitle.getText(4000000)); - assertNull(nestedSubtitle.getText(4500000)); - assertNull(nestedSubtitle.getText(Long.MAX_VALUE)); + assertSingleCueEmpty(nestedSubtitle.getCues(4000000)); + assertSingleCueEmpty(nestedSubtitle.getCues(4500000)); + assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE)); } private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { @@ -201,4 +227,13 @@ public class WebvttSubtitleTest extends TestCase { assertEquals(-1, subtitle.getNextEventTimeIndex(Long.MAX_VALUE)); } + private void assertSingleCueEmpty(List cues) { + assertTrue(cues.size() == 0); + } + + private void assertSingleCueTextEquals(String expected, List cues) { + assertTrue(cues.size() == 1); + assertEquals(expected, cues.get(0).text.toString()); + } + }