From 6b3187ccf12a4d8850a58c87c0208ae955111c0b Mon Sep 17 00:00:00 2001 From: Marcel Dopita Date: Sat, 15 Jan 2022 20:37:40 +0100 Subject: [PATCH] Support MKV embedded WebVTT captions --- .../extractor/mkv/MatroskaExtractor.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index e8876ca52c..2cfefe2cef 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -138,6 +138,7 @@ public class MatroskaExtractor implements Extractor { private static final String CODEC_ID_PCM_FLOAT = "A_PCM/FLOAT/IEEE"; private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final String CODEC_ID_ASS = "S_TEXT/ASS"; + private static final String CODEC_ID_VTT = "S_TEXT/WEBVTT"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; @@ -323,6 +324,32 @@ public class MatroskaExtractor implements Extractor { /** The format of an SSA timecode. */ private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d"; + /** + * A template for the prefix that must be added to each VTT sample. + * + *

The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #VTT_PREFIX_END_TIMECODE_OFFSET} is set to a placeholder value, and must be replaced + * with the duration of the subtitle. + * + *

Equivalent to the UTF-8 string: "WEBVTT\n\n00:00:00.000 --> 00:00:00.000\n". + */ + private static final byte[] VTT_PREFIX = + new byte[]{ + 87, 69, 66, 86, 84, 84, 10, 10, 48, 48, 58, 48, 48, 58, 48, 48, 46, 48, 48, 48, 32, 45, + 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 46, 48, 48, 48, 10 + }; + /** The byte offset of the end timecode in {@link #VTT_PREFIX}. */ + private static final int VTT_PREFIX_END_TIMECODE_OFFSET = 25; + /** + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in a VTT timecode (milliseconds). + */ + private static final long VTT_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000; + /** The format of a VTT timecode. */ + private static final String VTT_TIMECODE_FORMAT = "%02d:%02d:%02d.%03d"; + /** The length in bytes of a WAVEFORMATEX structure. */ private static final int WAVE_FORMAT_SIZE = 18; /** Format tag indicating a WAVEFORMATEXTENSIBLE structure. */ @@ -1342,7 +1369,8 @@ public class MatroskaExtractor implements Extractor { track.trueHdSampleRechunker.sampleMetadata( track.output, timeUs, flags, size, offset, track.cryptoData); } else { - if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { + if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId) + || CODEC_ID_VTT.equals(track.codecId)) { if (blockSampleCount > 1) { Log.w(TAG, "Skipping subtitle sample in laced block."); } else if (blockDurationUs == C.TIME_UNSET) { @@ -1415,6 +1443,9 @@ public class MatroskaExtractor implements Extractor { } else if (CODEC_ID_ASS.equals(track.codecId)) { writeSubtitleSampleData(input, SSA_PREFIX, size); return finishWriteSampleData(); + } else if (CODEC_ID_VTT.equals(track.codecId)) { + writeSubtitleSampleData(input, VTT_PREFIX, size); + return finishWriteSampleData(); } TrackOutput output = track.output; @@ -1641,7 +1672,8 @@ public class MatroskaExtractor implements Extractor { *

See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use * the duration as the end timecode. * - * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}. + * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP}, {@link #CODEC_ID_ASS} or + * {@link #CODEC_ID_VTT}. * @param durationUs The duration of the sample, in microseconds. * @param subtitleData The subtitle sample in which to overwrite the end timecode (output * parameter). @@ -1662,6 +1694,12 @@ public class MatroskaExtractor implements Extractor { durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR); endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET; break; + case CODEC_ID_VTT: + endTimecode = + formatSubtitleTimecode( + durationUs, VTT_TIMECODE_FORMAT, VTT_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = VTT_PREFIX_END_TIMECODE_OFFSET; + break; default: throw new IllegalArgumentException(); } @@ -1830,6 +1868,7 @@ public class MatroskaExtractor implements Extractor { case CODEC_ID_PCM_FLOAT: case CODEC_ID_SUBRIP: case CODEC_ID_ASS: + case CODEC_ID_VTT: case CODEC_ID_VOBSUB: case CODEC_ID_PGS: case CODEC_ID_DVBSUB: @@ -2157,6 +2196,9 @@ public class MatroskaExtractor implements Extractor { mimeType = MimeTypes.TEXT_SSA; initializationData = ImmutableList.of(SSA_DIALOGUE_FORMAT, getCodecPrivate(codecId)); break; + case CODEC_ID_VTT: + mimeType = MimeTypes.TEXT_VTT; + break; case CODEC_ID_VOBSUB: mimeType = MimeTypes.APPLICATION_VOBSUB; initializationData = ImmutableList.of(getCodecPrivate(codecId)); @@ -2245,6 +2287,7 @@ public class MatroskaExtractor implements Extractor { .setColorInfo(colorInfo); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType) || MimeTypes.TEXT_SSA.equals(mimeType) + || MimeTypes.TEXT_VTT.equals(mimeType) || MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType) || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {