Add setters of SubtitleParser.Factory and experimental toggle

The `SubtitleParser.Factory` is no longer @Nullable and the experimenting toggle is used to enable/disable the use of this factory for subtitle parsing during extraction.

The three places that will hold the "truth" for the `SubtitleParser.Factory` are: BundledChunkExtractor.Factory, SsChunkSource.Factory, DefaultHlsExtractorFactory

DASH: `DashMediaSource.Factory` would only propagate it to `DashChunkSource.Factory` -> `BundledChunkExtractor.Factory`

SS: `SSMediaSource.Factory` -> `SsChunkSource.Factory`

HLS: `HlsMediaSource.Factory` -> `HlsExtractorFactory`

#minor-release

PiperOrigin-RevId: 601151615
This commit is contained in:
jbibik 2024-01-24 09:52:48 -08:00 committed by Copybara-Service
parent 0acf6902e5
commit 4d7b23f0d1
15 changed files with 297 additions and 220 deletions

View File

@ -15,6 +15,11 @@
implementing a custom `CompositeSequenceableLoaderFactory`.
* Fix issue where repeating the same time causes metadata from this item
to be cleared ([#1007](https://github.com/androidx/media/issues/1007)).
* Rename `experimentalSetSubtitleParserFactory` methods on
`BundledChunkExtractor.Factory` and `DefaultHlsExtractorFactory` to
`setSubtitleParserFactory` and disallow passing `null`. Use the new
`experimentalParseSubtitlesDuringExtraction(boolean)` methods to control
parsing behaviour.
* Transformer:
* Track Selection:
* Extractors:

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.source.chunk;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import android.util.SparseArray;
@ -39,9 +40,11 @@ import androidx.media3.extractor.jpeg.JpegExtractor;
import androidx.media3.extractor.mkv.MatroskaExtractor;
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
import androidx.media3.extractor.png.PngExtractor;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleExtractor;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.extractor.text.SubtitleTranscodingExtractor;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
@ -57,24 +60,25 @@ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtrac
/** {@link ChunkExtractor.Factory} for {@link BundledChunkExtractor}. */
public static final class Factory implements ChunkExtractor.Factory {
/** Non-null if subtitles should be parsed during extraction, null otherwise. */
@Nullable private SubtitleParser.Factory subtitleParserFactory;
private SubtitleParser.Factory subtitleParserFactory;
private boolean parseSubtitlesDuringExtraction;
/**
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction, or
* null to parse subtitles during decoding. The default is null (subtitles parsed after
* decoding).
*
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
* in a future release.
*
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @return This factory, for convenience.
*/
public Factory experimentalSetSubtitleParserFactory(
@Nullable SubtitleParser.Factory subtitleParserFactory) {
this.subtitleParserFactory = subtitleParserFactory;
public Factory() {
subtitleParserFactory = new DefaultSubtitleParserFactory();
}
@CanIgnoreReturnValue
@Override
public Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
this.subtitleParserFactory = checkNotNull(subtitleParserFactory);
return this;
}
@CanIgnoreReturnValue
@Override
public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
return this;
}
@ -85,18 +89,16 @@ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtrac
* MimeTypes#APPLICATION_MEDIA3_CUES} if it is supported by {@link SubtitleParser.Factory}.
*
* <p>To modify the support behavior, you can {@linkplain
* #experimentalSetSubtitleParserFactory(SubtitleParser.Factory) set your own subtitle parser
* factory}.
* #setSubtitleParserFactory(SubtitleParser.Factory) set your own subtitle parser factory}.
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(sourceFormat)) {
@Format.CueReplacementBehavior
int cueReplacementBehavior = subtitleParserFactory.getCueReplacementBehavior(sourceFormat);
if (parseSubtitlesDuringExtraction && subtitleParserFactory.supportsFormat(sourceFormat)) {
return sourceFormat
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(cueReplacementBehavior)
.setCueReplacementBehavior(
subtitleParserFactory.getCueReplacementBehavior(sourceFormat))
.setCodecs(
sourceFormat.sampleMimeType
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
@ -119,7 +121,7 @@ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtrac
@Nullable String containerMimeType = representationFormat.containerMimeType;
Extractor extractor;
if (MimeTypes.isText(containerMimeType)) {
if (subtitleParserFactory == null) {
if (!parseSubtitlesDuringExtraction) {
// Subtitles will be parsed after decoding
return null;
} else {
@ -129,15 +131,10 @@ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtrac
}
} else if (MimeTypes.isMatroska(containerMimeType)) {
@MatroskaExtractor.Flags int flags = MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES;
if (subtitleParserFactory == null) {
if (!parseSubtitlesDuringExtraction) {
flags |= MatroskaExtractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
}
extractor =
new MatroskaExtractor(
subtitleParserFactory != null
? subtitleParserFactory
: SubtitleParser.Factory.UNSUPPORTED,
flags);
extractor = new MatroskaExtractor(subtitleParserFactory, flags);
} else if (Objects.equals(containerMimeType, MimeTypes.IMAGE_JPEG)) {
extractor = new JpegExtractor(JpegExtractor.FLAG_READ_IMAGE);
} else if (Objects.equals(containerMimeType, MimeTypes.IMAGE_PNG)) {
@ -147,21 +144,19 @@ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtrac
if (enableEventMessageTrack) {
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
}
if (subtitleParserFactory == null) {
if (!parseSubtitlesDuringExtraction) {
flags |= FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
}
extractor =
new FragmentedMp4Extractor(
subtitleParserFactory != null
? subtitleParserFactory
: SubtitleParser.Factory.UNSUPPORTED,
subtitleParserFactory,
flags,
/* timestampAdjuster= */ null,
/* sideloadedTrack= */ null,
closedCaptionFormats,
playerEmsgTrackOutput);
}
if (subtitleParserFactory != null
if (parseSubtitlesDuringExtraction
&& !MimeTypes.isText(containerMimeType)
&& !(extractor.getUnderlyingImplementation() instanceof FragmentedMp4Extractor)
&& !(extractor.getUnderlyingImplementation() instanceof MatroskaExtractor)) {

View File

@ -25,6 +25,8 @@ import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.TrackOutput;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.List;
@ -41,24 +43,34 @@ public interface ChunkExtractor {
interface Factory {
/**
* Returns a new {@link ChunkExtractor} instance.
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction. The
* default factory value is implementation dependent.
*
* @param primaryTrackType The {@link C.TrackType type} of the primary track.
* @param representationFormat The format of the representation to extract from.
* @param enableEventMessageTrack Whether to enable the event message track.
* @param closedCaptionFormats The {@link Format Formats} of the Closed-Caption tracks.
* @param playerEmsgTrackOutput The {@link TrackOutput} for extracted EMSG messages, or null.
* @param playerId The {@link PlayerId} of the player using this chunk extractor.
* @return A new {@link ChunkExtractor} instance, or null if not applicable.
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @return This factory, for convenience.
*/
@Nullable
ChunkExtractor createProgressiveMediaExtractor(
@C.TrackType int primaryTrackType,
Format representationFormat,
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput,
PlayerId playerId);
@CanIgnoreReturnValue
default Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
return this;
}
/**
* Sets whether subtitles should be parsed as part of extraction (before being added to the
* sample queue) or as part of rendering (when being taken from the sample queue). Defaults to
* {@code false} (i.e. subtitles will be parsed as part of rendering).
*
* <p>This method is experimental and will be renamed or removed in a future release.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
return this;
}
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples}
@ -79,6 +91,26 @@ public interface ChunkExtractor {
default Format getOutputTextFormat(Format sourceFormat) {
return sourceFormat;
}
/**
* Returns a new {@link ChunkExtractor} instance.
*
* @param primaryTrackType The {@link C.TrackType type} of the primary track.
* @param representationFormat The format of the representation to extract from.
* @param enableEventMessageTrack Whether to enable the event message track.
* @param closedCaptionFormats The {@link Format Formats} of the Closed-Caption tracks.
* @param playerEmsgTrackOutput The {@link TrackOutput} for extracted EMSG messages, or null.
* @param playerId The {@link PlayerId} of the player using this chunk extractor.
* @return A new {@link ChunkExtractor} instance, or null if not applicable.
*/
@Nullable
ChunkExtractor createProgressiveMediaExtractor(
@C.TrackType int primaryTrackType,
Format representationFormat,
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput,
PlayerId playerId);
}
/** Provides {@link TrackOutput} instances to be written to during extraction. */

View File

@ -30,6 +30,8 @@ import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
/** A {@link ChunkSource} for DASH streams. */
@ -39,6 +41,36 @@ public interface DashChunkSource extends ChunkSource {
/** Factory for {@link DashChunkSource}s. */
interface Factory {
/**
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction. The
* default factory value is implementation dependent.
*
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
return this;
}
/**
* Sets whether subtitles should be parsed as part of extraction (before being added to the
* sample queue) or as part of rendering (when being taken from the sample queue). Defaults to
* {@code false} (i.e. subtitles will be parsed as part of rendering).
*
* <p>This method is experimental and will be renamed or removed in a future release.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
return this;
}
/**
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest.

View File

@ -77,8 +77,6 @@ import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.exoplayer.util.SntpClient;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.base.Charsets;
import com.google.common.math.LongMath;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@ -114,7 +112,6 @@ public final class DashMediaSource extends BaseMediaSource {
private DrmSessionManagerProvider drmSessionManagerProvider;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@Nullable private SubtitleParser.Factory subtitleParserFactory;
private long fallbackTargetLiveOffsetMs;
private long minLiveStartPositionUs;
@Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
@ -199,33 +196,11 @@ public final class DashMediaSource extends BaseMediaSource {
return this;
}
/**
* {@inheritDoc}
*
* <p>This method may only be used with {@link DefaultDashChunkSource.Factory}.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
// TODO: b/289916598 - Flip the default of this to true.
@Override
@CanIgnoreReturnValue
public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
if (parseSubtitlesDuringExtraction) {
if (subtitleParserFactory == null) {
this.subtitleParserFactory = new DefaultSubtitleParserFactory();
}
} else {
this.subtitleParserFactory = null;
}
if (chunkSourceFactory instanceof DefaultDashChunkSource.Factory) {
((DefaultDashChunkSource.Factory) chunkSourceFactory)
.setSubtitleParserFactory(subtitleParserFactory);
} else {
throw new IllegalStateException();
}
chunkSourceFactory.experimentalParseSubtitlesDuringExtraction(parseSubtitlesDuringExtraction);
return this;
}

View File

@ -63,6 +63,7 @@ import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -113,18 +114,19 @@ public class DefaultDashChunkSource implements DashChunkSource {
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
}
/**
* Sets the {@link SubtitleParser.Factory} to be used for parsing subtitles during extraction,
* or null to parse subtitles during decoding.
*
* <p>This may only be used with {@link BundledChunkExtractor.Factory}.
*/
/* package */ Factory setSubtitleParserFactory(
@Nullable SubtitleParser.Factory subtitleParserFactory) {
if (chunkExtractorFactory instanceof BundledChunkExtractor.Factory) {
((BundledChunkExtractor.Factory) chunkExtractorFactory)
.experimentalSetSubtitleParserFactory(subtitleParserFactory);
}
@CanIgnoreReturnValue
@Override
public Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
chunkExtractorFactory.setSubtitleParserFactory(subtitleParserFactory);
return this;
}
@CanIgnoreReturnValue
@Override
public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
chunkExtractorFactory.experimentalParseSubtitlesDuringExtraction(
parseSubtitlesDuringExtraction);
return this;
}

View File

@ -17,7 +17,6 @@ package androidx.media3.exoplayer.hls;
import static androidx.media3.common.util.Assertions.checkState;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.Format;
import androidx.media3.common.util.TimestampAdjuster;
@ -47,7 +46,8 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
@VisibleForTesting /* package */ final Extractor extractor;
private final Format multivariantPlaylistFormat;
private final TimestampAdjuster timestampAdjuster;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
private final SubtitleParser.Factory subtitleParserFactory;
private final boolean parseSubtitlesDuringExtraction;
/**
* Creates a new instance.
@ -62,7 +62,8 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
extractor,
multivariantPlaylistFormat,
timestampAdjuster,
/* subtitleParserFactory= */ null);
SubtitleParser.Factory.UNSUPPORTED,
/* parseSubtitlesDuringExtraction= */ false);
}
/**
@ -77,16 +78,18 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
* multivariantPlaylistFormat.
*/
// TODO(b/289983417): Once the subtitle-parsing-during-extraction is the only available flow, make
// this constructor public and remove @Nullable from subtitleParserFactory
// this constructor public and remove parseSubtitlesDuringExtraction parameter
/* package */ BundledHlsMediaChunkExtractor(
Extractor extractor,
Format multivariantPlaylistFormat,
TimestampAdjuster timestampAdjuster,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
SubtitleParser.Factory subtitleParserFactory,
boolean parseSubtitlesDuringExtraction) {
this.extractor = extractor;
this.multivariantPlaylistFormat = multivariantPlaylistFormat;
this.timestampAdjuster = timestampAdjuster;
this.subtitleParserFactory = subtitleParserFactory;
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
}
@Override
@ -124,18 +127,11 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
Extractor newExtractorInstance;
// LINT.IfChange(extractor_instantiation)
if (extractor instanceof WebvttExtractor) {
SubtitleParser.Factory webvttSubtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED;
boolean parseSubtitlesDuringExtraction = false;
if (subtitleParserFactory != null
&& subtitleParserFactory.supportsFormat(multivariantPlaylistFormat)) {
webvttSubtitleParserFactory = subtitleParserFactory;
parseSubtitlesDuringExtraction = true;
}
newExtractorInstance =
new WebvttExtractor(
multivariantPlaylistFormat.language,
timestampAdjuster,
webvttSubtitleParserFactory,
subtitleParserFactory,
parseSubtitlesDuringExtraction);
} else if (extractor instanceof AdtsExtractor) {
newExtractorInstance = new AdtsExtractor();
@ -150,7 +146,11 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
"Unexpected extractor type for recreation: " + extractor.getClass().getSimpleName());
}
return new BundledHlsMediaChunkExtractor(
newExtractorInstance, multivariantPlaylistFormat, timestampAdjuster, subtitleParserFactory);
newExtractorInstance,
multivariantPlaylistFormat,
timestampAdjuster,
subtitleParserFactory,
parseSubtitlesDuringExtraction);
}
@Override

View File

@ -33,6 +33,7 @@ import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.mp3.Mp3Extractor;
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.extractor.ts.Ac3Extractor;
import androidx.media3.extractor.ts.Ac4Extractor;
@ -41,6 +42,7 @@ import androidx.media3.extractor.ts.DefaultTsPayloadReaderFactory;
import androidx.media3.extractor.ts.TsExtractor;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
@ -67,8 +69,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
private final @DefaultTsPayloadReaderFactory.Flags int payloadReaderFactoryFlags;
/** Non-null if subtitles should be parsed during extraction, null otherwise. */
@Nullable private SubtitleParser.Factory subtitleParserFactory;
private SubtitleParser.Factory subtitleParserFactory;
private boolean parseSubtitlesDuringExtraction;
private final boolean exposeCea608WhenMissingDeclarations;
@ -96,6 +98,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) {
this.payloadReaderFactoryFlags = payloadReaderFactoryFlags;
this.exposeCea608WhenMissingDeclarations = exposeCea608WhenMissingDeclarations;
subtitleParserFactory = new DefaultSubtitleParserFactory();
}
@Override
@ -135,7 +138,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
createExtractorByFileType(fileType, format, muxedCaptionFormats, timestampAdjuster));
if (sniffQuietly(extractor, sniffingExtractorInput)) {
return new BundledHlsMediaChunkExtractor(
extractor, format, timestampAdjuster, subtitleParserFactory);
extractor,
format,
timestampAdjuster,
subtitleParserFactory,
parseSubtitlesDuringExtraction);
}
if (fallBackExtractor == null
&& (fileType == formatInferredFileType
@ -149,26 +156,29 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
}
return new BundledHlsMediaChunkExtractor(
checkNotNull(fallBackExtractor), format, timestampAdjuster, subtitleParserFactory);
checkNotNull(fallBackExtractor),
format,
timestampAdjuster,
subtitleParserFactory,
parseSubtitlesDuringExtraction);
}
/**
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction, or null
* to parse subtitles during decoding. The default is null (subtitles parsed after decoding).
*
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
* in a future release.
*
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @return This factory, for convenience.
*/
public DefaultHlsExtractorFactory experimentalSetSubtitleParserFactory(
@Nullable SubtitleParser.Factory subtitleParserFactory) {
@CanIgnoreReturnValue
@Override
public DefaultHlsExtractorFactory setSubtitleParserFactory(
SubtitleParser.Factory subtitleParserFactory) {
this.subtitleParserFactory = subtitleParserFactory;
return this;
}
@CanIgnoreReturnValue
@Override
public DefaultHlsExtractorFactory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
return this;
}
/**
* {@inheritDoc}
*
@ -176,18 +186,15 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
* MimeTypes#APPLICATION_MEDIA3_CUES} if it is supported by {@link SubtitleParser.Factory}.
*
* <p>To modify the support behavior, you can {@linkplain
* #experimentalSetSubtitleParserFactory(SubtitleParser.Factory) set your own subtitle parser
* factory}.
* #setSubtitleParserFactory(SubtitleParser.Factory) set your own subtitle parser factory}.
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(sourceFormat)) {
@Format.CueReplacementBehavior
int cueReplacementBehavior = subtitleParserFactory.getCueReplacementBehavior(sourceFormat);
if (parseSubtitlesDuringExtraction && subtitleParserFactory.supportsFormat(sourceFormat)) {
return sourceFormat
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(cueReplacementBehavior)
.setCueReplacementBehavior(subtitleParserFactory.getCueReplacementBehavior(sourceFormat))
.setCodecs(
sourceFormat.sampleMimeType
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
@ -216,16 +223,10 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// LINT.IfChange(extractor_instantiation)
switch (fileType) {
case FileTypes.WEBVTT:
SubtitleParser.Factory webvttSubtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED;
boolean parseSubtitlesDuringExtraction = false;
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(format)) {
webvttSubtitleParserFactory = subtitleParserFactory;
parseSubtitlesDuringExtraction = true;
}
return new WebvttExtractor(
format.language,
timestampAdjuster,
webvttSubtitleParserFactory,
subtitleParserFactory,
parseSubtitlesDuringExtraction);
case FileTypes.ADTS:
return new AdtsExtractor();
@ -237,7 +238,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
case FileTypes.MP4:
return createFragmentedMp4Extractor(
subtitleParserFactory, timestampAdjuster, format, muxedCaptionFormats);
subtitleParserFactory,
parseSubtitlesDuringExtraction,
timestampAdjuster,
format,
muxedCaptionFormats);
case FileTypes.TS:
return createTsExtractor(
payloadReaderFactoryFlags,
@ -245,7 +250,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
format,
muxedCaptionFormats,
timestampAdjuster,
subtitleParserFactory);
subtitleParserFactory,
parseSubtitlesDuringExtraction);
default:
return null;
}
@ -257,7 +263,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
Format format,
@Nullable List<Format> muxedCaptionFormats,
TimestampAdjuster timestampAdjuster,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
SubtitleParser.Factory subtitleParserFactory,
boolean parseSubtitlesDuringExtraction) {
@DefaultTsPayloadReaderFactory.Flags
int payloadReaderFactoryFlags =
DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM
@ -287,7 +294,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
}
}
@TsExtractor.Flags int extractorFlags = 0;
if (subtitleParserFactory == null) {
if (!parseSubtitlesDuringExtraction) {
subtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED;
extractorFlags |= TsExtractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
}
@ -301,7 +308,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
}
private static FragmentedMp4Extractor createFragmentedMp4Extractor(
@Nullable SubtitleParser.Factory subtitleParserFactory,
SubtitleParser.Factory subtitleParserFactory,
boolean parseSubtitlesDuringExtraction,
TimestampAdjuster timestampAdjuster,
Format format,
@Nullable List<Format> muxedCaptionFormats) {
@ -309,7 +317,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// creating a separate EMSG track for every audio track in a video stream.
@FragmentedMp4Extractor.Flags
int flags = isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0;
if (subtitleParserFactory == null) {
if (!parseSubtitlesDuringExtraction) {
subtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED;
flags |= FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
}

View File

@ -26,6 +26,8 @@ import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@ -63,6 +65,37 @@ public interface HlsExtractorFactory {
PlayerId playerId)
throws IOException;
/**
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction. The
* default factory value is implementation dependent.
*
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default HlsExtractorFactory setSubtitleParserFactory(
SubtitleParser.Factory subtitleParserFactory) {
return this;
}
/**
* Sets whether subtitles should be parsed as part of extraction (before being added to the sample
* queue) or as part of rendering (when being taken from the sample queue). Defaults to {@code
* false} (i.e. subtitles will be parsed as part of rendering).
*
* <p>This method is experimental and will be renamed or removed in a future release.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default HlsExtractorFactory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
return this;
}
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples} which
* were originally in {@code sourceFormat}.

View File

@ -58,8 +58,6 @@ import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.lang.annotation.Documented;
@ -113,7 +111,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@Nullable private SubtitleParser.Factory subtitleParserFactory;
private boolean allowChunklessPreparation;
private @MetadataType int metadataType;
private boolean useSessionKeys;
@ -199,32 +197,11 @@ public final class HlsMediaSource extends BaseMediaSource
return this;
}
/**
* {@inheritDoc}
*
* <p>This method may only be used with {@link DefaultHlsExtractorFactory}.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
// TODO: b/289916598 - Flip the default of this to true.
@Override
@CanIgnoreReturnValue
public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
if (parseSubtitlesDuringExtraction) {
if (subtitleParserFactory == null) {
this.subtitleParserFactory = new DefaultSubtitleParserFactory();
}
} else {
this.subtitleParserFactory = null;
}
if (extractorFactory instanceof DefaultHlsExtractorFactory) {
((DefaultHlsExtractorFactory) extractorFactory)
.experimentalSetSubtitleParserFactory(subtitleParserFactory);
} else {
throw new IllegalStateException();
}
extractorFactory.experimentalParseSubtitlesDuringExtraction(parseSubtitlesDuringExtraction);
return this;
}

View File

@ -53,8 +53,10 @@ import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
import androidx.media3.extractor.mp4.Track;
import androidx.media3.extractor.mp4.TrackEncryptionBox;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.List;
@ -65,24 +67,29 @@ public class DefaultSsChunkSource implements SsChunkSource {
public static final class Factory implements SsChunkSource.Factory {
private final DataSource.Factory dataSourceFactory;
@Nullable private SubtitleParser.Factory subtitleParserFactory;
private SubtitleParser.Factory subtitleParserFactory;
private boolean parseSubtitlesDuringExtraction;
public Factory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
subtitleParserFactory = new DefaultSubtitleParserFactory();
}
/**
* Sets the {@link SubtitleParser.Factory} to be used for parsing subtitles during extraction,
* or {@code null} to parse subtitles during decoding.
*
* <p>This may only be used with {@link BundledChunkExtractor.Factory}.
*/
/* package */ Factory setSubtitleParserFactory(
@Nullable SubtitleParser.Factory subtitleParserFactory) {
@CanIgnoreReturnValue
@Override
public Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
this.subtitleParserFactory = subtitleParserFactory;
return this;
}
@CanIgnoreReturnValue
@Override
public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
return this;
}
/**
* {@inheritDoc}
*
@ -91,13 +98,12 @@ public class DefaultSsChunkSource implements SsChunkSource {
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(sourceFormat)) {
@Format.CueReplacementBehavior
int cueReplacementBehavior = subtitleParserFactory.getCueReplacementBehavior(sourceFormat);
if (parseSubtitlesDuringExtraction && subtitleParserFactory.supportsFormat(sourceFormat)) {
return sourceFormat
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(cueReplacementBehavior)
.setCueReplacementBehavior(
subtitleParserFactory.getCueReplacementBehavior(sourceFormat))
.setCodecs(
sourceFormat.sampleMimeType
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
@ -127,7 +133,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
trackSelection,
dataSource,
cmcdConfiguration,
subtitleParserFactory);
subtitleParserFactory,
parseSubtitlesDuringExtraction);
}
}
@ -158,6 +165,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
*/
public DefaultSsChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower,
@ -166,7 +175,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
ExoTrackSelection trackSelection,
DataSource dataSource,
@Nullable CmcdConfiguration cmcdConfiguration,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
SubtitleParser.Factory subtitleParserFactory,
boolean parseSubtitlesDuringExtraction) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
this.streamElementIndex = streamElementIndex;
@ -203,14 +213,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
int flags =
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX;
if (!parseSubtitlesDuringExtraction) {
flags |= FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
}
Extractor extractor =
new FragmentedMp4Extractor(
subtitleParserFactory == null
? SubtitleParser.Factory.UNSUPPORTED
: subtitleParserFactory,
subtitleParserFactory == null
? flags | FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA
: flags,
subtitleParserFactory,
flags,
/* timestampAdjuster= */ null,
track,
/* closedCaptionFormats= */ ImmutableList.of(),

View File

@ -27,6 +27,8 @@ import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** A {@link ChunkSource} for SmoothStreaming. */
@UnstableApi
@ -55,6 +57,36 @@ public interface SsChunkSource extends ChunkSource {
@Nullable TransferListener transferListener,
@Nullable CmcdConfiguration cmcdConfiguration);
/**
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction. The
* default factory value is implementation dependent.
*
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
* extraction.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
return this;
}
/**
* Sets whether subtitles should be parsed as part of extraction (before being added to the
* sample queue) or as part of rendering (when being taken from the sample queue). Defaults to
* {@code false} (i.e. subtitles will be parsed as part of rendering).
*
* <p>This method is experimental and will be renamed or removed in a future release.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
default Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
return this;
}
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples}
* which were originally in {@code sourceFormat}.

View File

@ -65,8 +65,6 @@ import androidx.media3.exoplayer.upstream.Loader;
import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
@ -93,7 +91,6 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@Nullable private SubtitleParser.Factory subtitleParserFactory;
private long livePresentationDelayMs;
@Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;
@ -155,32 +152,11 @@ public final class SsMediaSource extends BaseMediaSource
return this;
}
/**
* {@inheritDoc}
*
* <p>This method may only be used with {@link DefaultSsChunkSource.Factory}.
*
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
* rendering.
* @return This factory, for convenience.
*/
// TODO: b/289916598 - Flip the default of this to true.
@Override
@CanIgnoreReturnValue
public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) {
if (parseSubtitlesDuringExtraction) {
if (subtitleParserFactory == null) {
this.subtitleParserFactory = new DefaultSubtitleParserFactory();
}
} else {
this.subtitleParserFactory = null;
}
if (chunkSourceFactory instanceof DefaultSsChunkSource.Factory) {
((DefaultSsChunkSource.Factory) chunkSourceFactory)
.setSubtitleParserFactory(subtitleParserFactory);
} else {
throw new IllegalStateException();
}
chunkSourceFactory.experimentalParseSubtitlesDuringExtraction(parseSubtitlesDuringExtraction);
return this;
}

View File

@ -33,6 +33,7 @@ import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
@ -315,6 +316,7 @@ public class DefaultSsChunkSourceTest {
adaptiveTrackSelection,
new FakeDataSource(),
cmcdConfiguration,
/* subtitleParserFactory= */ null);
SubtitleParser.Factory.UNSUPPORTED,
/* parseSubtitlesDuringExtraction= */ false);
}
}

View File

@ -16,7 +16,6 @@
package androidx.media3.exoplayer.smoothstreaming;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import androidx.annotation.Nullable;
@ -33,6 +32,7 @@ import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
@ -104,14 +104,12 @@ public class SsMediaSourceTest {
@Test
public void
setExperimentalParseSubtitlesDuringExtraction_withNonDefaultChunkSourceFactory_setThrows() {
setExperimentalParseSubtitlesDuringExtraction_withNonDefaultChunkSourceFactory_setSucceeds() {
SsMediaSource.Factory ssMediaSourceFactory =
new SsMediaSource.Factory(
/* chunkSourceFactory= */ this::createSampleSsChunkSource,
/* manifestDataSourceFactory= */ () -> createSampleDataSource(SAMPLE_MANIFEST));
assertThrows(
IllegalStateException.class,
() -> ssMediaSourceFactory.experimentalParseSubtitlesDuringExtraction(false));
ssMediaSourceFactory.experimentalParseSubtitlesDuringExtraction(false);
}
@Test
@ -219,6 +217,7 @@ public class SsMediaSourceTest {
trackSelection,
new FakeDataSource(),
cmcdConfiguration,
/* subtitleParserFactory= */ null);
SubtitleParser.Factory.UNSUPPORTED,
/* parseSubtitlesDuringExtraction= */ false);
}
}