Plumb SubtitleParser.Factory into WebvttExtractor

WebvttExtractor will no longer be wrapped in SubtitleTranscodingExtractor, but instead use SubtitleTranscodingExtractorOutput under the hood.

A new constructor will take a boolean parameter to toggle between subtitle parsing during extraction (before the sample queue) or during decoding (after the sample queue).

PiperOrigin-RevId: 597604942
This commit is contained in:
jbibik 2024-01-11 11:10:06 -08:00 committed by Copybara-Service
parent f2be3fd0cb
commit 59afb4fb01
5 changed files with 107 additions and 13 deletions

View File

@ -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) {

View File

@ -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:

View File

@ -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));
}

View File

@ -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;
}

View File

@ -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