Update SubtitleExtractor to use SubtitleParser directly

`SubtitleExtractor` used to rely on a `SubtitleDecoder`. However, now all SubtitleDecoders that are used for side-loaded subtitles have been migrated to a `SubtitleParser` interface. We can therefore refactor the extractor.

The `SubtitleExtractor` is only used for side-loaded subtitles which means we do not require the migration of the CEA-608/708 `SubtitleDecoders`.

PiperOrigin-RevId: 552471710
This commit is contained in:
jbibik 2023-07-31 15:17:05 +01:00 committed by Rohit Singh
parent 9e975b25d1
commit b8e6f27caf
3 changed files with 57 additions and 99 deletions

View File

@ -38,7 +38,6 @@ import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.ads.AdsLoader; import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource; import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.exoplayer.text.SubtitleDecoderFactory;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.extractor.DefaultExtractorsFactory;
@ -49,6 +48,7 @@ import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleExtractor; import androidx.media3.extractor.text.SubtitleExtractor;
import com.google.common.base.Supplier; import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -484,12 +484,12 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
.setLabel(subtitleConfigurations.get(i).label) .setLabel(subtitleConfigurations.get(i).label)
.setId(subtitleConfigurations.get(i).id) .setId(subtitleConfigurations.get(i).id)
.build(); .build();
DefaultSubtitleParserFactory subtitleParserFactory = new DefaultSubtitleParserFactory();
ExtractorsFactory extractorsFactory = ExtractorsFactory extractorsFactory =
() -> () ->
new Extractor[] { new Extractor[] {
SubtitleDecoderFactory.DEFAULT.supportsFormat(format) subtitleParserFactory.supportsFormat(format)
? new SubtitleExtractor( ? new SubtitleExtractor(subtitleParserFactory.create(format), format)
SubtitleDecoderFactory.DEFAULT.createDecoder(format), format)
: new UnknownSubtitlesExtractor(format) : new UnknownSubtitlesExtractor(format)
}; };
ProgressiveMediaSource.Factory progressiveMediaSourceFactory = ProgressiveMediaSource.Factory progressiveMediaSourceFactory =

View File

@ -15,17 +15,16 @@
*/ */
package androidx.media3.extractor.text; package androidx.media3.extractor.text;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
@ -37,12 +36,12 @@ import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -82,28 +81,31 @@ public class SubtitleExtractor implements Extractor {
private static final int DEFAULT_BUFFER_SIZE = 1024; private static final int DEFAULT_BUFFER_SIZE = 1024;
private final SubtitleDecoder subtitleDecoder; private final SubtitleParser subtitleParser;
private final CueEncoder cueEncoder; private final CueEncoder cueEncoder;
private final ParsableByteArray subtitleData;
private final Format format; private final Format format;
private final List<Long> timestamps; private final List<Long> timestamps;
private final List<ParsableByteArray> samples; private final List<byte[]> samples;
private final ParsableByteArray scratchSampleArray;
private @MonotonicNonNull ExtractorOutput extractorOutput; private byte[] subtitleData;
private @MonotonicNonNull TrackOutput trackOutput; private @MonotonicNonNull TrackOutput trackOutput;
private int bytesRead; private int bytesRead;
private @State int state; private @State int state;
private long seekTimeUs; private long seekTimeUs;
/** /**
* @param subtitleDecoder The decoder used for decoding the subtitle data. The extractor will * Creates an instance.
* release the decoder in {@link SubtitleExtractor#release()}. *
* @param format Format that describes subtitle data. * @param subtitleParser The parser used for parsing the subtitle data. The extractor will reset
* the parser in {@link SubtitleExtractor#release()}.
* @param format {@link Format} that describes subtitle data.
*/ */
public SubtitleExtractor(SubtitleDecoder subtitleDecoder, Format format) { public SubtitleExtractor(SubtitleParser subtitleParser, Format format) {
this.subtitleDecoder = subtitleDecoder; this.subtitleParser = subtitleParser;
cueEncoder = new CueEncoder(); cueEncoder = new CueEncoder();
subtitleData = new ParsableByteArray(); subtitleData = Util.EMPTY_BYTE_ARRAY;
scratchSampleArray = new ParsableByteArray();
this.format = this.format =
format format
.buildUpon() .buildUpon()
@ -127,10 +129,9 @@ public class SubtitleExtractor implements Extractor {
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
checkState(state == STATE_CREATED); checkState(state == STATE_CREATED);
extractorOutput = output; trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_TEXT);
trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_TEXT); output.endTracks();
extractorOutput.endTracks(); output.seekMap(
extractorOutput.seekMap(
new IndexSeekMap( new IndexSeekMap(
/* positions= */ new long[] {0}, /* positions= */ new long[] {0},
/* timesUs= */ new long[] {0}, /* timesUs= */ new long[] {0},
@ -143,17 +144,20 @@ public class SubtitleExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
checkState(state != STATE_CREATED && state != STATE_RELEASED); checkState(state != STATE_CREATED && state != STATE_RELEASED);
if (state == STATE_INITIALIZED) { if (state == STATE_INITIALIZED) {
subtitleData.reset( int length =
input.getLength() != C.LENGTH_UNSET input.getLength() != C.LENGTH_UNSET
? Ints.checkedCast(input.getLength()) ? Ints.checkedCast(input.getLength())
: DEFAULT_BUFFER_SIZE); : DEFAULT_BUFFER_SIZE;
if (length > subtitleData.length) {
subtitleData = new byte[length];
}
bytesRead = 0; bytesRead = 0;
state = STATE_EXTRACTING; state = STATE_EXTRACTING;
} }
if (state == STATE_EXTRACTING) { if (state == STATE_EXTRACTING) {
boolean inputFinished = readFromInput(input); boolean inputFinished = readFromInput(input);
if (inputFinished) { if (inputFinished) {
decode(); parse();
writeToOutput(); writeToOutput();
state = STATE_FINISHED; state = STATE_FINISHED;
} }
@ -183,13 +187,13 @@ public class SubtitleExtractor implements Extractor {
} }
} }
/** Releases the extractor's resources, including the {@link SubtitleDecoder}. */ /** Releases the extractor's resources, including resetting the {@link SubtitleParser}. */
@Override @Override
public void release() { public void release() {
if (state == STATE_RELEASED) { if (state == STATE_RELEASED) {
return; return;
} }
subtitleDecoder.release(); subtitleParser.reset();
state = STATE_RELEASED; state = STATE_RELEASED;
} }
@ -204,11 +208,13 @@ public class SubtitleExtractor implements Extractor {
/** Returns whether reading has been finished. */ /** Returns whether reading has been finished. */
private boolean readFromInput(ExtractorInput input) throws IOException { private boolean readFromInput(ExtractorInput input) throws IOException {
if (subtitleData.capacity() == bytesRead) { if (subtitleData.length == bytesRead) {
subtitleData.ensureCapacity(bytesRead + DEFAULT_BUFFER_SIZE); subtitleData =
Arrays.copyOf(subtitleData, /* newLength= */ subtitleData.length + DEFAULT_BUFFER_SIZE);
} }
int readResult = int readResult =
input.read(subtitleData.getData(), bytesRead, subtitleData.capacity() - bytesRead); input.read(
subtitleData, /* offset= */ bytesRead, /* length= */ subtitleData.length - bytesRead);
if (readResult != C.RESULT_END_OF_INPUT) { if (readResult != C.RESULT_END_OF_INPUT) {
bytesRead += readResult; bytesRead += readResult;
} }
@ -217,45 +223,19 @@ public class SubtitleExtractor implements Extractor {
|| readResult == C.RESULT_END_OF_INPUT; || readResult == C.RESULT_END_OF_INPUT;
} }
/** Decodes the subtitle data and stores the samples in the memory of the extractor. */ /** Parses the subtitle data and stores the samples in the memory of the extractor. */
private void decode() throws IOException { private void parse() throws IOException {
try { try {
@Nullable SubtitleInputBuffer inputBuffer = subtitleDecoder.dequeueInputBuffer(); List<CuesWithTiming> cuesWithTimingList = checkNotNull(subtitleParser.parse(subtitleData));
while (inputBuffer == null) { for (int i = 0; i < cuesWithTimingList.size(); i++) {
Thread.sleep(5); CuesWithTiming cuesWithTiming = cuesWithTimingList.get(i);
inputBuffer = subtitleDecoder.dequeueInputBuffer(); long eventTimeUs = cuesWithTiming.startTimeUs;
} byte[] cuesSample = cueEncoder.encode(cuesWithTiming.cues, cuesWithTiming.durationUs);
inputBuffer.ensureSpaceForWrite(bytesRead);
inputBuffer.data.put(subtitleData.getData(), /* offset= */ 0, bytesRead);
inputBuffer.data.limit(bytesRead);
subtitleDecoder.queueInputBuffer(inputBuffer);
@Nullable SubtitleOutputBuffer outputBuffer = subtitleDecoder.dequeueOutputBuffer();
while (outputBuffer == null) {
Thread.sleep(5);
outputBuffer = subtitleDecoder.dequeueOutputBuffer();
}
for (int i = 0; i < outputBuffer.getEventTimeCount(); i++) {
long eventTimeUs = outputBuffer.getEventTime(i);
List<Cue> cues = outputBuffer.getCues(eventTimeUs);
if (cues.isEmpty() && i != 0) {
// An empty cue list has already been implicitly encoded in the duration of the previous
// sample (unless there was no previous sample).
continue;
}
long durationUs =
i < outputBuffer.getEventTimeCount() - 1
? outputBuffer.getEventTime(i + 1) - eventTimeUs
: C.TIME_UNSET;
byte[] cuesSample = cueEncoder.encode(cues, durationUs);
timestamps.add(eventTimeUs); timestamps.add(eventTimeUs);
samples.add(new ParsableByteArray(cuesSample)); samples.add(cuesSample);
} }
outputBuffer.release(); } catch (RuntimeException e) {
} catch (InterruptedException e) { throw ParserException.createForMalformedContainer("SubtitleParser failed.", e);
Thread.currentThread().interrupt();
throw new InterruptedIOException();
} catch (SubtitleDecoderException e) {
throw ParserException.createForMalformedContainer("SubtitleDecoder failed.", e);
} }
} }
@ -268,10 +248,10 @@ public class SubtitleExtractor implements Extractor {
: Util.binarySearchFloor( : Util.binarySearchFloor(
timestamps, seekTimeUs, /* inclusive= */ true, /* stayInBounds= */ true); timestamps, seekTimeUs, /* inclusive= */ true, /* stayInBounds= */ true);
for (int i = index; i < samples.size(); i++) { for (int i = index; i < samples.size(); i++) {
ParsableByteArray sample = samples.get(i); byte[] sample = samples.get(i);
sample.setPosition(0); int size = sample.length;
int size = sample.getData().length; scratchSampleArray.reset(sample);
trackOutput.sampleData(sample, size); trackOutput.sampleData(scratchSampleArray, size);
trackOutput.sampleMetadata( trackOutput.sampleMetadata(
/* timeUs= */ timestamps.get(i), /* timeUs= */ timestamps.get(i),
/* flags= */ C.BUFFER_FLAG_KEY_FRAME, /* flags= */ C.BUFFER_FLAG_KEY_FRAME,

View File

@ -21,7 +21,6 @@ import static org.junit.Assert.assertThrows;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.text.DelegatingSubtitleDecoder;
import androidx.media3.extractor.Extractor; import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.webvtt.WebvttParser; import androidx.media3.extractor.text.webvtt.WebvttParser;
import androidx.media3.test.utils.FakeExtractorInput; import androidx.media3.test.utils.FakeExtractorInput;
@ -65,9 +64,7 @@ public class SubtitleExtractorTest {
.build(); .build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(
new DelegatingSubtitleDecoder( new WebvttParser(), new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output); extractor.init(output);
while (extractor.read(input, null) != Extractor.RESULT_END_OF_INPUT) {} while (extractor.read(input, null) != Extractor.RESULT_END_OF_INPUT) {}
@ -109,9 +106,7 @@ public class SubtitleExtractorTest {
.build(); .build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(
new DelegatingSubtitleDecoder( new WebvttParser(), new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output); extractor.init(output);
FakeTrackOutput trackOutput = output.trackOutputs.get(0); FakeTrackOutput trackOutput = output.trackOutputs.get(0);
@ -152,9 +147,7 @@ public class SubtitleExtractorTest {
.build(); .build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(
new DelegatingSubtitleDecoder( new WebvttParser(), new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output); extractor.init(output);
FakeTrackOutput trackOutput = output.trackOutputs.get(0); FakeTrackOutput trackOutput = output.trackOutputs.get(0);
@ -189,10 +182,7 @@ public class SubtitleExtractorTest {
public void read_withoutInit_fails() { public void read_withoutInit_fails() {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
assertThrows(IllegalStateException.class, () -> extractor.read(input, null)); assertThrows(IllegalStateException.class, () -> extractor.read(input, null));
} }
@ -201,10 +191,7 @@ public class SubtitleExtractorTest {
public void read_afterRelease_fails() { public void read_afterRelease_fails() {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
FakeExtractorOutput output = new FakeExtractorOutput(); FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output); extractor.init(output);
@ -216,10 +203,7 @@ public class SubtitleExtractorTest {
@Test @Test
public void seek_withoutInit_fails() { public void seek_withoutInit_fails() {
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
assertThrows(IllegalStateException.class, () -> extractor.seek(0, 0)); assertThrows(IllegalStateException.class, () -> extractor.seek(0, 0));
} }
@ -227,10 +211,7 @@ public class SubtitleExtractorTest {
@Test @Test
public void seek_afterRelease_fails() { public void seek_afterRelease_fails() {
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
FakeExtractorOutput output = new FakeExtractorOutput(); FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output); extractor.init(output);
@ -242,10 +223,7 @@ public class SubtitleExtractorTest {
@Test @Test
public void released_calledTwice() { public void released_calledTwice() {
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
FakeExtractorOutput output = new FakeExtractorOutput(); FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output); extractor.init(output);