mirror of
https://github.com/androidx/media.git
synced 2025-05-11 01:31:40 +08:00
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:
parent
9e975b25d1
commit
b8e6f27caf
@ -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 =
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user