@Override protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { // 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, length); List<Cue> resultingCueList = new ArrayList<>(); while (sampleData.bytesLeft() > 0) { if (sampleData.bytesLeft() < BOX_HEADER_SIZE) { throw new SubtitleDecoderException("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 SubtitleDecoderException { builder.reset(); while (remainingCueBoxBytes > 0) { if (remainingCueBoxBytes < BOX_HEADER_SIZE) { throw new SubtitleDecoderException("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(null, boxPayload.trim(), builder, Collections.<WebvttCssStyle>emptyList()); } else { // Other VTTCueBox children are still not supported and are ignored. } } return builder.build(); }
private static void parseLineAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); } else { builder.setLineAnchor(Cue.TYPE_UNSET); } if (s.endsWith("%")) { builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); } else { int lineNumber = Integer.parseInt(s); if (lineNumber < 0) { // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as // Cue defines it to be the first row that's not visible. lineNumber--; } builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER); } }
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; }
@SuppressWarnings("PMD.NPathComplexity") // TODO break this method up private void setupBitmapLayout() { int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); int width = Math.round(parentWidth * cueSize); int height = isCueDimensionSet(cueBitmapHeight) ? Math.round(parentHeight * cueBitmapHeight) : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2f)) : anchorX); int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2f)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); }
static TextCues map(List<Cue> cues) { if (cues == null) { return TextCues.of(Collections.<NoPlayerCue>emptyList()); } List<NoPlayerCue> noPlayerCues = new ArrayList<>(cues.size()); for (Cue cue : cues) { NoPlayerCue noPlayerCue = new NoPlayerCue( cue.text, cue.textAlignment, cue.bitmap, cue.line, cue.lineType, cue.lineAnchor, cue.position, cue.positionAnchor, cue.size, cue.bitmapHeight, cue.windowColorSet, cue.windowColor ); noPlayerCues.add(noPlayerCue); } return TextCues.of(noPlayerCues); }
@Override protected Mp4WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { // 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, length); List<Cue> resultingCueList = new ArrayList<>(); while (sampleData.bytesLeft() > 0) { if (sampleData.bytesLeft() < BOX_HEADER_SIZE) { throw new SubtitleDecoderException("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); }
public void testDecodeWithPositioning() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); // 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); }
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 protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) { ArrayList<Cue> cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray data = new ParsableByteArray(bytes, length); if (!haveInitializationData) { parseHeader(data); } parseEventBody(data, cues, cueTimesUs); Cue[] cuesArray = new Cue[cues.size()]; cues.toArray(cuesArray); long[] cueTimesUsArray = cueTimesUs.toArray(); return new SsaSubtitle(cuesArray, cueTimesUsArray); }
public void testDecodeWithPositioning() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); // 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, -11f, 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); }
private void setupBitmapLayout() { int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); int width = Math.round(parentWidth * cueSize); int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight) : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); }
/** * Sets the cues to be displayed by the view. * * @param cues The cues to display. */ public void setCues(List<Cue> cues) { if (this.cues == cues) { return; } this.cues = cues; // Ensure we have sufficient painters. int cueCount = (cues == null) ? 0 : cues.size(); while (painters.size() < cueCount) { painters.add(new SubtitlePainter(getContext())); } // Invalidate to trigger drawing. invalidate(); }
@Override protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableByteArray.reset(bytes, length); String cueTextString = readSubtitleText(parsableByteArray); if (cueTextString.isEmpty()) { return Tx3gSubtitle.EMPTY; } // Attach default styles. SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString); attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(), SPAN_PRIORITY_LOW); attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(), SPAN_PRIORITY_LOW); attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(), SPAN_PRIORITY_LOW); float verticalPlacement = defaultVerticalPlacement; // Find and attach additional styles. while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) { int position = parsableByteArray.getPosition(); int atomSize = parsableByteArray.readInt(); int atomType = parsableByteArray.readInt(); if (atomType == TYPE_STYL) { assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); int styleRecordCount = parsableByteArray.readUnsignedShort(); for (int i = 0; i < styleRecordCount; i++) { applyStyleRecord(parsableByteArray, cueText); } } else if (atomType == TYPE_TBOX && customVerticalPlacement) { assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); int requestedVerticalPlacement = parsableByteArray.readUnsignedShort(); verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight; verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f); } parsableByteArray.setPosition(position + atomSize); } return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); }
private static void parsePositionAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); } 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 WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment, float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float position, @Cue.AnchorType int positionAnchor, float width) { super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); this.startTime = startTime; this.endTime = endTime; }
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); }
private List<Cue> getDisplayCues() { List<Cea708Cue> displayCues = new ArrayList<>(); for (int i = 0; i < NUM_WINDOWS; i++) { if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) { displayCues.add(cueBuilders[i].build()); } } Collections.sort(displayCues); return Collections.<Cue>unmodifiableList(displayCues); }
private List<Cue> getDisplayCues() { List<Cue> displayCues = new ArrayList<>(); for (int i = 0; i < cueBuilders.size(); i++) { Cue cue = cueBuilders.get(i).build(); if (cue != null) { displayCues.add(cue); } } return displayCues; }
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, region.lineType, region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); } return cues; }
public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float width) { this.id = id; this.position = position; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; this.width = width; }
@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.emptyList(); } else { return Collections.singletonList(cues[index]); } }
TextRenderer.Output output() { return new TextRenderer.Output() { @Override public void onCues(List<Cue> cues) { TextCues textCues = ExoPlayerCueMapper.map(cues); playerView.setSubtitleCue(textCues); } }; }
private TextCues givenPlayerHasLoadedSubtitleCues() { final List<Cue> cueList = Arrays.asList(new Cue("first cue"), new Cue("secondCue")); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { TextRendererOutput output = invocation.getArgument(0); output.output().onCues(cueList); return null; } }).when(exoPlayerFacade).setSubtitleRendererOutput(any(TextRendererOutput.class)); return ExoPlayerCueMapper.map(cueList); }
@Override protected Subtitle decode(byte[] bytes, int length) { parsableByteArray.reset(bytes, length); int textLength = parsableByteArray.readUnsignedShort(); if (textLength == 0) { return Tx3gSubtitle.EMPTY; } String cueText = parsableByteArray.readString(textLength); return new Tx3gSubtitle(new Cue(cueText)); }