mirror of
https://github.com/androidx/media.git
synced 2025-05-13 02:29:52 +08:00
Further improve WebVTT parser according to WebVTT spec
This commit is contained in:
parent
71f542f7c2
commit
e4e02f9189
@ -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.
|
|
@ -8,7 +8,9 @@ with multiple lines
|
|||||||
00:00.000 --> 00:01.234
|
00:00.000 --> 00:01.234
|
||||||
This is the first subtitle.
|
This is the first subtitle.
|
||||||
|
|
||||||
NOTE Single line comment
|
NOTE Single line comment with a space
|
||||||
|
|
||||||
|
NOTE Single line comment with a tab
|
||||||
|
|
||||||
2
|
2
|
||||||
00:02.345 --> 00:03.456
|
00:02.345 --> 00:03.456
|
||||||
|
@ -16,7 +16,13 @@ NOTE Line as percentage and line alignment
|
|||||||
00:04.000 --> 00:05.000 line:45%,end align:middle size:35%
|
00:04.000 --> 00:05.000 line:45%,end align:middle size:35%
|
||||||
This is the third subtitle.
|
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.
|
|
@ -15,32 +15,31 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text.webvtt;
|
package com.google.android.exoplayer.text.webvtt;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
|
||||||
import android.text.Layout;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer.text.Cue;
|
import com.google.android.exoplayer.text.Cue;
|
||||||
|
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import android.text.Layout.Alignment;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link WebvttParser}.
|
* Unit test for {@link WebvttParser}.
|
||||||
*/
|
*/
|
||||||
public class WebvttParserTest extends InstrumentationTestCase {
|
public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
private static final String TYPICAL_WEBVTT_FILE = "webvtt/typical";
|
private static final String TYPICAL_FILE = "webvtt/typical";
|
||||||
private static final String TYPICAL_WITH_IDS_WEBVTT_FILE = "webvtt/typical_with_identifiers";
|
private static final String TYPICAL_WITH_IDS_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_FILE = "webvtt/typical_with_comments";
|
||||||
private static final String TYPICAL_WITH_COMMENTS_WEBVTT_FILE = "webvtt/typical_with_comments";
|
private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning";
|
||||||
private static final String TYPICAL_WITH_METADATA_WEBVTT_FILE = "webvtt/typical_with_metadata";
|
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
|
||||||
private static final String LIVE_TYPICAL_WEBVTT_FILE = "webvtt/live_typical";
|
private static final String EMPTY_FILE = "webvtt/empty";
|
||||||
private static final String EMPTY_WEBVTT_FILE = "webvtt/empty";
|
|
||||||
|
|
||||||
public void testParseNullWebvttFile() throws IOException {
|
public void testParseEmpty() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttParser parser = new WebvttParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
|
||||||
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_WEBVTT_FILE);
|
.open(EMPTY_FILE);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parser.parse(inputStream);
|
parser.parse(inputStream);
|
||||||
fail("Expected IOException");
|
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();
|
WebvttParser parser = new WebvttParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream =
|
||||||
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_WEBVTT_FILE);
|
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_FILE);
|
||||||
WebvttSubtitle subtitle = parser.parse(inputStream);
|
WebvttSubtitle subtitle = parser.parse(inputStream);
|
||||||
|
|
||||||
// test event count
|
// test event count
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// test first cue
|
// test cues
|
||||||
assertEquals(0, subtitle.getEventTime(0));
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
||||||
assertEquals("This is the first subtitle.",
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseTypicalWithIdsWebvttFile() throws IOException {
|
public void testParseTypicalWithIds() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttParser parser = new WebvttParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
|
||||||
getInstrumentation().getContext().getResources().getAssets()
|
.open(TYPICAL_WITH_IDS_FILE);
|
||||||
.open(TYPICAL_WITH_IDS_WEBVTT_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.parse(inputStream);
|
WebvttSubtitle subtitle = parser.parse(inputStream);
|
||||||
|
|
||||||
// test event count
|
// test event count
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// test first cue
|
// test cues
|
||||||
assertEquals(0, subtitle.getEventTime(0));
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
||||||
assertEquals("This is the first subtitle.",
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseTypicalWithTagsWebvttFile() throws IOException {
|
public void testParseTypicalWithComments() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttParser parser = new WebvttParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
|
||||||
getInstrumentation().getContext().getResources().getAssets()
|
.open(TYPICAL_WITH_COMMENTS_FILE);
|
||||||
.open(TYPICAL_WITH_TAGS_WEBVTT_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);
|
WebvttSubtitle subtitle = parser.parse(inputStream);
|
||||||
|
|
||||||
// test event count
|
// test event count
|
||||||
assertEquals(8, subtitle.getEventTimeCount());
|
assertEquals(8, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// test first cue
|
// test cues
|
||||||
assertEquals(0, subtitle.getEventTime(0));
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
||||||
assertEquals("This is the first subtitle.",
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
|
||||||
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.");
|
||||||
assertEquals(1234000, subtitle.getEventTime(1));
|
assertCue(subtitle, 6, 6000000, 7000000, "This is the <fourth> &subtitle.");
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseTypicalWithCommentsWebvttFile() throws IOException {
|
public void testParseWithPositioning() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttParser parser = new WebvttParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
|
||||||
getInstrumentation().getContext().getResources().getAssets()
|
.open(WITH_POSITIONING_FILE);
|
||||||
.open(TYPICAL_WITH_COMMENTS_WEBVTT_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.parse(inputStream);
|
WebvttSubtitle subtitle = parser.parse(inputStream);
|
||||||
|
|
||||||
// test event count
|
// test event count
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(10, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// test first cue
|
// test cues
|
||||||
assertEquals(0, subtitle.getEventTime(0));
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL,
|
||||||
assertEquals("This is the first subtitle.",
|
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f);
|
||||||
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.",
|
||||||
assertEquals(1234000, subtitle.getEventTime(1));
|
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
|
||||||
|
Cue.TYPE_UNSET, 0.35f);
|
||||||
// test second cue
|
assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.",
|
||||||
assertEquals(2345000, subtitle.getEventTime(2));
|
Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
|
||||||
assertEquals("This is the second subtitle.",
|
Cue.TYPE_UNSET, 0.35f);
|
||||||
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
|
assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.",
|
||||||
assertEquals(3456000, subtitle.getEventTime(3));
|
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 {
|
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
|
||||||
WebvttParser parser = new WebvttParser();
|
int endTimeUs, String text) {
|
||||||
InputStream inputStream =
|
assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET,
|
||||||
getInstrumentation().getContext().getResources().getAssets()
|
Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
|
||||||
.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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseLiveTypicalWebvttFile() throws IOException {
|
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
|
||||||
WebvttParser parser = new WebvttParser();
|
int endTimeUs, String text, Alignment textAlignment, float line, int lineType, int lineAnchor,
|
||||||
InputStream inputStream =
|
float position, int positionAnchor, float size) {
|
||||||
getInstrumentation().getContext().getResources().getAssets().open(LIVE_TYPICAL_WEBVTT_FILE);
|
assertEquals(startTimeUs, subtitle.getEventTime(eventTimeIndex));
|
||||||
WebvttSubtitle subtitle = parser.parse(inputStream);
|
assertEquals(endTimeUs, subtitle.getEventTime(eventTimeIndex + 1));
|
||||||
|
List<Cue> cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex));
|
||||||
// test event count
|
assertEquals(1, cues.size());
|
||||||
long startTimeUs = 0;
|
// Assert cue properties
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
Cue cue = cues.get(0);
|
||||||
|
assertEquals(text, cue.text.toString());
|
||||||
// test first cue
|
assertEquals(textAlignment, cue.textAlignment);
|
||||||
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
assertEquals(line, cue.line);
|
||||||
assertEquals("This is the first subtitle.",
|
assertEquals(lineType, cue.lineType);
|
||||||
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
assertEquals(lineAnchor, cue.lineAnchor);
|
||||||
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
assertEquals(position, cue.position);
|
||||||
|
assertEquals(positionAnchor, cue.positionAnchor);
|
||||||
// test second cue
|
assertEquals(size, cue.size);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,30 +23,117 @@ import android.text.Layout.Alignment;
|
|||||||
public class Cue {
|
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 CharSequence text;
|
||||||
|
/**
|
||||||
public final int line;
|
* The alignment of the cue text within the cue box.
|
||||||
public final int position;
|
*/
|
||||||
public final Alignment alignment;
|
public final Alignment textAlignment;
|
||||||
public final int size;
|
/**
|
||||||
|
* 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() {
|
public Cue() {
|
||||||
this(null);
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cue(CharSequence text) {
|
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.text = text;
|
||||||
|
this.textAlignment = textAlignment;
|
||||||
this.line = line;
|
this.line = line;
|
||||||
|
this.lineType = lineType;
|
||||||
|
this.lineAnchor = lineAnchor;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.alignment = alignment;
|
this.positionAnchor = positionAnchor;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,8 +63,13 @@ import android.util.Log;
|
|||||||
|
|
||||||
// Previous input variables.
|
// Previous input variables.
|
||||||
private CharSequence cueText;
|
private CharSequence cueText;
|
||||||
private int cuePosition;
|
private Alignment cueTextAlignment;
|
||||||
private Alignment cueAlignment;
|
private float cueLine;
|
||||||
|
private int cueLineType;
|
||||||
|
private int cueLineAnchor;
|
||||||
|
private float cuePosition;
|
||||||
|
private int cuePositionAnchor;
|
||||||
|
private float cueSize;
|
||||||
private boolean applyEmbeddedStyles;
|
private boolean applyEmbeddedStyles;
|
||||||
private int foregroundColor;
|
private int foregroundColor;
|
||||||
private int backgroundColor;
|
private int backgroundColor;
|
||||||
@ -120,7 +125,7 @@ import android.util.Log;
|
|||||||
* @param style The style to use when drawing the cue text.
|
* @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 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
|
* @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 canvas The canvas into which to draw.
|
||||||
* @param cueBoxLeft The left position of the enclosing cue box.
|
* @param cueBoxLeft The left position of the enclosing cue box.
|
||||||
* @param cueBoxTop The top 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();
|
cueText = cueText.toString();
|
||||||
}
|
}
|
||||||
if (areCharSequencesEqual(this.cueText, cueText)
|
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
|
&& this.cuePosition == cue.position
|
||||||
&& Util.areEqual(this.cueAlignment, cue.alignment)
|
&& Util.areEqual(this.cuePositionAnchor, cue.positionAnchor)
|
||||||
|
&& this.cueSize == cue.size
|
||||||
&& this.applyEmbeddedStyles == applyEmbeddedStyles
|
&& this.applyEmbeddedStyles == applyEmbeddedStyles
|
||||||
&& this.foregroundColor == style.foregroundColor
|
&& this.foregroundColor == style.foregroundColor
|
||||||
&& this.backgroundColor == style.backgroundColor
|
&& this.backgroundColor == style.backgroundColor
|
||||||
@ -161,8 +171,13 @@ import android.util.Log;
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.cueText = cueText;
|
this.cueText = cueText;
|
||||||
|
this.cueTextAlignment = cue.textAlignment;
|
||||||
|
this.cueLine = cue.line;
|
||||||
|
this.cueLineType = cue.lineType;
|
||||||
|
this.cueLineAnchor = cue.lineAnchor;
|
||||||
this.cuePosition = cue.position;
|
this.cuePosition = cue.position;
|
||||||
this.cueAlignment = cue.alignment;
|
this.cuePositionAnchor = cue.positionAnchor;
|
||||||
|
this.cueSize = cue.size;
|
||||||
this.applyEmbeddedStyles = applyEmbeddedStyles;
|
this.applyEmbeddedStyles = applyEmbeddedStyles;
|
||||||
this.foregroundColor = style.foregroundColor;
|
this.foregroundColor = style.foregroundColor;
|
||||||
this.backgroundColor = style.backgroundColor;
|
this.backgroundColor = style.backgroundColor;
|
||||||
@ -182,16 +197,19 @@ import android.util.Log;
|
|||||||
|
|
||||||
textPaint.setTextSize(textSizePx);
|
textPaint.setTextSize(textSizePx);
|
||||||
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
|
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
|
||||||
|
|
||||||
int availableWidth = parentWidth - textPaddingX * 2;
|
int availableWidth = parentWidth - textPaddingX * 2;
|
||||||
|
if (cueSize != Cue.DIMEN_UNSET) {
|
||||||
|
availableWidth = (int) (availableWidth * cueSize);
|
||||||
|
}
|
||||||
if (availableWidth <= 0) {
|
if (availableWidth <= 0) {
|
||||||
Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
|
Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Alignment layoutAlignment = cueAlignment == null ? Alignment.ALIGN_CENTER : cueAlignment;
|
Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
|
||||||
textLayout = new StaticLayout(cueText, textPaint, availableWidth, layoutAlignment, spacingMult,
|
textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,
|
||||||
spacingAdd, true);
|
spacingAdd, true);
|
||||||
|
|
||||||
int textHeight = textLayout.getHeight();
|
int textHeight = textLayout.getHeight();
|
||||||
int textWidth = 0;
|
int textWidth = 0;
|
||||||
int lineCount = textLayout.getLineCount();
|
int lineCount = textLayout.getLineCount();
|
||||||
@ -202,14 +220,13 @@ import android.util.Log;
|
|||||||
|
|
||||||
int textLeft;
|
int textLeft;
|
||||||
int textRight;
|
int textRight;
|
||||||
if (cue.position != Cue.UNSET_VALUE) {
|
if (cuePosition != Cue.DIMEN_UNSET) {
|
||||||
if (cue.alignment == Alignment.ALIGN_OPPOSITE) {
|
int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;
|
||||||
textRight = (parentWidth * cue.position) / 100 + parentLeft;
|
textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth
|
||||||
textLeft = Math.max(textRight - textWidth, parentLeft);
|
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2
|
||||||
} else {
|
: anchorPosition;
|
||||||
textLeft = (parentWidth * cue.position) / 100 + parentLeft;
|
textLeft = Math.max(textLeft, parentLeft);
|
||||||
textRight = Math.min(textLeft + textWidth, parentRight);
|
textRight = Math.min(textLeft + textWidth, parentRight);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
textLeft = (parentWidth - textWidth) / 2;
|
textLeft = (parentWidth - textWidth) / 2;
|
||||||
textRight = textLeft + textWidth;
|
textRight = textLeft + textWidth;
|
||||||
@ -217,12 +234,29 @@ import android.util.Log;
|
|||||||
|
|
||||||
int textTop;
|
int textTop;
|
||||||
int textBottom;
|
int textBottom;
|
||||||
if (cue.line != Cue.UNSET_VALUE) {
|
if (cueLine != Cue.DIMEN_UNSET) {
|
||||||
textTop = (parentHeight * cue.line) / 100 + parentTop;
|
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;
|
textBottom = textTop + textHeight;
|
||||||
if (textBottom > parentBottom) {
|
if (textBottom > parentBottom) {
|
||||||
textTop = parentBottom - textHeight;
|
textTop = parentBottom - textHeight;
|
||||||
textBottom = parentBottom;
|
textBottom = parentBottom;
|
||||||
|
} else if (textTop < parentTop) {
|
||||||
|
textTop = parentTop;
|
||||||
|
textBottom = parentTop + textHeight;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);
|
textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);
|
||||||
@ -232,7 +266,7 @@ import android.util.Log;
|
|||||||
textWidth = textRight - textLeft;
|
textWidth = textRight - textLeft;
|
||||||
|
|
||||||
// Update the derived drawing variables.
|
// 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);
|
spacingAdd, true);
|
||||||
this.textLeft = textLeft;
|
this.textLeft = textLeft;
|
||||||
this.textTop = textTop;
|
this.textTop = textTop;
|
||||||
|
@ -38,7 +38,7 @@ public final class SubtitleLayout extends View {
|
|||||||
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
|
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.
|
* fraction of the viewport height.
|
||||||
*
|
*
|
||||||
* @see #setBottomPaddingFraction(float)
|
* @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
|
* as a fraction of the view's remaining height after its top and bottom padding have been
|
||||||
* subtracted.
|
* subtracted.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -28,16 +28,17 @@ import android.text.Layout.Alignment;
|
|||||||
public final long endTime;
|
public final long endTime;
|
||||||
|
|
||||||
public WebvttCue(CharSequence text) {
|
public WebvttCue(CharSequence text) {
|
||||||
this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text);
|
this(0, 0, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebvttCue(long startTime, long endTime, CharSequence 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,
|
public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
|
||||||
Alignment alignment, int size) {
|
float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) {
|
||||||
super(text, line, position, alignment, size);
|
super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
|
||||||
this.startTime = startTime;
|
this.startTime = startTime;
|
||||||
this.endTime = endTime;
|
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.
|
* @return True if this cue should be placed in the default position; false otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean isNormalCue() {
|
public boolean isNormalCue() {
|
||||||
return (line == UNSET_VALUE && position == UNSET_VALUE);
|
return (line == DIMEN_UNSET && position == DIMEN_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@ import com.google.android.exoplayer.util.MimeTypes;
|
|||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Layout.Alignment;
|
import android.text.Layout.Alignment;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -43,34 +42,15 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
|
|
||||||
private static final String TAG = "WebvttParser";
|
private static final String TAG = "WebvttParser";
|
||||||
|
|
||||||
private static final String WEBVTT_FILE_HEADER_STRING = "^\uFEFF?WEBVTT((\u0020|\u0009).*)?$";
|
private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$");
|
||||||
private static final Pattern WEBVTT_FILE_HEADER =
|
private static final Pattern COMMENT_BLOCK = Pattern.compile("^NOTE((\u0020|\u0009).*)?$");
|
||||||
Pattern.compile(WEBVTT_FILE_HEADER_STRING);
|
private static final Pattern METADATA_HEADER = Pattern.compile("\\S*[:=]\\S*");
|
||||||
|
private static final Pattern CUE_IDENTIFIER = Pattern.compile("^(?!.*(-->)).*$");
|
||||||
private static final String WEBVTT_COMMENT_BLOCK_STRING = "^NOTE((\u0020|\u0009).*)?$";
|
private static final Pattern TIMESTAMP = Pattern.compile("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}");
|
||||||
private static final Pattern WEBVTT_COMMENT_BLOCK =
|
private static final Pattern CUE_SETTING = Pattern.compile("\\S*:\\S*");
|
||||||
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 final PositionHolder positionHolder;
|
||||||
private final StringBuilder textBuilder;
|
private final StringBuilder textBuilder;
|
||||||
|
|
||||||
private final boolean strictParsing;
|
private final boolean strictParsing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,6 +68,7 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
*/
|
*/
|
||||||
public WebvttParser(boolean strictParsing) {
|
public WebvttParser(boolean strictParsing) {
|
||||||
this.strictParsing = strictParsing;
|
this.strictParsing = strictParsing;
|
||||||
|
positionHolder = new PositionHolder();
|
||||||
textBuilder = new StringBuilder();
|
textBuilder = new StringBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +81,7 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
|
|
||||||
// file should start with "WEBVTT"
|
// file should start with "WEBVTT"
|
||||||
line = webvttData.readLine();
|
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);
|
throw new ParserException("Expected WEBVTT. Got " + line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +97,7 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (strictParsing) {
|
if (strictParsing) {
|
||||||
Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line);
|
Matcher matcher = METADATA_HEADER.matcher(line);
|
||||||
if (!matcher.find()) {
|
if (!matcher.find()) {
|
||||||
throw new ParserException("Unexpected line: " + line);
|
throw new ParserException("Unexpected line: " + line);
|
||||||
}
|
}
|
||||||
@ -126,38 +107,45 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
// process the cues and text
|
// process the cues and text
|
||||||
while ((line = webvttData.readLine()) != null) {
|
while ((line = webvttData.readLine()) != null) {
|
||||||
// parse webvtt comment block in case it is present
|
// parse webvtt comment block in case it is present
|
||||||
Matcher matcher = WEBVTT_COMMENT_BLOCK.matcher(line);
|
Matcher matcher = COMMENT_BLOCK.matcher(line);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
// read lines until finding an empty one (webvtt line terminator: CRLF, or LF or CR)
|
// read lines until finding an empty one (webvtt line terminator: CRLF, or LF or CR)
|
||||||
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
|
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
|
||||||
// just ignoring comment text
|
// ignore comment text
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the cue identifier (if present) {
|
// parse the cue identifier (if present) {
|
||||||
matcher = WEBVTT_CUE_IDENTIFIER.matcher(line);
|
matcher = CUE_IDENTIFIER.matcher(line);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
// ignore the identifier (we currently don't use it) and read the next line
|
// ignore the identifier (we currently don't use it) and read the next line
|
||||||
line = webvttData.readLine();
|
line = webvttData.readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// end of file
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long startTime = Cue.UNSET_VALUE;
|
long cueStartTime;
|
||||||
long endTime = Cue.UNSET_VALUE;
|
long cueEndTime;
|
||||||
CharSequence text = null;
|
CharSequence cueText;
|
||||||
int lineNum = Cue.UNSET_VALUE;
|
Alignment cueTextAlignment = null;
|
||||||
int position = Cue.UNSET_VALUE;
|
float cueLine = Cue.DIMEN_UNSET;
|
||||||
Alignment alignment = null;
|
int cueLineType = Cue.TYPE_UNSET;
|
||||||
int size = Cue.UNSET_VALUE;
|
int cueLineAnchor = Cue.TYPE_UNSET;
|
||||||
|
float cuePosition = Cue.DIMEN_UNSET;
|
||||||
|
int cuePositionAnchor = Cue.TYPE_UNSET;
|
||||||
|
float cueWidth = Cue.DIMEN_UNSET;
|
||||||
|
|
||||||
// parse the cue timestamps
|
// parse the cue timestamps
|
||||||
matcher = WEBVTT_TIMESTAMP.matcher(line);
|
matcher = TIMESTAMP.matcher(line);
|
||||||
|
|
||||||
// parse start timestamp
|
// parse start timestamp
|
||||||
if (!matcher.find()) {
|
if (!matcher.find()) {
|
||||||
throw new ParserException("Expected cue start time: " + line);
|
throw new ParserException("Expected cue start time: " + line);
|
||||||
} else {
|
} else {
|
||||||
startTime = parseTimestampUs(matcher.group());
|
cueStartTime = parseTimestampUs(matcher.group());
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse end timestamp
|
// parse end timestamp
|
||||||
@ -166,12 +154,12 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
throw new ParserException("Expected cue end time: " + line);
|
throw new ParserException("Expected cue end time: " + line);
|
||||||
} else {
|
} else {
|
||||||
endTimeString = matcher.group();
|
endTimeString = matcher.group();
|
||||||
endTime = parseTimestampUs(endTimeString);
|
cueEndTime = parseTimestampUs(endTimeString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the (optional) cue setting list
|
// parse the (optional) cue setting list
|
||||||
line = line.substring(line.indexOf(endTimeString) + endTimeString.length());
|
line = line.substring(line.indexOf(endTimeString) + endTimeString.length());
|
||||||
matcher = WEBVTT_CUE_SETTING.matcher(line);
|
matcher = CUE_SETTING.matcher(line);
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
String match = matcher.group();
|
String match = matcher.group();
|
||||||
String[] parts = match.split(":", 2);
|
String[] parts = match.split(":", 2);
|
||||||
@ -180,52 +168,44 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if ("line".equals(name)) {
|
if ("line".equals(name)) {
|
||||||
Pair<String, Alignment> lineMetadata = parseLinePositionAttributes(value);
|
parseLineAttribute(value, positionHolder);
|
||||||
value = lineMetadata.first;
|
cueLine = positionHolder.position;
|
||||||
if (value.endsWith("%")) {
|
cueLineType = positionHolder.lineType;
|
||||||
lineNum = parseIntPercentage(value);
|
cueLineAnchor = positionHolder.positionAnchor;
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ("align".equals(name)) {
|
} else if ("align".equals(name)) {
|
||||||
// TODO: handle for RTL languages
|
cueTextAlignment = parseTextAlignment(value);
|
||||||
alignment = parseAlignment(value);
|
|
||||||
} else if ("position".equals(name)) {
|
} else if ("position".equals(name)) {
|
||||||
Pair<String, Alignment> lineMetadata = parseLinePositionAttributes(value);
|
parsePositionAttribute(value, positionHolder);
|
||||||
value = lineMetadata.first;
|
cuePosition = positionHolder.position;
|
||||||
position = parseIntPercentage(value);
|
cuePositionAnchor = positionHolder.positionAnchor;
|
||||||
} else if ("size".equals(name)) {
|
} else if ("size".equals(name)) {
|
||||||
size = parseIntPercentage(value);
|
cueWidth = parsePercentage(value);
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Unknown cue setting " + name + ":" + value);
|
Log.w(TAG, "Unknown cue setting " + name + ":" + value);
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} 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
|
// parse text
|
||||||
textBuilder.setLength(0);
|
textBuilder.setLength(0);
|
||||||
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
|
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
|
||||||
if (textBuilder.length() > 0) {
|
if (textBuilder.length() > 0) {
|
||||||
textBuilder.append("<br>");
|
textBuilder.append("<br>");
|
||||||
}
|
}
|
||||||
textBuilder.append(line.trim());
|
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);
|
subtitles.add(cue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,67 +217,116 @@ public final class WebvttParser implements SubtitleParser {
|
|||||||
return MimeTypes.TEXT_VTT.equals(mimeType);
|
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 {
|
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;
|
long value = 0;
|
||||||
for (String group : parts[0].split(":")) {
|
String[] parts = s.split("\\.", 2);
|
||||||
value = value * 60 + Long.parseLong(group);
|
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;
|
return (value * 1000 + Long.parseLong(parts[1])) * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pair<String, Alignment> parseLinePositionAttributes(String s) {
|
private static void parseLineAttribute(String s, PositionHolder out)
|
||||||
String value;
|
throws NumberFormatException {
|
||||||
Alignment alignment = null;
|
int lineAnchor;
|
||||||
|
int commaPosition = s.indexOf(",");
|
||||||
int commaPos;
|
if (commaPosition != -1) {
|
||||||
if ((commaPos = s.indexOf(",")) > 0 && commaPos < s.length() - 1) {
|
lineAnchor = parsePositionAnchor(s.substring(commaPosition + 1));
|
||||||
alignment = parseAlignment(s.substring(commaPos + 1));
|
s = s.substring(0, commaPosition);
|
||||||
value = s.substring(0, commaPos);
|
|
||||||
} else {
|
} else {
|
||||||
value = s;
|
lineAnchor = Cue.TYPE_UNSET;
|
||||||
}
|
}
|
||||||
|
float line;
|
||||||
return new Pair<String, Alignment>(value, alignment);
|
int lineType;
|
||||||
}
|
if (s.endsWith("%")) {
|
||||||
|
line = parsePercentage(s);
|
||||||
private static Alignment parseAlignment(String s) {
|
lineType = Cue.LINE_TYPE_FRACTION;
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Invalid align value: " + s);
|
line = Integer.parseInt(s);
|
||||||
|
lineType = Cue.LINE_TYPE_NUMBER;
|
||||||
}
|
}
|
||||||
return alignment;
|
out.position = line;
|
||||||
|
out.positionAnchor = lineAnchor;
|
||||||
|
out.lineType = lineType;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
positionAnchor = Cue.TYPE_UNSET;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user