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

View File

@ -15,17 +15,16 @@
*/
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.checkStateNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -37,12 +36,12 @@ import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.TrackOutput;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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 final SubtitleDecoder subtitleDecoder;
private final SubtitleParser subtitleParser;
private final CueEncoder cueEncoder;
private final ParsableByteArray subtitleData;
private final Format format;
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 int bytesRead;
private @State int state;
private long seekTimeUs;
/**
* @param subtitleDecoder The decoder used for decoding the subtitle data. The extractor will
* release the decoder in {@link SubtitleExtractor#release()}.
* @param format Format that describes subtitle data.
* Creates an instance.
*
* @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) {
this.subtitleDecoder = subtitleDecoder;
public SubtitleExtractor(SubtitleParser subtitleParser, Format format) {
this.subtitleParser = subtitleParser;
cueEncoder = new CueEncoder();
subtitleData = new ParsableByteArray();
subtitleData = Util.EMPTY_BYTE_ARRAY;
scratchSampleArray = new ParsableByteArray();
this.format =
format
.buildUpon()
@ -127,10 +129,9 @@ public class SubtitleExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
checkState(state == STATE_CREATED);
extractorOutput = output;
trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_TEXT);
extractorOutput.endTracks();
extractorOutput.seekMap(
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_TEXT);
output.endTracks();
output.seekMap(
new IndexSeekMap(
/* positions= */ 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 {
checkState(state != STATE_CREATED && state != STATE_RELEASED);
if (state == STATE_INITIALIZED) {
subtitleData.reset(
int length =
input.getLength() != C.LENGTH_UNSET
? Ints.checkedCast(input.getLength())
: DEFAULT_BUFFER_SIZE);
: DEFAULT_BUFFER_SIZE;
if (length > subtitleData.length) {
subtitleData = new byte[length];
}
bytesRead = 0;
state = STATE_EXTRACTING;
}
if (state == STATE_EXTRACTING) {
boolean inputFinished = readFromInput(input);
if (inputFinished) {
decode();
parse();
writeToOutput();
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
public void release() {
if (state == STATE_RELEASED) {
return;
}
subtitleDecoder.release();
subtitleParser.reset();
state = STATE_RELEASED;
}
@ -204,11 +208,13 @@ public class SubtitleExtractor implements Extractor {
/** Returns whether reading has been finished. */
private boolean readFromInput(ExtractorInput input) throws IOException {
if (subtitleData.capacity() == bytesRead) {
subtitleData.ensureCapacity(bytesRead + DEFAULT_BUFFER_SIZE);
if (subtitleData.length == bytesRead) {
subtitleData =
Arrays.copyOf(subtitleData, /* newLength= */ subtitleData.length + DEFAULT_BUFFER_SIZE);
}
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) {
bytesRead += readResult;
}
@ -217,45 +223,19 @@ public class SubtitleExtractor implements Extractor {
|| readResult == C.RESULT_END_OF_INPUT;
}
/** Decodes the subtitle data and stores the samples in the memory of the extractor. */
private void decode() throws IOException {
/** Parses the subtitle data and stores the samples in the memory of the extractor. */
private void parse() throws IOException {
try {
@Nullable SubtitleInputBuffer inputBuffer = subtitleDecoder.dequeueInputBuffer();
while (inputBuffer == null) {
Thread.sleep(5);
inputBuffer = subtitleDecoder.dequeueInputBuffer();
}
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);
List<CuesWithTiming> cuesWithTimingList = checkNotNull(subtitleParser.parse(subtitleData));
for (int i = 0; i < cuesWithTimingList.size(); i++) {
CuesWithTiming cuesWithTiming = cuesWithTimingList.get(i);
long eventTimeUs = cuesWithTiming.startTimeUs;
byte[] cuesSample = cueEncoder.encode(cuesWithTiming.cues, cuesWithTiming.durationUs);
timestamps.add(eventTimeUs);
samples.add(new ParsableByteArray(cuesSample));
samples.add(cuesSample);
}
outputBuffer.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException();
} catch (SubtitleDecoderException e) {
throw ParserException.createForMalformedContainer("SubtitleDecoder failed.", e);
} catch (RuntimeException e) {
throw ParserException.createForMalformedContainer("SubtitleParser failed.", e);
}
}
@ -268,10 +248,10 @@ public class SubtitleExtractor implements Extractor {
: Util.binarySearchFloor(
timestamps, seekTimeUs, /* inclusive= */ true, /* stayInBounds= */ true);
for (int i = index; i < samples.size(); i++) {
ParsableByteArray sample = samples.get(i);
sample.setPosition(0);
int size = sample.getData().length;
trackOutput.sampleData(sample, size);
byte[] sample = samples.get(i);
int size = sample.length;
scratchSampleArray.reset(sample);
trackOutput.sampleData(scratchSampleArray, size);
trackOutput.sampleMetadata(
/* timeUs= */ timestamps.get(i),
/* 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.MimeTypes;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.text.DelegatingSubtitleDecoder;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.webvtt.WebvttParser;
import androidx.media3.test.utils.FakeExtractorInput;
@ -65,9 +64,7 @@ public class SubtitleExtractorTest {
.build();
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
new WebvttParser(), new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output);
while (extractor.read(input, null) != Extractor.RESULT_END_OF_INPUT) {}
@ -109,9 +106,7 @@ public class SubtitleExtractorTest {
.build();
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
new WebvttParser(), new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output);
FakeTrackOutput trackOutput = output.trackOutputs.get(0);
@ -152,9 +147,7 @@ public class SubtitleExtractorTest {
.build();
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
new WebvttParser(), new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output);
FakeTrackOutput trackOutput = output.trackOutputs.get(0);
@ -189,10 +182,7 @@ public class SubtitleExtractorTest {
public void read_withoutInit_fails() {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
assertThrows(IllegalStateException.class, () -> extractor.read(input, null));
}
@ -201,10 +191,7 @@ public class SubtitleExtractorTest {
public void read_afterRelease_fails() {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output);
@ -216,10 +203,7 @@ public class SubtitleExtractorTest {
@Test
public void seek_withoutInit_fails() {
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
assertThrows(IllegalStateException.class, () -> extractor.seek(0, 0));
}
@ -227,10 +211,7 @@ public class SubtitleExtractorTest {
@Test
public void seek_afterRelease_fails() {
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output);
@ -242,10 +223,7 @@ public class SubtitleExtractorTest {
@Test
public void released_calledTwice() {
SubtitleExtractor extractor =
new SubtitleExtractor(
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().build());
new SubtitleExtractor(new WebvttParser(), new Format.Builder().build());
FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output);