diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java index 7ff9fcf28e..82a7262325 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java @@ -16,21 +16,18 @@ package androidx.media3.extractor.text; -import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.UnstableApi; import com.google.common.collect.ImmutableList; -import java.util.ArrayList; -import java.util.List; /** * Parses subtitle data into timed {@linkplain CuesWithTiming} instances. * - *

Instances are stateful, so samples can be fed in repeated calls to {@link #parse(byte[])}, and - * one or more complete {@link CuesWithTiming} instances will be returned when enough data has been + *

Instances are stateful, so samples can be fed in repeated calls to {@link #parse}, and one or + * more complete {@link CuesWithTiming} instances will be returned when enough data has been * received. Due to this stateful-ness, {@link #reset()} must be called after a seek or similar * discontinuity in the source data. */ @@ -124,17 +121,6 @@ public interface SubtitleParser { } } - /** - * Parses {@code data} (and any data stored from previous invocations) and returns any resulting - * complete {@link CuesWithTiming} instances. - * - *

Equivalent to {@link #parse(byte[], int, int) parse(data, 0, data.length)}. - */ - @Nullable - default List parse(byte[] data) { - return parse(data, /* offset= */ 0, data.length); - } - /** * Parses {@code data} (and any data stored from previous invocations) and emits resulting {@link * CuesWithTiming} instances. @@ -146,33 +132,6 @@ public interface SubtitleParser { parse(data, /* offset= */ 0, data.length, outputOptions, output); } - /** - * Parses {@code data} (and any data stored from previous invocations) and returns any resulting - * complete {@link CuesWithTiming} instances. - * - *

Any samples not used from {@code data} will be persisted and used during subsequent calls to - * this method. - * - *

{@link CuesWithTiming#startTimeUs} in the returned instance is derived only from the - * provided sample data, so has to be considered together with any relevant {@link - * Format#subsampleOffsetUs}. If the provided sample doesn't contain any timing information then - * at most one {@link CuesWithTiming} instance will be returned, with {@link - * CuesWithTiming#startTimeUs} set to {@link C#TIME_UNSET}, in which case {@link - * Format#subsampleOffsetUs} must be {@link Format#OFFSET_SAMPLE_RELATIVE}. - * - * @param data The subtitle data to parse. This must contain only complete samples. For subtitles - * muxed inside a media container, a sample is usually defined by the container. For subtitles - * read from a text file, a sample is usually the entire contents of the text file. - * @param offset The index in {@code data} to start reading from (inclusive). - * @param length The number of bytes to read from {@code data}. - * @return The {@linkplain CuesWithTiming} instances parsed from {@code data} (and possibly - * previous provided samples too), sorted in ascending order by {@link - * CuesWithTiming#startTimeUs}. Otherwise null if there is insufficient data to generate a - * complete {@link CuesWithTiming}. - */ - @Nullable - List parse(byte[] data, int offset, int length); - /** * Parses {@code data} (and any data stored from previous invocations) and emits any resulting * complete {@link CuesWithTiming} instances via {@code output}. @@ -197,35 +156,12 @@ public interface SubtitleParser { * will be made on the thread that called this method, and will be completed before this * method returns. */ - default void parse( + void parse( byte[] data, int offset, int length, OutputOptions outputOptions, - Consumer output) { - List cuesWithTimingList = parse(data, offset, length); - if (cuesWithTimingList == null) { - return; - } - @Nullable - List cuesWithTimingBeforeRequestedStartTimeUs = - outputOptions.startTimeUs != C.TIME_UNSET && outputOptions.outputAllCues - ? new ArrayList<>() - : null; - for (CuesWithTiming cuesWithTiming : cuesWithTimingList) { - if (outputOptions.startTimeUs == C.TIME_UNSET - || cuesWithTiming.startTimeUs >= outputOptions.startTimeUs) { - output.accept(cuesWithTiming); - } else if (cuesWithTimingBeforeRequestedStartTimeUs != null) { - cuesWithTimingBeforeRequestedStartTimeUs.add(cuesWithTiming); - } - } - if (cuesWithTimingBeforeRequestedStartTimeUs != null) { - for (CuesWithTiming cuesWithTiming : cuesWithTimingBeforeRequestedStartTimeUs) { - output.accept(cuesWithTiming); - } - } - } + Consumer output); /** * Parses {@code data} to a legacy {@link Subtitle} instance. @@ -250,7 +186,7 @@ public interface SubtitleParser { } /** - * Clears any data stored inside this parser from previous {@link #parse(byte[])} calls. + * Clears any data stored inside this parser from previous {@link #parse} calls. * *

This must be called after a seek or other similar discontinuity in the source data. * diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/dvb/DvbParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/dvb/DvbParser.java index d75b2d3299..4047830e97 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/dvb/DvbParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/dvb/DvbParser.java @@ -29,6 +29,7 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.text.Cue; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableBitArray; import androidx.media3.common.util.ParsableByteArray; @@ -143,9 +144,19 @@ public final class DvbParser implements SubtitleParser { } @Override - public ImmutableList parse(byte[] data, int offset, int length) { + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { ParsableBitArray dataBitArray = new ParsableBitArray(data, /* limit= */ offset + length); dataBitArray.setPosition(offset); + output.accept(parse(dataBitArray)); + } + + private CuesWithTiming parse(ParsableBitArray dataBitArray) { + while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40) && dataBitArray.readBits(8) == 0x0F) { parseSubtitlingSegment(dataBitArray, subtitleService); @@ -153,9 +164,8 @@ public final class DvbParser implements SubtitleParser { @Nullable PageComposition pageComposition = subtitleService.pageComposition; if (pageComposition == null) { - return ImmutableList.of( - new CuesWithTiming( - ImmutableList.of(), /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET)); + return new CuesWithTiming( + ImmutableList.of(), /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET); } // Update the canvas bitmap if necessary. @@ -266,8 +276,8 @@ public final class DvbParser implements SubtitleParser { canvas.restore(); } - return ImmutableList.of( - new CuesWithTiming(cues, /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET)); + return new CuesWithTiming( + cues, /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET); } // Static parsing. diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java index c916302a1b..87b4d4f6c7 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java @@ -23,12 +23,12 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.text.Cue; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.SubtitleParser; -import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; import java.util.zip.Inflater; @@ -68,7 +68,12 @@ public final class PgsParser implements SubtitleParser { } @Override - public ImmutableList parse(byte[] data, int offset, int length) { + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { buffer.reset(data, /* limit= */ offset + length); buffer.setPosition(offset); maybeInflateData(buffer); @@ -80,7 +85,7 @@ public final class PgsParser implements SubtitleParser { cues.add(cue); } } - return ImmutableList.of( + output.accept( new CuesWithTiming(cues, /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET)); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java index 5098b64f35..fc80730e72 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java @@ -32,6 +32,7 @@ import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; @@ -40,7 +41,6 @@ import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.SubtitleParser; import com.google.common.base.Ascii; import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableList; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -131,9 +131,13 @@ public final class SsaParser implements SubtitleParser { return CUE_REPLACEMENT_BEHAVIOR; } - @Nullable @Override - public ImmutableList parse(byte[] data, int offset, int length) { + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { List> cues = new ArrayList<>(); List startTimesUs = new ArrayList<>(); @@ -146,7 +150,11 @@ public final class SsaParser implements SubtitleParser { } parseEventBody(parsableByteArray, cues, startTimesUs, charset); - ImmutableList.Builder cuesWithStartTimeAndDuration = ImmutableList.builder(); + @Nullable + List cuesWithTimingBeforeRequestedStartTimeUs = + outputOptions.startTimeUs != C.TIME_UNSET && outputOptions.outputAllCues + ? new ArrayList<>() + : null; for (int i = 0; i < cues.size(); i++) { List cuesForThisStartTime = cues.get(i); if (cuesForThisStartTime.isEmpty() && i != 0) { @@ -160,10 +168,19 @@ public final class SsaParser implements SubtitleParser { long startTimeUs = startTimesUs.get(i); // It's safe to inspect element i+1, because we already exited the loop above if i=size()-1. long durationUs = startTimesUs.get(i + 1) - startTimesUs.get(i); - cuesWithStartTimeAndDuration.add( - new CuesWithTiming(cuesForThisStartTime, startTimeUs, durationUs)); + if (outputOptions.startTimeUs == C.TIME_UNSET || startTimeUs >= outputOptions.startTimeUs) { + output.accept(new CuesWithTiming(cuesForThisStartTime, startTimeUs, durationUs)); + + } else if (cuesWithTimingBeforeRequestedStartTimeUs != null) { + cuesWithTimingBeforeRequestedStartTimeUs.add( + new CuesWithTiming(cuesForThisStartTime, startTimeUs, durationUs)); + } + } + if (cuesWithTimingBeforeRequestedStartTimeUs != null) { + for (CuesWithTiming cuesWithTiming : cuesWithTimingBeforeRequestedStartTimeUs) { + output.accept(cuesWithTiming); + } } - return cuesWithStartTimeAndDuration.build(); } /** diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java index 1ed05b83ba..c6794cbd37 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java @@ -22,10 +22,12 @@ import android.text.Spanned; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; @@ -35,6 +37,7 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -91,15 +94,22 @@ public final class SubripParser implements SubtitleParser { return CUE_REPLACEMENT_BEHAVIOR; } - @Nullable @Override - public ImmutableList parse(byte[] data, int offset, int length) { - ImmutableList.Builder cues = new ImmutableList.Builder<>(); - + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { parsableByteArray.reset(data, /* limit= */ offset + length); parsableByteArray.setPosition(offset); Charset charset = detectUtfCharset(parsableByteArray); + @Nullable + List cuesWithTimingBeforeRequestedStartTimeUs = + outputOptions.startTimeUs != C.TIME_UNSET && outputOptions.outputAllCues + ? new ArrayList<>() + : null; @Nullable String currentLine; while ((currentLine = parsableByteArray.readLine(charset)) != null) { if (currentLine.length() == 0) { @@ -156,13 +166,25 @@ public final class SubripParser implements SubtitleParser { break; } } - cues.add( - new CuesWithTiming( - ImmutableList.of(buildCue(text, alignmentTag)), - startTimeUs, - /* durationUs= */ endTimeUs - startTimeUs)); + if (outputOptions.startTimeUs == C.TIME_UNSET || startTimeUs >= outputOptions.startTimeUs) { + output.accept( + new CuesWithTiming( + ImmutableList.of(buildCue(text, alignmentTag)), + startTimeUs, + /* durationUs= */ endTimeUs - startTimeUs)); + } else if (cuesWithTimingBeforeRequestedStartTimeUs != null) { + cuesWithTimingBeforeRequestedStartTimeUs.add( + new CuesWithTiming( + ImmutableList.of(buildCue(text, alignmentTag)), + startTimeUs, + /* durationUs= */ endTimeUs - startTimeUs)); + } + } + if (cuesWithTimingBeforeRequestedStartTimeUs != null) { + for (CuesWithTiming cuesWithTiming : cuesWithTimingBeforeRequestedStartTimeUs) { + output.accept(cuesWithTiming); + } } - return cues.build(); } /** diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java index 5899680cc6..64b3a1b499 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java @@ -41,7 +41,6 @@ import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.SubtitleDecoderException; import androidx.media3.extractor.text.SubtitleParser; import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableList; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayDeque; @@ -133,13 +132,6 @@ public final class TtmlParser implements SubtitleParser { return CUE_REPLACEMENT_BEHAVIOR; } - @Override - public ImmutableList parse(byte[] data, int offset, int length) { - ImmutableList.Builder cues = ImmutableList.builder(); - parse(data, offset, length, OutputOptions.allCues(), cues::add); - return cues.build(); - } - @Override public void parse( byte[] data, diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gParser.java index 3478693286..240c4617e7 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gParser.java @@ -32,6 +32,7 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.text.Cue; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; @@ -137,16 +138,22 @@ public final class Tx3gParser implements SubtitleParser { } @Override - public ImmutableList parse(byte[] data, int offset, int length) { + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { parsableByteArray.reset(data, /* limit= */ offset + length); parsableByteArray.setPosition(offset); String cueTextString = readSubtitleText(parsableByteArray); if (cueTextString.isEmpty()) { - return ImmutableList.of( + output.accept( new CuesWithTiming( /* cues= */ ImmutableList.of(), /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET)); + return; } // Attach default styles. SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString); @@ -180,7 +187,7 @@ public final class Tx3gParser implements SubtitleParser { .setLine(verticalPlacement, LINE_TYPE_FRACTION) .setLineAnchor(ANCHOR_TYPE_START) .build(); - return ImmutableList.of( + output.accept( new CuesWithTiming( ImmutableList.of(cue), /* startTimeUs= */ C.TIME_UNSET, diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java index 6e46ad786d..bac014addd 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/Mp4WebvttParser.java @@ -22,12 +22,12 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.text.Cue; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.SubtitleParser; -import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -67,7 +67,12 @@ public final class Mp4WebvttParser implements SubtitleParser { } @Override - public ImmutableList parse(byte[] data, int offset, int length) { + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { parsableByteArray.reset(data, /* limit= */ offset + length); parsableByteArray.setPosition(offset); List cues = new ArrayList<>(); @@ -86,7 +91,7 @@ public final class Mp4WebvttParser implements SubtitleParser { parsableByteArray.skipBytes(boxSize - BOX_HEADER_SIZE); } } - return ImmutableList.of( + output.accept( new CuesWithTiming(cues, /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET)); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttParser.java index c374f79b48..f08653a4c7 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttParser.java @@ -26,7 +26,6 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.LegacySubtitleUtil; import androidx.media3.extractor.text.SubtitleParser; -import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; @@ -67,13 +66,6 @@ public final class WebvttParser implements SubtitleParser { return CUE_REPLACEMENT_BEHAVIOR; } - @Override - public ImmutableList parse(byte[] data, int offset, int length) { - ImmutableList.Builder result = ImmutableList.builder(); - parse(data, offset, length, OutputOptions.allCues(), result::add); - return result.build(); - } - @Override public void parse( byte[] data,