diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java index 287ce9eb29..eb7a8f0524 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java @@ -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 = diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleExtractor.java index fdf5443feb..6978000f04 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleExtractor.java @@ -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 timestamps; - private final List samples; + private final List 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 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 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, diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/SubtitleExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/SubtitleExtractorTest.java index f60c720eac..9bbc2a0d24 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/SubtitleExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/SubtitleExtractorTest.java @@ -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);