Further improve WebVTT parser according to WebVTT spec

This commit is contained in:
Oliver Woodman 2015-09-28 12:20:27 +01:00
parent 71f542f7c2
commit e4e02f9189
10 changed files with 408 additions and 333 deletions

View File

@ -1,7 +0,0 @@
WEBVTT
00:00.000 --> 00:01.234
This is the first subtitle.
00:02.345 --> 00:03.456
This is the second subtitle.

View File

@ -8,7 +8,9 @@ with multiple lines
00:00.000 --> 00:01.234
This is the first subtitle.
NOTE Single line comment
NOTE Single line comment with a space
NOTE Single line comment with a tab
2
00:02.345 --> 00:03.456

View File

@ -16,7 +16,13 @@ NOTE Line as percentage and line alignment
00:04.000 --> 00:05.000 line:45%,end align:middle size:35%
This is the third subtitle.
NOTE Line as absolute negative number and without line alignment
NOTE Line as absolute negative number and without line alignment.
00:06.000 --> 00:07.000 line:-10 align:middle
This is the fourth subtitle.
NOTE The positioning alignment should be inherited from align.
00:07.000 --> 00:08.000 position:10% align:end size:10%
This is the fifth subtitle.
00:06.000 --> 00:07.000 line:-10 align:middle size:35%
This is the forth subtitle.

View File

@ -15,32 +15,31 @@
*/
package com.google.android.exoplayer.text.webvtt;
import android.test.InstrumentationTestCase;
import android.text.Layout;
import com.google.android.exoplayer.text.Cue;
import android.test.InstrumentationTestCase;
import android.text.Layout.Alignment;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* Unit test for {@link WebvttParser}.
*/
public class WebvttParserTest extends InstrumentationTestCase {
private static final String TYPICAL_WEBVTT_FILE = "webvtt/typical";
private static final String TYPICAL_WITH_IDS_WEBVTT_FILE = "webvtt/typical_with_identifiers";
private static final String TYPICAL_WITH_TAGS_WEBVTT_FILE = "webvtt/typical_with_tags";
private static final String TYPICAL_WITH_COMMENTS_WEBVTT_FILE = "webvtt/typical_with_comments";
private static final String TYPICAL_WITH_METADATA_WEBVTT_FILE = "webvtt/typical_with_metadata";
private static final String LIVE_TYPICAL_WEBVTT_FILE = "webvtt/live_typical";
private static final String EMPTY_WEBVTT_FILE = "webvtt/empty";
private static final String TYPICAL_FILE = "webvtt/typical";
private static final String TYPICAL_WITH_IDS_FILE = "webvtt/typical_with_identifiers";
private static final String TYPICAL_WITH_COMMENTS_FILE = "webvtt/typical_with_comments";
private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning";
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
private static final String EMPTY_FILE = "webvtt/empty";
public void testParseNullWebvttFile() throws IOException {
public void testParseEmpty() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_WEBVTT_FILE);
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
.open(EMPTY_FILE);
try {
parser.parse(inputStream);
fail("Expected IOException");
@ -49,189 +48,113 @@ public class WebvttParserTest extends InstrumentationTestCase {
}
}
public void testParseTypicalWebvttFile() throws IOException {
public void testParseTypical() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_WEBVTT_FILE);
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
assertEquals(0, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
assertEquals(1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
assertEquals(3456000, subtitle.getEventTime(3));
// test cues
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
}
public void testParseTypicalWithIdsWebvttFile() throws IOException {
public void testParseTypicalWithIds() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_IDS_WEBVTT_FILE);
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_IDS_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
assertEquals(0, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
assertEquals(1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
assertEquals(3456000, subtitle.getEventTime(3));
// test cues
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
}
public void testParseTypicalWithTagsWebvttFile() throws IOException {
public void testParseTypicalWithComments() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_TAGS_WEBVTT_FILE);
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_COMMENTS_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
// test cues
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
}
public void testParseWithTags() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
.open(WITH_TAGS_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
assertEquals(8, subtitle.getEventTimeCount());
// test first cue
assertEquals(0, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
assertEquals(1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
assertEquals(3456000, subtitle.getEventTime(3));
// test third cue
assertEquals(4000000, subtitle.getEventTime(4));
assertEquals("This is the third subtitle.",
subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString());
assertEquals(5000000, subtitle.getEventTime(5));
// test fourth cue
assertEquals(6000000, subtitle.getEventTime(6));
assertEquals("This is the <fourth> &subtitle.",
subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString());
assertEquals(7000000, subtitle.getEventTime(7));
// test cues
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.");
assertCue(subtitle, 6, 6000000, 7000000, "This is the <fourth> &subtitle.");
}
public void testParseTypicalWithCommentsWebvttFile() throws IOException {
public void testParseWithPositioning() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_COMMENTS_WEBVTT_FILE);
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
.open(WITH_POSITIONING_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
assertEquals(10, subtitle.getEventTimeCount());
// test first cue
assertEquals(0, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
assertEquals(1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
assertEquals(3456000, subtitle.getEventTime(3));
// test cues
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL,
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f);
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.",
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, 0.35f);
assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.",
Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, 0.35f);
assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.",
Alignment.ALIGN_CENTER, -10f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.",
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f,
Cue.ANCHOR_TYPE_END, 0.1f);
}
public void testParseTypicalWithMetadataWebvttFile() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_METADATA_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
assertEquals(8, subtitle.getEventTimeCount());
// test first cue
assertEquals(0, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
assertEquals(10,
subtitle.getCues(subtitle.getEventTime(0)).get(0).position);
assertEquals(Layout.Alignment.ALIGN_NORMAL,
subtitle.getCues(subtitle.getEventTime(0)).get(0).alignment);
assertEquals(35,
subtitle.getCues(subtitle.getEventTime(0)).get(0).size);
assertEquals(1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
assertEquals(Cue.UNSET_VALUE,
subtitle.getCues(subtitle.getEventTime(2)).get(0).position);
assertEquals(Layout.Alignment.ALIGN_OPPOSITE,
subtitle.getCues(subtitle.getEventTime(2)).get(0).alignment);
assertEquals(35,
subtitle.getCues(subtitle.getEventTime(2)).get(0).size);
assertEquals(3456000, subtitle.getEventTime(3));
// test third cue
assertEquals(4000000, subtitle.getEventTime(4));
assertEquals("This is the third subtitle.",
subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString());
assertEquals(45,
subtitle.getCues(subtitle.getEventTime(4)).get(0).line);
assertEquals(Layout.Alignment.ALIGN_CENTER,
subtitle.getCues(subtitle.getEventTime(4)).get(0).alignment);
assertEquals(35,
subtitle.getCues(subtitle.getEventTime(4)).get(0).size);
assertEquals(5000000, subtitle.getEventTime(5));
// test forth cue
assertEquals(6000000, subtitle.getEventTime(6));
assertEquals("This is the forth subtitle.",
subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString());
assertEquals(-10,
subtitle.getCues(subtitle.getEventTime(6)).get(0).line);
assertEquals(Layout.Alignment.ALIGN_CENTER,
subtitle.getCues(subtitle.getEventTime(6)).get(0).alignment);
assertEquals(35,
subtitle.getCues(subtitle.getEventTime(6)).get(0).size);
assertEquals(7000000, subtitle.getEventTime(7));
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
int endTimeUs, String text) {
assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
}
public void testParseLiveTypicalWebvttFile() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(LIVE_TYPICAL_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream);
// test event count
long startTimeUs = 0;
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
assertEquals(startTimeUs, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
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.getCues(subtitle.getEventTime(2)).get(0).text.toString());
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
int endTimeUs, String text, Alignment textAlignment, float line, int lineType, int lineAnchor,
float position, int positionAnchor, float size) {
assertEquals(startTimeUs, subtitle.getEventTime(eventTimeIndex));
assertEquals(endTimeUs, subtitle.getEventTime(eventTimeIndex + 1));
List<Cue> cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex));
assertEquals(1, cues.size());
// Assert cue properties
Cue cue = cues.get(0);
assertEquals(text, cue.text.toString());
assertEquals(textAlignment, cue.textAlignment);
assertEquals(line, cue.line);
assertEquals(lineType, cue.lineType);
assertEquals(lineAnchor, cue.lineAnchor);
assertEquals(position, cue.position);
assertEquals(positionAnchor, cue.positionAnchor);
assertEquals(size, cue.size);
}
}

View File

@ -23,30 +23,117 @@ import android.text.Layout.Alignment;
public class Cue {
/**
* Used by some methods to indicate that no value is set.
* An unset position or width.
*/
public static final int UNSET_VALUE = -1;
public static final float DIMEN_UNSET = Float.MIN_VALUE;
/**
* An unset anchor or line type value.
*/
public static final int TYPE_UNSET = Integer.MIN_VALUE;
/**
* Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue
* box.
*/
public static final int ANCHOR_TYPE_START = 0;
/**
* Anchors the middle of the cue box.
*/
public static final int ANCHOR_TYPE_MIDDLE = 1;
/**
* Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue
* box.
*/
public static final int ANCHOR_TYPE_END = 2;
/**
* Value for {@link #lineType} when {@link #line} is a fractional position.
*/
public static final int LINE_TYPE_FRACTION = 0;
/**
* Value for {@link #lineType} when {@link #line} is a line number.
*/
public static final int LINE_TYPE_NUMBER = 1;
/**
* The cue text. Note the {@link CharSequence} may be decorated with styling spans.
*/
public final CharSequence text;
public final int line;
public final int position;
public final Alignment alignment;
public final int size;
/**
* The alignment of the cue text within the cue box.
*/
public final Alignment textAlignment;
/**
* The position of the {@link #lineAnchor} of the cue box within the viewport in the direction
* orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of
* the value depends on the value of {@link #lineType}.
* <p>
* For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the
* fractional vertical position relative to the top of the viewport.
*/
public final float line;
/**
* The type of the {@link #line} value.
* <p>
* {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
* viewport.
* <p>
* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
* line is taken to be the size of the first line of the cue. When {@link #line} is greater than
* or equal to 0, lines count from the start of the viewport (the first line is numbered 0). When
* {@link #line} is negative, lines count from the end of the viewport (the last line is numbered
* -1). For horizontal text the size of the first line of the cue is its height, and the start
* and end of the viewport are the top and bottom respectively.
*/
public final int lineType;
/**
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
* respectively.
*/
public final int lineAnchor;
/**
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
* the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}.
* <p>
* For horizontal text, this is the horizontal position relative to the left of the viewport. Note
* that positioning is relative to the left of the viewport even in the case of right-to-left
* text.
*/
public final float position;
/**
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
* respectively.
*/
public final int positionAnchor;
/**
* The size of the cue box in the writing direction specified as a fraction of the viewport size
* in that direction, or {@link #DIMEN_UNSET}.
*/
public final float size;
public Cue() {
this(null);
}
public Cue(CharSequence text) {
this(text, UNSET_VALUE, UNSET_VALUE, null, UNSET_VALUE);
this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
}
public Cue(CharSequence text, int line, int position, Alignment alignment, int size) {
public Cue(CharSequence text, Alignment textAlignment, float line, int lineType,
int lineAnchor, float position, int positionAnchor, float size) {
this.text = text;
this.textAlignment = textAlignment;
this.line = line;
this.lineType = lineType;
this.lineAnchor = lineAnchor;
this.position = position;
this.alignment = alignment;
this.positionAnchor = positionAnchor;
this.size = size;
}

View File

@ -63,8 +63,13 @@ import android.util.Log;
// Previous input variables.
private CharSequence cueText;
private int cuePosition;
private Alignment cueAlignment;
private Alignment cueTextAlignment;
private float cueLine;
private int cueLineType;
private int cueLineAnchor;
private float cuePosition;
private int cuePositionAnchor;
private float cueSize;
private boolean applyEmbeddedStyles;
private int foregroundColor;
private int backgroundColor;
@ -120,7 +125,7 @@ import android.util.Log;
* @param style The style to use when drawing the cue text.
* @param textSizePx The text size to use when drawing the cue text, in pixels.
* @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is
* {@link Cue#UNSET_VALUE}, as a fraction of the viewport height
* {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw.
* @param cueBoxLeft The left position of the enclosing cue box.
* @param cueBoxTop The top position of the enclosing cue box.
@ -140,8 +145,13 @@ import android.util.Log;
cueText = cueText.toString();
}
if (areCharSequencesEqual(this.cueText, cueText)
&& Util.areEqual(this.cueTextAlignment, cue.textAlignment)
&& this.cueLine == cue.line
&& this.cueLineType == cue.lineType
&& Util.areEqual(this.cueLineAnchor, cue.lineAnchor)
&& this.cuePosition == cue.position
&& Util.areEqual(this.cueAlignment, cue.alignment)
&& Util.areEqual(this.cuePositionAnchor, cue.positionAnchor)
&& this.cueSize == cue.size
&& this.applyEmbeddedStyles == applyEmbeddedStyles
&& this.foregroundColor == style.foregroundColor
&& this.backgroundColor == style.backgroundColor
@ -161,8 +171,13 @@ import android.util.Log;
}
this.cueText = cueText;
this.cueTextAlignment = cue.textAlignment;
this.cueLine = cue.line;
this.cueLineType = cue.lineType;
this.cueLineAnchor = cue.lineAnchor;
this.cuePosition = cue.position;
this.cueAlignment = cue.alignment;
this.cuePositionAnchor = cue.positionAnchor;
this.cueSize = cue.size;
this.applyEmbeddedStyles = applyEmbeddedStyles;
this.foregroundColor = style.foregroundColor;
this.backgroundColor = style.backgroundColor;
@ -182,16 +197,19 @@ import android.util.Log;
textPaint.setTextSize(textSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) {
availableWidth = (int) (availableWidth * cueSize);
}
if (availableWidth <= 0) {
Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
return;
}
Alignment layoutAlignment = cueAlignment == null ? Alignment.ALIGN_CENTER : cueAlignment;
textLayout = new StaticLayout(cueText, textPaint, availableWidth, layoutAlignment, spacingMult,
Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,
spacingAdd, true);
int textHeight = textLayout.getHeight();
int textWidth = 0;
int lineCount = textLayout.getLineCount();
@ -202,14 +220,13 @@ import android.util.Log;
int textLeft;
int textRight;
if (cue.position != Cue.UNSET_VALUE) {
if (cue.alignment == Alignment.ALIGN_OPPOSITE) {
textRight = (parentWidth * cue.position) / 100 + parentLeft;
textLeft = Math.max(textRight - textWidth, parentLeft);
} else {
textLeft = (parentWidth * cue.position) / 100 + parentLeft;
textRight = Math.min(textLeft + textWidth, parentRight);
}
if (cuePosition != Cue.DIMEN_UNSET) {
int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;
textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2
: anchorPosition;
textLeft = Math.max(textLeft, parentLeft);
textRight = Math.min(textLeft + textWidth, parentRight);
} else {
textLeft = (parentWidth - textWidth) / 2;
textRight = textLeft + textWidth;
@ -217,12 +234,29 @@ import android.util.Log;
int textTop;
int textBottom;
if (cue.line != Cue.UNSET_VALUE) {
textTop = (parentHeight * cue.line) / 100 + parentTop;
if (cueLine != Cue.DIMEN_UNSET) {
int anchorPosition;
if (cueLineType == Cue.LINE_TYPE_FRACTION) {
anchorPosition = Math.round(parentHeight * cueLine) + parentTop;
} else {
// cueLineType == Cue.LINE_TYPE_NUMBER
int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0);
if (cueLine >= 0) {
anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop;
} else {
anchorPosition = Math.round(cueLine * firstLineHeight) + parentBottom;
}
}
textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2
: anchorPosition;
textBottom = textTop + textHeight;
if (textBottom > parentBottom) {
textTop = parentBottom - textHeight;
textBottom = parentBottom;
} else if (textTop < parentTop) {
textTop = parentTop;
textBottom = parentTop + textHeight;
}
} else {
textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);
@ -232,7 +266,7 @@ import android.util.Log;
textWidth = textRight - textLeft;
// Update the derived drawing variables.
this.textLayout = new StaticLayout(cueText, textPaint, textWidth, layoutAlignment, spacingMult,
this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult,
spacingAdd, true);
this.textLeft = textLeft;
this.textTop = textTop;

View File

@ -38,7 +38,7 @@ public final class SubtitleLayout extends View {
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
/**
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE}, as a
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET}, as a
* fraction of the viewport height.
*
* @see #setBottomPaddingFraction(float)
@ -174,7 +174,7 @@ public final class SubtitleLayout extends View {
}
/**
* Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE},
* Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET},
* as a fraction of the view's remaining height after its top and bottom padding have been
* subtracted.
* <p>

View File

@ -28,16 +28,17 @@ import android.text.Layout.Alignment;
public final long endTime;
public WebvttCue(CharSequence text) {
this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text);
this(0, 0, text);
}
public WebvttCue(long startTime, long endTime, CharSequence text) {
this(startTime, endTime, text, Cue.UNSET_VALUE, Cue.UNSET_VALUE, null, Cue.UNSET_VALUE);
this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
}
public WebvttCue(long startTime, long endTime, CharSequence text, int line, int position,
Alignment alignment, int size) {
super(text, line, position, alignment, size);
public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) {
super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
this.startTime = startTime;
this.endTime = endTime;
}
@ -49,7 +50,7 @@ import android.text.Layout.Alignment;
* @return True if this cue should be placed in the default position; false otherwise.
*/
public boolean isNormalCue() {
return (line == UNSET_VALUE && position == UNSET_VALUE);
return (line == DIMEN_UNSET && position == DIMEN_UNSET);
}
}

View File

@ -24,7 +24,6 @@ import com.google.android.exoplayer.util.MimeTypes;
import android.text.Html;
import android.text.Layout.Alignment;
import android.util.Log;
import android.util.Pair;
import java.io.BufferedReader;
import java.io.IOException;
@ -43,34 +42,15 @@ public final class WebvttParser implements SubtitleParser {
private static final String TAG = "WebvttParser";
private static final String WEBVTT_FILE_HEADER_STRING = "^\uFEFF?WEBVTT((\u0020|\u0009).*)?$";
private static final Pattern WEBVTT_FILE_HEADER =
Pattern.compile(WEBVTT_FILE_HEADER_STRING);
private static final String WEBVTT_COMMENT_BLOCK_STRING = "^NOTE((\u0020|\u0009).*)?$";
private static final Pattern WEBVTT_COMMENT_BLOCK =
Pattern.compile(WEBVTT_COMMENT_BLOCK_STRING);
private static final String WEBVTT_METADATA_HEADER_STRING = "\\S*[:=]\\S*";
private static final Pattern WEBVTT_METADATA_HEADER =
Pattern.compile(WEBVTT_METADATA_HEADER_STRING);
private static final String WEBVTT_CUE_IDENTIFIER_STRING = "^(?!.*(-->)).*$";
private static final Pattern WEBVTT_CUE_IDENTIFIER =
Pattern.compile(WEBVTT_CUE_IDENTIFIER_STRING);
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 String WEBVTT_PERCENTAGE_NUMBER_STRING = "^([0-9]+|[0-9]+\\.[0-9]+)$";
private static final String NON_NUMERIC_STRING = ".*[^0-9].*";
private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$");
private static final Pattern COMMENT_BLOCK = Pattern.compile("^NOTE((\u0020|\u0009).*)?$");
private static final Pattern METADATA_HEADER = Pattern.compile("\\S*[:=]\\S*");
private static final Pattern CUE_IDENTIFIER = Pattern.compile("^(?!.*(-->)).*$");
private static final Pattern TIMESTAMP = Pattern.compile("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}");
private static final Pattern CUE_SETTING = Pattern.compile("\\S*:\\S*");
private final PositionHolder positionHolder;
private final StringBuilder textBuilder;
private final boolean strictParsing;
/**
@ -88,6 +68,7 @@ public final class WebvttParser implements SubtitleParser {
*/
public WebvttParser(boolean strictParsing) {
this.strictParsing = strictParsing;
positionHolder = new PositionHolder();
textBuilder = new StringBuilder();
}
@ -100,7 +81,7 @@ public final class WebvttParser implements SubtitleParser {
// file should start with "WEBVTT"
line = webvttData.readLine();
if (line == null || !WEBVTT_FILE_HEADER.matcher(line).matches()) {
if (line == null || !HEADER.matcher(line).matches()) {
throw new ParserException("Expected WEBVTT. Got " + line);
}
@ -116,7 +97,7 @@ public final class WebvttParser implements SubtitleParser {
}
if (strictParsing) {
Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line);
Matcher matcher = METADATA_HEADER.matcher(line);
if (!matcher.find()) {
throw new ParserException("Unexpected line: " + line);
}
@ -126,38 +107,45 @@ public final class WebvttParser implements SubtitleParser {
// process the cues and text
while ((line = webvttData.readLine()) != null) {
// parse webvtt comment block in case it is present
Matcher matcher = WEBVTT_COMMENT_BLOCK.matcher(line);
if(matcher.find()) {
Matcher matcher = COMMENT_BLOCK.matcher(line);
if (matcher.find()) {
// read lines until finding an empty one (webvtt line terminator: CRLF, or LF or CR)
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
// just ignoring comment text
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
// ignore comment text
}
continue;
}
// parse the cue identifier (if present) {
matcher = WEBVTT_CUE_IDENTIFIER.matcher(line);
matcher = CUE_IDENTIFIER.matcher(line);
if (matcher.find()) {
// ignore the identifier (we currently don't use it) and read the next line
line = webvttData.readLine();
if (line == null) {
// end of file
break;
}
}
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;
long cueStartTime;
long cueEndTime;
CharSequence cueText;
Alignment cueTextAlignment = null;
float cueLine = Cue.DIMEN_UNSET;
int cueLineType = Cue.TYPE_UNSET;
int cueLineAnchor = Cue.TYPE_UNSET;
float cuePosition = Cue.DIMEN_UNSET;
int cuePositionAnchor = Cue.TYPE_UNSET;
float cueWidth = Cue.DIMEN_UNSET;
// parse the cue timestamps
matcher = WEBVTT_TIMESTAMP.matcher(line);
matcher = TIMESTAMP.matcher(line);
// parse start timestamp
if (!matcher.find()) {
throw new ParserException("Expected cue start time: " + line);
} else {
startTime = parseTimestampUs(matcher.group());
cueStartTime = parseTimestampUs(matcher.group());
}
// parse end timestamp
@ -166,12 +154,12 @@ public final class WebvttParser implements SubtitleParser {
throw new ParserException("Expected cue end time: " + line);
} else {
endTimeString = matcher.group();
endTime = parseTimestampUs(endTimeString);
cueEndTime = parseTimestampUs(endTimeString);
}
// parse the (optional) cue setting list
line = line.substring(line.indexOf(endTimeString) + endTimeString.length());
matcher = WEBVTT_CUE_SETTING.matcher(line);
matcher = CUE_SETTING.matcher(line);
while (matcher.find()) {
String match = matcher.group();
String[] parts = match.split(":", 2);
@ -180,52 +168,44 @@ public final class WebvttParser implements SubtitleParser {
try {
if ("line".equals(name)) {
Pair<String, Alignment> lineMetadata = parseLinePositionAttributes(value);
value = lineMetadata.first;
if (value.endsWith("%")) {
lineNum = parseIntPercentage(value);
} else {
// Following WebVTT spec, line number can be a negative number
int sign = 1;
if (value.startsWith("-") && value.length() > 1) {
sign = -1;
value = value.substring(1);
}
if (value.matches(NON_NUMERIC_STRING)) {
Log.w(TAG, "Invalid line value: " + value);
} else {
lineNum = sign * Integer.parseInt(value);
}
}
parseLineAttribute(value, positionHolder);
cueLine = positionHolder.position;
cueLineType = positionHolder.lineType;
cueLineAnchor = positionHolder.positionAnchor;
} else if ("align".equals(name)) {
// TODO: handle for RTL languages
alignment = parseAlignment(value);
cueTextAlignment = parseTextAlignment(value);
} else if ("position".equals(name)) {
Pair<String, Alignment> lineMetadata = parseLinePositionAttributes(value);
value = lineMetadata.first;
position = parseIntPercentage(value);
parsePositionAttribute(value, positionHolder);
cuePosition = positionHolder.position;
cuePositionAnchor = positionHolder.positionAnchor;
} else if ("size".equals(name)) {
size = parseIntPercentage(value);
cueWidth = parsePercentage(value);
} else {
Log.w(TAG, "Unknown cue setting " + name + ":" + value);
}
} catch (NumberFormatException e) {
Log.w(TAG, name + " contains an invalid value " + value, e);
Log.w(TAG, e.getMessage() + ": " + match);
}
}
if (cuePosition != Cue.DIMEN_UNSET && cuePositionAnchor == Cue.TYPE_UNSET) {
// Computed position alignment should be derived from the text alignment if it has not been
// set explicitly.
cuePositionAnchor = alignmentToAnchor(cueTextAlignment);
}
// parse text
textBuilder.setLength(0);
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
if (textBuilder.length() > 0) {
textBuilder.append("<br>");
}
textBuilder.append(line.trim());
}
text = Html.fromHtml(textBuilder.toString());
cueText = Html.fromHtml(textBuilder.toString());
WebvttCue cue = new WebvttCue(startTime, endTime, text, lineNum, position, alignment, size);
WebvttCue cue = new WebvttCue(cueStartTime, cueEndTime, cueText, cueTextAlignment, cueLine,
cueLineType, cueLineAnchor, cuePosition, cuePositionAnchor, cueWidth);
subtitles.add(cue);
}
@ -237,67 +217,116 @@ public final class WebvttParser implements SubtitleParser {
return MimeTypes.TEXT_VTT.equals(mimeType);
}
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(WEBVTT_PERCENTAGE_NUMBER_STRING)) {
throw new NumberFormatException(s + " contains an invalid character");
}
int value = Math.round(Float.parseFloat(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");
}
String[] parts = s.split("\\.", 2);
long value = 0;
for (String group : parts[0].split(":")) {
value = value * 60 + Long.parseLong(group);
String[] parts = s.split("\\.", 2);
String[] subparts = parts[0].split(":");
for (int i = 0; i < subparts.length; i++) {
value = value * 60 + Long.parseLong(subparts[i]);
}
return (value * 1000 + Long.parseLong(parts[1])) * 1000;
}
private static Pair<String, Alignment> parseLinePositionAttributes(String s) {
String value;
Alignment alignment = null;
int commaPos;
if ((commaPos = s.indexOf(",")) > 0 && commaPos < s.length() - 1) {
alignment = parseAlignment(s.substring(commaPos + 1));
value = s.substring(0, commaPos);
private static void parseLineAttribute(String s, PositionHolder out)
throws NumberFormatException {
int lineAnchor;
int commaPosition = s.indexOf(",");
if (commaPosition != -1) {
lineAnchor = parsePositionAnchor(s.substring(commaPosition + 1));
s = s.substring(0, commaPosition);
} else {
value = s;
lineAnchor = Cue.TYPE_UNSET;
}
return new Pair<String, Alignment>(value, alignment);
float line;
int lineType;
if (s.endsWith("%")) {
line = parsePercentage(s);
lineType = Cue.LINE_TYPE_FRACTION;
} else {
line = Integer.parseInt(s);
lineType = Cue.LINE_TYPE_NUMBER;
}
out.position = line;
out.positionAnchor = lineAnchor;
out.lineType = lineType;
}
private static Alignment parseAlignment(String s) {
Alignment alignment = null;
if ("start".equals(s)) {
alignment = Alignment.ALIGN_NORMAL;
} else if ("middle".equals(s)) {
alignment = Alignment.ALIGN_CENTER;
} else if ("end".equals(s)) {
alignment = Alignment.ALIGN_OPPOSITE;
} else if ("left".equals(s)) {
alignment = Alignment.ALIGN_NORMAL;
} else if ("right".equals(s)) {
alignment = Alignment.ALIGN_OPPOSITE;
private static void parsePositionAttribute(String s, PositionHolder out)
throws NumberFormatException {
int positionAnchor;
int commaPosition = s.indexOf(",");
if (commaPosition != -1) {
positionAnchor = parsePositionAnchor(s.substring(commaPosition + 1));
s = s.substring(0, commaPosition);
} else {
Log.w(TAG, "Invalid align value: " + s);
positionAnchor = Cue.TYPE_UNSET;
}
return alignment;
out.position = parsePercentage(s);
out.positionAnchor = positionAnchor;
out.lineType = Cue.TYPE_UNSET;
}
private static float parsePercentage(String s) throws NumberFormatException {
if (!s.endsWith("%")) {
throw new NumberFormatException("Percentages must end with %");
}
s = s.substring(0, s.length() - 1);
return Float.parseFloat(s) / 100;
}
private static int parsePositionAnchor(String s) {
switch (s) {
case "start":
return Cue.ANCHOR_TYPE_START;
case "middle":
return Cue.ANCHOR_TYPE_MIDDLE;
case "end":
return Cue.ANCHOR_TYPE_END;
default:
Log.w(TAG, "Invalid anchor value: " + s);
return Cue.TYPE_UNSET;
}
}
private static Alignment parseTextAlignment(String s) {
switch (s) {
case "start":
case "left":
return Alignment.ALIGN_NORMAL;
case "middle":
return Alignment.ALIGN_CENTER;
case "end":
case "right":
return Alignment.ALIGN_OPPOSITE;
default:
Log.w(TAG, "Invalid alignment value: " + s);
return null;
}
}
private static int alignmentToAnchor(Alignment alignment) {
if (alignment == null) {
return Cue.TYPE_UNSET;
}
switch (alignment) {
case ALIGN_NORMAL:
return Cue.ANCHOR_TYPE_START;
case ALIGN_CENTER:
return Cue.ANCHOR_TYPE_MIDDLE;
case ALIGN_OPPOSITE:
return Cue.ANCHOR_TYPE_END;
default:
Log.w(TAG, "Unrecognized alignment: " + alignment);
return Cue.ANCHOR_TYPE_START;
}
}
private static final class PositionHolder {
public float position;
public int positionAnchor;
public int lineType;
}
}