diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/BundledHlsMediaChunkExtractor.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/BundledHlsMediaChunkExtractor.java index 662de583d6..9b936b65d1 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/BundledHlsMediaChunkExtractor.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/BundledHlsMediaChunkExtractor.java @@ -29,7 +29,6 @@ import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.mp3.Mp3Extractor; import androidx.media3.extractor.mp4.FragmentedMp4Extractor; import androidx.media3.extractor.text.SubtitleParser; -import androidx.media3.extractor.text.SubtitleTranscodingExtractor; import androidx.media3.extractor.ts.Ac3Extractor; import androidx.media3.extractor.ts.Ac4Extractor; import androidx.media3.extractor.ts.AdtsExtractor; @@ -125,13 +124,19 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract Extractor newExtractorInstance; // LINT.IfChange(extractor_instantiation) if (extractor instanceof WebvttExtractor) { - newExtractorInstance = - new WebvttExtractor(multivariantPlaylistFormat.language, timestampAdjuster); + SubtitleParser.Factory webvttSubtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED; + boolean parseSubtitlesDuringExtraction = false; if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(multivariantPlaylistFormat)) { - newExtractorInstance = - new SubtitleTranscodingExtractor(newExtractorInstance, subtitleParserFactory); + webvttSubtitleParserFactory = subtitleParserFactory; + parseSubtitlesDuringExtraction = true; } + newExtractorInstance = + new WebvttExtractor( + multivariantPlaylistFormat.language, + timestampAdjuster, + webvttSubtitleParserFactory, + parseSubtitlesDuringExtraction); } else if (extractor instanceof AdtsExtractor) { newExtractorInstance = new AdtsExtractor(); } else if (extractor instanceof Ac3Extractor) { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java index 742fdcd543..e5b77f3486 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java @@ -34,7 +34,6 @@ import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.mp3.Mp3Extractor; import androidx.media3.extractor.mp4.FragmentedMp4Extractor; import androidx.media3.extractor.text.SubtitleParser; -import androidx.media3.extractor.text.SubtitleTranscodingExtractor; import androidx.media3.extractor.ts.Ac3Extractor; import androidx.media3.extractor.ts.Ac4Extractor; import androidx.media3.extractor.ts.AdtsExtractor; @@ -188,12 +187,17 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // LINT.IfChange(extractor_instantiation) switch (fileType) { case FileTypes.WEBVTT: + SubtitleParser.Factory webvttSubtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED; + boolean parseSubtitlesDuringExtraction = false; if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(format)) { - return new SubtitleTranscodingExtractor( - new WebvttExtractor(format.language, timestampAdjuster), subtitleParserFactory); - } else { - return new WebvttExtractor(format.language, timestampAdjuster); + webvttSubtitleParserFactory = subtitleParserFactory; + parseSubtitlesDuringExtraction = true; } + return new WebvttExtractor( + format.language, + timestampAdjuster, + webvttSubtitleParserFactory, + parseSubtitlesDuringExtraction); case FileTypes.ADTS: return new AdtsExtractor(); case FileTypes.AC3: diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/WebvttExtractor.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/WebvttExtractor.java index 3654cb190a..dd0b4a9ada 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/WebvttExtractor.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/WebvttExtractor.java @@ -31,6 +31,8 @@ import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.TrackOutput; +import androidx.media3.extractor.text.SubtitleParser; +import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput; import androidx.media3.extractor.text.webvtt.WebvttParserUtil; import java.io.IOException; import java.util.Arrays; @@ -58,17 +60,38 @@ public final class WebvttExtractor implements Extractor { @Nullable private final String language; private final TimestampAdjuster timestampAdjuster; private final ParsableByteArray sampleDataWrapper; + private final SubtitleParser.Factory subtitleParserFactory; + private final boolean parseSubtitlesDuringExtraction; private @MonotonicNonNull ExtractorOutput output; private byte[] sampleData; private int sampleSize; + /** + * @deprecated Use {@link #WebvttExtractor(String, TimestampAdjuster, SubtitleParser.Factory, + * boolean)} instead. + */ + @Deprecated public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) { + this( + language, + timestampAdjuster, + SubtitleParser.Factory.UNSUPPORTED, + /* parseSubtitlesDuringExtraction= */ false); + } + + public WebvttExtractor( + @Nullable String language, + TimestampAdjuster timestampAdjuster, + SubtitleParser.Factory subtitleParserFactory, + boolean parseSubtitlesDuringExtraction) { this.language = language; this.timestampAdjuster = timestampAdjuster; this.sampleDataWrapper = new ParsableByteArray(); sampleData = new byte[1024]; + this.subtitleParserFactory = subtitleParserFactory; + this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction; } // Extractor implementation. @@ -94,7 +117,10 @@ public final class WebvttExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - this.output = output; + this.output = + parseSubtitlesDuringExtraction + ? new SubtitleTranscodingExtractorOutput(output, subtitleParserFactory) + : output; output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/WebvttExtractorTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/WebvttExtractorTest.java index 3413e7e05f..17b2548a9a 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/WebvttExtractorTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/WebvttExtractorTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.util.TimestampAdjuster; import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.text.DefaultSubtitleParserFactory; +import androidx.media3.extractor.text.SubtitleParser; import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.FakeExtractorInput; import androidx.media3.test.utils.FakeExtractorOutput; @@ -72,7 +74,12 @@ public class WebvttExtractorTest { TimestampAdjuster timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); // Prime the TimestampAdjuster with a close-ish timestamp (5s before the first cue). timestampAdjuster.adjustTsTimestamp(384615190); - WebvttExtractor extractor = new WebvttExtractor(/* language= */ null, timestampAdjuster); + WebvttExtractor extractor = + new WebvttExtractor( + /* language= */ null, + timestampAdjuster, + SubtitleParser.Factory.UNSUPPORTED, + /* parseSubtitlesDuringExtraction= */ false); // We can't use ExtractorAsserts because WebvttExtractor doesn't fulfill the whole Extractor // interface (e.g. throws an exception from seek()). FakeExtractorOutput output = @@ -90,10 +97,42 @@ public class WebvttExtractorTest { "extractordumps/webvtt/with_x-timestamp-map_header.dump"); } + @Test + public void read_handlesLargeCueTimestamps_withSubtitleParsingDuringExtraction() + throws Exception { + TimestampAdjuster timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + // Prime the TimestampAdjuster with a close-ish timestamp (5s before the first cue). + timestampAdjuster.adjustTsTimestamp(384615190); + WebvttExtractor extractor = + new WebvttExtractor( + /* language= */ null, + timestampAdjuster, + new DefaultSubtitleParserFactory(), + /* parseSubtitlesDuringExtraction= */ true); + + FakeExtractorOutput output = + TestUtil.extractAllSamplesFromFile( + extractor, + ApplicationProvider.getApplicationContext(), + "media/webvtt/with_x-timestamp-map_header"); + + // There are 2 cues in the file which are fed into 2 different samples during extraction + // This is different to the parsing-during-decoding flow where the whole file becomes a sample + DumpFileAsserts.assertOutput( + ApplicationProvider.getApplicationContext(), + output, + "extractordumps/webvtt/with_x-timestamp-map_header_parsed_during_extraction.dump"); + } + private static boolean sniffData(byte[] data) throws IOException { ExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); try { - return new WebvttExtractor(/* language= */ null, new TimestampAdjuster(0)).sniff(input); + return new WebvttExtractor( + /* language= */ null, + new TimestampAdjuster(0), + SubtitleParser.Factory.UNSUPPORTED, + /* parseSubtitlesDuringExtraction= */ false) + .sniff(input); } catch (EOFException e) { return false; } diff --git a/libraries/test_data/src/test/assets/extractordumps/webvtt/with_x-timestamp-map_header_parsed_during_extraction.dump b/libraries/test_data/src/test/assets/extractordumps/webvtt/with_x-timestamp-map_header_parsed_during_extraction.dump new file mode 100644 index 0000000000..27e71970a3 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/webvtt/with_x-timestamp-map_header_parsed_during_extraction.dump @@ -0,0 +1,20 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 2248 + sample count = 2 + format 0: + sampleMimeType = application/x-media3-cues + codecs = text/vtt + sample 0: + time = 5000155 + flags = 1 + data = length 1124, hash E87709B3 + sample 1: + time = 6754155 + flags = 1 + data = length 1124, hash 5C8A8288 +tracksEnded = true