@Override public Mp4WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException { // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: // first 4 bytes size and then 4 bytes type. sampleData.reset(bytes, offset + length); sampleData.setPosition(offset); List<Cue> resultingCueList = new ArrayList<>(); while (sampleData.bytesLeft() > 0) { if (sampleData.bytesLeft() < BOX_HEADER_SIZE) { throw new ParserException("Incomplete Mp4Webvtt Top Level box header found."); } int boxSize = sampleData.readInt(); int boxType = sampleData.readInt(); if (boxType == TYPE_vttc) { resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE)); } else { // Peers of the VTTCueBox are still not supported and are skipped. sampleData.skipBytes(boxSize - BOX_HEADER_SIZE); } } return new Mp4WebvttSubtitle(resultingCueList); }
private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder, int remainingCueBoxBytes) throws ParserException { builder.reset(); while (remainingCueBoxBytes > 0) { if (remainingCueBoxBytes < BOX_HEADER_SIZE) { throw new ParserException("Incomplete vtt cue box header found."); } int boxSize = sampleData.readInt(); int boxType = sampleData.readInt(); remainingCueBoxBytes -= BOX_HEADER_SIZE; int payloadLength = boxSize - BOX_HEADER_SIZE; String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength); sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { WebvttCueParser.parseCueSettingsList(boxPayload, builder); } else if (boxType == TYPE_payl) { WebvttCueParser.parseCueText(boxPayload.trim(), builder); } else { // Other VTTCueBox children are still not supported and are ignored. } } return builder.build(); }
private Builder derivePositionAnchorFromAlignment() { if (textAlignment == null) { positionAnchor = Cue.TYPE_UNSET; } else { switch (textAlignment) { case ALIGN_NORMAL: positionAnchor = Cue.ANCHOR_TYPE_START; break; case ALIGN_CENTER: positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; break; case ALIGN_OPPOSITE: positionAnchor = Cue.ANCHOR_TYPE_END; break; default: Log.w(TAG, "Unrecognized alignment: " + textAlignment); positionAnchor = Cue.ANCHOR_TYPE_START; break; } } return this; }
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); }
private void assertSpans(TtmlSubtitle subtitle, int second, String text, String font, int fontStyle, int backgroundColor, int color, boolean isUnderline, boolean isLinethrough, Layout.Alignment alignment) { long timeUs = second * 1000000; List<Cue> cues = subtitle.getCues(timeUs); assertEquals(1, cues.size()); assertEquals(text, String.valueOf(cues.get(0).text)); assertEquals("single cue expected for timeUs: " + timeUs, 1, cues.size()); SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; assertFont(spannable, font); assertStyle(spannable, fontStyle); assertUnderline(spannable, isUnderline); assertStrikethrough(spannable, isLinethrough); assertUnderline(spannable, isUnderline); assertBackground(spannable, backgroundColor); assertForeground(spannable, color); assertAlignment(spannable, alignment); }
@Override public Subtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs) throws IOException { DataInputStream dataInputStream = new DataInputStream(inputStream); String cueText = dataInputStream.readUTF(); return new Tx3gSubtitle(startTimeUs, new Cue(cueText)); }
private void invokeRendererInternal(String cueText) { if (cueText == null) { textRenderer.onCues(Collections.<Cue>emptyList()); } else { textRenderer.onCues(Collections.singletonList(new Cue(cueText))); } }
@Override public List<Cue> getCues(long timeUs) { CharSequence cueText = root.getText(timeUs - startTimeUs); if (cueText == null) { return Collections.<Cue>emptyList(); } else { Cue cue = new Cue(cueText); return Collections.singletonList(cue); } }
@Override public List<Cue> getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); if (index == -1 || index % 2 == 1) { // timeUs is earlier than the start of the first cue, or corresponds to a gap between cues. return Collections.<Cue>emptyList(); } else { return Collections.singletonList(cues[index / 2]); } }
public void selectTrack(int type, int index) { if (selectedTracks[type] == index) { return; } selectedTracks[type] = index; pushTrackSelection(type, true); if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) { captionListener.onCues(Collections.<Cue>emptyList()); } }
private static void parseLineAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { int commaPosition = s.indexOf(','); if (commaPosition != -1) { builder.setLineAnchor(parsePositionAnchor(s.substring(commaPosition + 1))); s = s.substring(0, commaPosition); } else { builder.setLineAnchor(Cue.TYPE_UNSET); } if (s.endsWith("%")) { builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); } else { builder.setLine(Integer.parseInt(s)).setLineType(Cue.LINE_TYPE_NUMBER); } }
private static void parsePositionAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { int commaPosition = s.indexOf(','); if (commaPosition != -1) { builder.setPositionAnchor(parsePositionAnchor(s.substring(commaPosition + 1))); s = s.substring(0, commaPosition); } else { builder.setPositionAnchor(Cue.TYPE_UNSET); } builder.setPosition(WebvttParserUtil.parsePercentage(s)); }
private static int parsePositionAnchor(String s) { switch (s) { case "start": return Cue.ANCHOR_TYPE_START; case "center": 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; } }
public void reset() { startTime = 0; endTime = 0; text = null; textAlignment = null; line = Cue.DIMEN_UNSET; lineType = Cue.TYPE_UNSET; lineAnchor = Cue.TYPE_UNSET; position = Cue.DIMEN_UNSET; positionAnchor = Cue.TYPE_UNSET; width = Cue.DIMEN_UNSET; }
public WebvttCue build() { if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) { derivePositionAnchorFromAlignment(); } return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); }
public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> regionMap) { TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>(); traverseForText(timeUs, false, regionId, regionOutputs); traverseForStyle(globalStyles, regionOutputs); List<Cue> cues = new ArrayList<>(); for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, Cue.TYPE_UNSET, Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); } return cues; }
@Override public List<Cue> getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); if (index == -1 || cues[index] == null) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.<Cue>emptyList(); } else { return Collections.singletonList(cues[index]); } }
public void testParseWithPositioning() throws IOException { WebvttParser parser = new WebvttParser(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE); WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length); // test event count assertEquals(12, subtitle.getEventTimeCount()); // 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); assertCue(subtitle, 10, 10000000, 11000000, "This is the sixth subtitle.", Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, 0.35f); }
/** * Asserts that the Subtitle's cues (which are all part of the event at t=0) are equal to the * expected Cues. * * @param sub The parsed {@link Subtitle} to check. * @param expectedCues Expected {@link Cue}s in order of appearance. */ private static void assertMp4WebvttSubtitleEquals(Subtitle sub, Cue... expectedCues) { assertEquals(1, sub.getEventTimeCount()); assertEquals(0, sub.getEventTime(0)); List<Cue> subtitleCues = sub.getCues(0); assertEquals(expectedCues.length, subtitleCues.size()); for (int i = 0; i < subtitleCues.size(); i++) { List<String> differences = getCueDifferences(subtitleCues.get(i), expectedCues[i]); assertTrue("Cues at position " + i + " are not equal. Different fields are " + Arrays.toString(differences.toArray()), differences.isEmpty()); } }
/** * Checks whether two non null cues are equal. Check fails if any of the Cues are null. * * @return a set that contains the names of the different fields. */ private static List<String> getCueDifferences(Cue aCue, Cue anotherCue) { assertNotNull(aCue); assertNotNull(anotherCue); List<String> differences = new ArrayList<>(); if (aCue.line != anotherCue.line) { differences.add("line: " + aCue.line + " | " + anotherCue.line); } if (aCue.lineAnchor != anotherCue.lineAnchor) { differences.add("lineAnchor: " + aCue.lineAnchor + " | " + anotherCue.lineAnchor); } if (aCue.lineType != anotherCue.lineType) { differences.add("lineType: " + aCue.lineType + " | " + anotherCue.lineType); } if (aCue.position != anotherCue.position) { differences.add("position: " + aCue.position + " | " + anotherCue.position); } if (aCue.positionAnchor != anotherCue.positionAnchor) { differences.add("positionAnchor: " + aCue.positionAnchor + " | " + anotherCue.positionAnchor); } if (aCue.size != anotherCue.size) { differences.add("size: " + aCue.size + " | " + anotherCue.size); } if (!Util.areEqual(aCue.text.toString(), anotherCue.text.toString())) { differences.add("text: '" + aCue.text + "' | '" + anotherCue.text + '\''); } if (!Util.areEqual(aCue.textAlignment, anotherCue.textAlignment)) { differences.add("textAlignment: " + aCue.textAlignment + " | " + anotherCue.textAlignment); } return differences; }
public void testFontSizeSpans() throws IOException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE); assertEquals(10, subtitle.getEventTimeCount()); List<Cue> cues = subtitle.getCues(10 * 1000000); assertEquals(1, cues.size()); SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("text 1", String.valueOf(spannable)); assertAbsoluteFontSize(spannable, 32); cues = subtitle.getCues(20 * 1000000); assertEquals(1, cues.size()); spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("text 2", String.valueOf(cues.get(0).text)); assertRelativeFontSize(spannable, 2.2f); cues = subtitle.getCues(30 * 1000000); assertEquals(1, cues.size()); spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("text 3", String.valueOf(cues.get(0).text)); assertRelativeFontSize(spannable, 1.5f); cues = subtitle.getCues(40 * 1000000); assertEquals(1, cues.size()); spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("two values", String.valueOf(cues.get(0).text)); assertAbsoluteFontSize(spannable, 16); cues = subtitle.getCues(50 * 1000000); assertEquals(1, cues.size()); spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("leading dot", String.valueOf(cues.get(0).text)); assertRelativeFontSize(spannable, 0.5f); }
public void testFontSizeWithMissingUnitIsIgnored() throws IOException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_MISSING_UNIT_TTML_FILE); assertEquals(2, subtitle.getEventTimeCount()); List<Cue> cues = subtitle.getCues(10 * 1000000); assertEquals(1, cues.size()); SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("no unit", String.valueOf(spannable)); assertEquals(0, spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class).length); assertEquals(0, spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class).length); }
public void testFontSizeWithInvalidValueIsIgnored() throws IOException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_INVALID_TTML_FILE); assertEquals(6, subtitle.getEventTimeCount()); List<Cue> cues = subtitle.getCues(10 * 1000000); assertEquals(1, cues.size()); SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("invalid", String.valueOf(spannable)); assertEquals(0, spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class).length); assertEquals(0, spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class).length); cues = subtitle.getCues(20 * 1000000); assertEquals(1, cues.size()); spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("invalid", String.valueOf(spannable)); assertEquals(0, spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class).length); assertEquals(0, spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class).length); cues = subtitle.getCues(30 * 1000000); assertEquals(1, cues.size()); spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("invalid dot", String.valueOf(spannable)); assertEquals(0, spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class).length); assertEquals(0, spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class).length); }
public void testFontSizeWithEmptyValueIsIgnored() throws IOException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_EMPTY_TTML_FILE); assertEquals(2, subtitle.getEventTimeCount()); List<Cue> cues = subtitle.getCues(10 * 1000000); assertEquals(1, cues.size()); SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; assertEquals("empty", String.valueOf(spannable)); assertEquals(0, spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class).length); assertEquals(0, spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class).length); }
/** * Sets the selected track. */ public void setSelectedTrack(int type, int index) { player.setSelectedTrack(type, index); if (type == TYPE_TEXT && index < 0 && captionListener != null) { captionListener.onCues(Collections.<Cue>emptyList()); } }
/** TextRenderer */ @Override public void onCues(List<Cue> cues) { if (captionListener != null && !player.isDisabledTrack(Player.TYPE_TEXT)) { captionListener.onCues(cues); } }
public void selectTrack(int type, int index) { if (selectedTracks[type] == index) { return; } selectedTracks[type] = index; pushTrackSelection(true); if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) { captionListener.onCues(Collections.<Cue>emptyList()); } }
/** * Retrieve the subtitle cues that should be displayed at a given time. * * @param timeUs The time in microseconds. * @return A list of cues that should be displayed, possibly empty. */ @Override public List<Cue> getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); if (index == -1 || cues[index] == null) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.<Cue>emptyList(); } else { return Collections.singletonList(cues[index]); } }
public Cue getCue(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); if (index == -1 || cues[index] == null) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return null; } else { return cues[index]; } }
@Override public void onCues(List<Cue> cues) { StringBuilder sb = new StringBuilder(); for (Cue cue : cues){ sb.append(cue.text); } LOGD(TAG, "subTitle = " + sb.toString()); if (mSubtView != null) { mSubtView.setCues(cues); } }
/** * When subtitles arrive, display them in the text view. * @param cues The subtitles that must be displayed. */ @Override public void onCues(List<Cue> cues) { StringBuilder sb = new StringBuilder(); for (Cue cue : cues){ sb.append(cue.text); } this.subtitles.setText(sb.toString()); }
public void setSelectedTrack(int type, int index) { player.setSelectedTrack(type, index); if (type == TYPE_TEXT && index < 0 && captionListener != null) { captionListener.onCues(Collections.<Cue>emptyList()); } }