Remove SubtitleParser.Factory references from Hls/Ss/DashMediaPeriod

Those classes only needed to have access to a `SubtitleParser.Factory` to get a potentially updated `Format` for TrackGroups. The `SubtitleParser.Factory` was only used to check the support for the `mimeType` and getting some cue-related behaviour.

This introduced complexity in a way that both Periods and Extractors needed to have the same `SubtitleParser.Factory` in their individual stacks. To ensure that the sample queue would get the same transcoded/original format.

Instead, now we expose `getOutputTextFormat` methods on `ChunkExtractor.Factory`, `SsChunkSource.Factory` and `HlsExtractorFactory`. Those are the dependencies that Hls/Ss/DashMediaPeriod can make use of to delegate the format-updating logic to.

#minor-release

PiperOrigin-RevId: 601130714
This commit is contained in:
jbibik 2024-01-24 08:36:02 -08:00 committed by Copybara-Service
parent f8dbbc82e2
commit 966b710897
17 changed files with 249 additions and 126 deletions

View File

@ -78,6 +78,35 @@ public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtrac
return this; return this;
} }
/**
* {@inheritDoc}
*
* <p>This implementation performs transcoding of the original format to {@link
* 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}.
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(sourceFormat)) {
@Format.CueReplacementBehavior
int cueReplacementBehavior = subtitleParserFactory.getCueReplacementBehavior(sourceFormat);
return sourceFormat
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(cueReplacementBehavior)
.setCodecs(
sourceFormat.sampleMimeType
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.build();
} else {
return sourceFormat;
}
}
@Nullable @Nullable
@Override @Override
public ChunkExtractor createProgressiveMediaExtractor( public ChunkExtractor createProgressiveMediaExtractor(

View File

@ -18,9 +18,11 @@ package androidx.media3.exoplayer.source.chunk;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.extractor.ChunkIndex; import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import java.io.IOException; import java.io.IOException;
@ -57,6 +59,26 @@ public interface ChunkExtractor {
List<Format> closedCaptionFormats, List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput, @Nullable TrackOutput playerEmsgTrackOutput,
PlayerId playerId); PlayerId playerId);
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples}
* which were originally in {@code sourceFormat}.
*
* <p>In many cases, where an {@link Extractor} emits samples from the source without mutation,
* this method simply returns {@code sourceFormat}. In other cases, such as an {@link Extractor}
* that transcodes subtitles from the {@code sourceFormat} to {@link
* MimeTypes#APPLICATION_MEDIA3_CUES}, the format is updated to indicate the transcoding that is
* taking place.
*
* <p>Non-text source formats are always returned without mutation.
*
* @param sourceFormat The original text-based format.
* @return The {@link Format} that will be associated with a {@linkplain C#TRACK_TYPE_TEXT text
* track}.
*/
default Format getOutputTextFormat(Format sourceFormat) {
return sourceFormat;
}
} }
/** Provides {@link TrackOutput} instances to be written to during extraction. */ /** Provides {@link TrackOutput} instances to be written to during extraction. */

View File

@ -19,6 +19,7 @@ import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
@ -28,6 +29,7 @@ import androidx.media3.exoplayer.source.chunk.ChunkSource;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.Extractor;
import java.util.List; import java.util.List;
/** A {@link ChunkSource} for DASH streams. */ /** A {@link ChunkSource} for DASH streams. */
@ -74,6 +76,26 @@ public interface DashChunkSource extends ChunkSource {
@Nullable TransferListener transferListener, @Nullable TransferListener transferListener,
PlayerId playerId, PlayerId playerId,
@Nullable CmcdConfiguration cmcdConfiguration); @Nullable CmcdConfiguration cmcdConfiguration);
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples}
* which were originally in {@code sourceFormat}.
*
* <p>In many cases, where an {@link Extractor} emits samples from the source without mutation,
* this method simply returns {@code sourceFormat}. In other cases, such as an {@link Extractor}
* that transcodes subtitles from the {@code sourceFormat} to {@link
* MimeTypes#APPLICATION_MEDIA3_CUES}, the format is updated to indicate the transcoding that is
* taking place.
*
* <p>Non-text source formats are always returned without mutation.
*
* @param sourceFormat The original text-based format.
* @return The {@link Format} that will be associated with a {@linkplain C#TRACK_TYPE_TEXT text
* track}.
*/
default Format getOutputTextFormat(Format sourceFormat) {
return sourceFormat;
}
} }
/** /**

View File

@ -58,7 +58,6 @@ import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
@ -132,8 +131,7 @@ import java.util.regex.Pattern;
Allocator allocator, Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
PlayerEmsgCallback playerEmsgCallback, PlayerEmsgCallback playerEmsgCallback,
PlayerId playerId, PlayerId playerId) {
@Nullable SubtitleParser.Factory subtitleParserFactory) {
this.id = id; this.id = id;
this.manifest = manifest; this.manifest = manifest;
this.baseUrlExclusionList = baseUrlExclusionList; this.baseUrlExclusionList = baseUrlExclusionList;
@ -160,7 +158,7 @@ import java.util.regex.Pattern;
eventStreams = period.eventStreams; eventStreams = period.eventStreams;
Pair<TrackGroupArray, TrackGroupInfo[]> result = Pair<TrackGroupArray, TrackGroupInfo[]> result =
buildTrackGroups( buildTrackGroups(
drmSessionManager, subtitleParserFactory, period.adaptationSets, eventStreams); drmSessionManager, chunkSourceFactory, period.adaptationSets, eventStreams);
trackGroups = result.first; trackGroups = result.first;
trackGroupInfos = result.second; trackGroupInfos = result.second;
} }
@ -505,7 +503,7 @@ import java.util.regex.Pattern;
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups( private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
@Nullable SubtitleParser.Factory subtitleParserFactory, DashChunkSource.Factory chunkSourceFactory,
List<AdaptationSet> adaptationSets, List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams) { List<EventStream> eventStreams) {
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
@ -528,7 +526,7 @@ import java.util.regex.Pattern;
int trackGroupCount = int trackGroupCount =
buildPrimaryAndEmbeddedTrackGroupInfos( buildPrimaryAndEmbeddedTrackGroupInfos(
drmSessionManager, drmSessionManager,
subtitleParserFactory, chunkSourceFactory,
adaptationSets, adaptationSets,
groupedAdaptationSetIndices, groupedAdaptationSetIndices,
primaryGroupCount, primaryGroupCount,
@ -668,7 +666,7 @@ import java.util.regex.Pattern;
private static int buildPrimaryAndEmbeddedTrackGroupInfos( private static int buildPrimaryAndEmbeddedTrackGroupInfos(
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
@Nullable SubtitleParser.Factory subtitleParserFactory, DashChunkSource.Factory chunkSourceFactory,
List<AdaptationSet> adaptationSets, List<AdaptationSet> adaptationSets,
int[][] groupedAdaptationSetIndices, int[][] groupedAdaptationSetIndices,
int primaryGroupCount, int primaryGroupCount,
@ -704,7 +702,7 @@ import java.util.regex.Pattern;
int closedCaptionTrackGroupIndex = int closedCaptionTrackGroupIndex =
primaryGroupClosedCaptionTrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET; primaryGroupClosedCaptionTrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;
maybeUpdateFormatsForParsedText(subtitleParserFactory, formats); maybeUpdateFormatsForParsedText(chunkSourceFactory, formats);
trackGroups[primaryTrackGroupIndex] = new TrackGroup(trackGroupId, formats); trackGroups[primaryTrackGroupIndex] = new TrackGroup(trackGroupId, formats);
trackGroupInfos[primaryTrackGroupIndex] = trackGroupInfos[primaryTrackGroupIndex] =
TrackGroupInfo.primaryTrack( TrackGroupInfo.primaryTrack(
@ -732,7 +730,7 @@ import java.util.regex.Pattern;
primaryTrackGroupIndex, primaryTrackGroupIndex,
ImmutableList.copyOf(primaryGroupClosedCaptionTrackFormats[i])); ImmutableList.copyOf(primaryGroupClosedCaptionTrackFormats[i]));
maybeUpdateFormatsForParsedText( maybeUpdateFormatsForParsedText(
subtitleParserFactory, primaryGroupClosedCaptionTrackFormats[i]); chunkSourceFactory, primaryGroupClosedCaptionTrackFormats[i]);
trackGroups[closedCaptionTrackGroupIndex] = trackGroups[closedCaptionTrackGroupIndex] =
new TrackGroup(closedCaptionTrackGroupId, primaryGroupClosedCaptionTrackFormats[i]); new TrackGroup(closedCaptionTrackGroupId, primaryGroupClosedCaptionTrackFormats[i]);
} }
@ -928,22 +926,9 @@ import java.util.regex.Pattern;
* during extraction. * during extraction.
*/ */
private static void maybeUpdateFormatsForParsedText( private static void maybeUpdateFormatsForParsedText(
SubtitleParser.Factory subtitleParserFactory, Format[] formats) { DashChunkSource.Factory chunkSourceFactory, Format[] formats) {
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
if (subtitleParserFactory == null || !subtitleParserFactory.supportsFormat(formats[i])) { formats[i] = chunkSourceFactory.getOutputTextFormat(formats[i]);
continue;
}
formats[i] =
formats[i]
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(
subtitleParserFactory.getCueReplacementBehavior(formats[i]))
.setCodecs(
formats[i].sampleMimeType
+ (formats[i].codecs != null ? " " + formats[i].codecs : ""))
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.build();
} }
} }

View File

@ -210,6 +210,7 @@ public final class DashMediaSource extends BaseMediaSource {
*/ */
// TODO: b/289916598 - Flip the default of this to true. // TODO: b/289916598 - Flip the default of this to true.
@Override @Override
@CanIgnoreReturnValue
public Factory experimentalParseSubtitlesDuringExtraction( public Factory experimentalParseSubtitlesDuringExtraction(
boolean parseSubtitlesDuringExtraction) { boolean parseSubtitlesDuringExtraction) {
if (parseSubtitlesDuringExtraction) { if (parseSubtitlesDuringExtraction) {
@ -347,7 +348,6 @@ public final class DashMediaSource extends BaseMediaSource {
cmcdConfiguration, cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
subtitleParserFactory,
fallbackTargetLiveOffsetMs, fallbackTargetLiveOffsetMs,
minLiveStartPositionUs); minLiveStartPositionUs);
} }
@ -386,7 +386,6 @@ public final class DashMediaSource extends BaseMediaSource {
cmcdConfiguration, cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
subtitleParserFactory,
fallbackTargetLiveOffsetMs, fallbackTargetLiveOffsetMs,
minLiveStartPositionUs); minLiveStartPositionUs);
} }
@ -445,7 +444,6 @@ public final class DashMediaSource extends BaseMediaSource {
private final Runnable simulateManifestRefreshRunnable; private final Runnable simulateManifestRefreshRunnable;
private final PlayerEmsgCallback playerEmsgCallback; private final PlayerEmsgCallback playerEmsgCallback;
private final LoaderErrorThrower manifestLoadErrorThrower; private final LoaderErrorThrower manifestLoadErrorThrower;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
private DataSource dataSource; private DataSource dataSource;
private Loader loader; private Loader loader;
@ -481,7 +479,6 @@ public final class DashMediaSource extends BaseMediaSource {
@Nullable CmcdConfiguration cmcdConfiguration, @Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@Nullable SubtitleParser.Factory subtitleParserFactory,
long fallbackTargetLiveOffsetMs, long fallbackTargetLiveOffsetMs,
long minLiveStartPositionUs) { long minLiveStartPositionUs) {
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
@ -495,7 +492,6 @@ public final class DashMediaSource extends BaseMediaSource {
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.subtitleParserFactory = subtitleParserFactory;
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs; this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
this.minLiveStartPositionUs = minLiveStartPositionUs; this.minLiveStartPositionUs = minLiveStartPositionUs;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
@ -601,8 +597,7 @@ public final class DashMediaSource extends BaseMediaSource {
allocator, allocator,
compositeSequenceableLoaderFactory, compositeSequenceableLoaderFactory,
playerEmsgCallback, playerEmsgCallback,
getPlayerId(), getPlayerId());
subtitleParserFactory);
periodsById.put(mediaPeriod.id, mediaPeriod); periodsById.put(mediaPeriod.id, mediaPeriod);
return mediaPeriod; return mediaPeriod;
} }

View File

@ -166,6 +166,17 @@ public class DefaultDashChunkSource implements DashChunkSource {
playerId, playerId,
cmcdConfiguration); cmcdConfiguration);
} }
/**
* {@inheritDoc}
*
* <p>This implementation delegates determining of the output format to the {@link
* ChunkExtractor.Factory} passed to the constructor of this class.
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
return chunkExtractorFactory.getOutputTextFormat(sourceFormat);
}
} }
private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoaderErrorThrower manifestLoaderErrorThrower;

View File

@ -15,7 +15,9 @@
*/ */
package androidx.media3.exoplayer.dash; package androidx.media3.exoplayer.dash;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.net.Uri; import android.net.Uri;
import androidx.media3.common.Format; import androidx.media3.common.Format;
@ -205,12 +207,14 @@ public final class DashMediaPeriodTest {
private static DashMediaPeriod createDashMediaPeriod(DashManifest manifest, int periodIndex) { private static DashMediaPeriod createDashMediaPeriod(DashManifest manifest, int periodIndex) {
MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object()); MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object());
DashChunkSource.Factory chunkSourceFactory = mock(DashChunkSource.Factory.class);
when(chunkSourceFactory.getOutputTextFormat(any())).thenCallRealMethod();
return new DashMediaPeriod( return new DashMediaPeriod(
/* id= */ periodIndex, /* id= */ periodIndex,
manifest, manifest,
new BaseUrlExclusionList(), new BaseUrlExclusionList(),
periodIndex, periodIndex,
mock(DashChunkSource.Factory.class), chunkSourceFactory,
mock(TransferListener.class), mock(TransferListener.class),
/* cmcdConfiguration= */ null, /* cmcdConfiguration= */ null,
DrmSessionManager.DRM_UNSUPPORTED, DrmSessionManager.DRM_UNSUPPORTED,
@ -224,8 +228,7 @@ public final class DashMediaPeriodTest {
mock(Allocator.class), mock(Allocator.class),
mock(CompositeSequenceableLoaderFactory.class), mock(CompositeSequenceableLoaderFactory.class),
mock(PlayerEmsgCallback.class), mock(PlayerEmsgCallback.class),
PlayerId.UNSET, PlayerId.UNSET);
/* subtitleParserFactory= */ null);
} }
private static DashManifest parseManifest(String fileName) throws IOException { private static DashManifest parseManifest(String fileName) throws IOException {

View File

@ -169,6 +169,35 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
return this; return this;
} }
/**
* {@inheritDoc}
*
* <p>This implementation performs transcoding of the original format to {@link
* 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}.
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(sourceFormat)) {
@Format.CueReplacementBehavior
int cueReplacementBehavior = subtitleParserFactory.getCueReplacementBehavior(sourceFormat);
return sourceFormat
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(cueReplacementBehavior)
.setCodecs(
sourceFormat.sampleMimeType
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.build();
} else {
return sourceFormat;
}
}
private static void addFileTypeIfValidAndNotPresent( private static void addFileTypeIfValidAndNotPresent(
@FileTypes.Type int fileType, List<Integer> fileTypes) { @FileTypes.Type int fileType, List<Integer> fileTypes) {
if (Ints.indexOf(DEFAULT_EXTRACTOR_ORDER, fileType) == -1 || fileTypes.contains(fileType)) { if (Ints.indexOf(DEFAULT_EXTRACTOR_ORDER, fileType) == -1 || fileTypes.contains(fileType)) {

View File

@ -17,7 +17,9 @@ package androidx.media3.exoplayer.hls;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.TimestampAdjuster; import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
@ -60,4 +62,24 @@ public interface HlsExtractorFactory {
ExtractorInput sniffingExtractorInput, ExtractorInput sniffingExtractorInput,
PlayerId playerId) PlayerId playerId)
throws IOException; throws IOException;
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples} which
* were originally in {@code sourceFormat}.
*
* <p>In many cases, where an {@link Extractor} emits samples from the source without mutation,
* this method simply returns {@code sourceFormat}. In other cases, such as an {@link Extractor}
* that transcodes subtitles from the {@code sourceFormat} to {@link
* MimeTypes#APPLICATION_MEDIA3_CUES}, the format is updated to indicate the transcoding that is
* taking place.
*
* <p>Non-text source formats are always returned without mutation.
*
* @param sourceFormat The original text-based format.
* @return The {@link Format} that will be associated with a {@linkplain C#TRACK_TYPE_TEXT text
* track}.
*/
default Format getOutputTextFormat(Format sourceFormat) {
return sourceFormat;
}
} }

View File

@ -51,7 +51,6 @@ import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.Extractor; import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -86,7 +85,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final PlayerId playerId; private final PlayerId playerId;
private final HlsSampleStreamWrapper.Callback sampleStreamWrapperCallback; private final HlsSampleStreamWrapper.Callback sampleStreamWrapperCallback;
private final long timestampAdjusterInitializationTimeoutMs; private final long timestampAdjusterInitializationTimeoutMs;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
@Nullable private MediaPeriod.Callback mediaPeriodCallback; @Nullable private MediaPeriod.Callback mediaPeriodCallback;
private int pendingPrepareCount; private int pendingPrepareCount;
@ -141,8 +139,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@HlsMediaSource.MetadataType int metadataType, @HlsMediaSource.MetadataType int metadataType,
boolean useSessionKeys, boolean useSessionKeys,
PlayerId playerId, PlayerId playerId,
long timestampAdjusterInitializationTimeoutMs, long timestampAdjusterInitializationTimeoutMs) {
@Nullable SubtitleParser.Factory subtitleParserFactory) {
this.extractorFactory = extractorFactory; this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker; this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
@ -167,7 +164,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleStreamWrappers = new HlsSampleStreamWrapper[0]; sampleStreamWrappers = new HlsSampleStreamWrapper[0];
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
manifestUrlIndicesPerWrapper = new int[0][]; manifestUrlIndicesPerWrapper = new int[0][];
this.subtitleParserFactory = subtitleParserFactory;
} }
public void release() { public void release() {
@ -538,7 +534,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleStreamWrapper.prepareWithMultivariantPlaylistInfo( sampleStreamWrapper.prepareWithMultivariantPlaylistInfo(
new TrackGroup[] { new TrackGroup[] {
new TrackGroup( new TrackGroup(
sampleStreamWrapperUid, maybeUpdateFormatForParsedText(originalSubtitleFormat)) sampleStreamWrapperUid,
extractorFactory.getOutputTextFormat(originalSubtitleFormat))
}, },
/* primaryTrackGroupIndex= */ 0); /* primaryTrackGroupIndex= */ 0);
} }
@ -686,7 +683,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < ccFormats.size(); i++) { for (int i = 0; i < ccFormats.size(); i++) {
String ccId = sampleStreamWrapperUid + ":cc:" + i; String ccId = sampleStreamWrapperUid + ":cc:" + i;
muxedTrackGroups.add( muxedTrackGroups.add(
new TrackGroup(ccId, maybeUpdateFormatForParsedText(ccFormats.get(i)))); new TrackGroup(ccId, extractorFactory.getOutputTextFormat(ccFormats.get(i))));
} }
} }
} else /* numberOfAudioCodecs > 0 */ { } else /* numberOfAudioCodecs > 0 */ {
@ -910,23 +907,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.build(); .build();
} }
/**
* Returns a modified {@link Format} if subtitle/caption parsing is configured to happen during
* extraction.
*/
private Format maybeUpdateFormatForParsedText(Format format) {
if (subtitleParserFactory == null || !subtitleParserFactory.supportsFormat(format)) {
return format;
}
return format
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(subtitleParserFactory.getCueReplacementBehavior(format))
.setCodecs(format.sampleMimeType + (format.codecs != null ? " " + format.codecs : ""))
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.build();
}
private class SampleStreamWrapperCallback implements HlsSampleStreamWrapper.Callback { private class SampleStreamWrapperCallback implements HlsSampleStreamWrapper.Callback {
@Override @Override
public void onPrepared() { public void onPrepared() {

View File

@ -413,7 +413,6 @@ public final class HlsMediaSource extends BaseMediaSource
mediaItem, mediaItem,
hlsDataSourceFactory, hlsDataSourceFactory,
extractorFactory, extractorFactory,
subtitleParserFactory,
compositeSequenceableLoaderFactory, compositeSequenceableLoaderFactory,
cmcdConfiguration, cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
@ -445,7 +444,6 @@ public final class HlsMediaSource extends BaseMediaSource
private final HlsPlaylistTracker playlistTracker; private final HlsPlaylistTracker playlistTracker;
private final long elapsedRealTimeOffsetMs; private final long elapsedRealTimeOffsetMs;
private final long timestampAdjusterInitializationTimeoutMs; private final long timestampAdjusterInitializationTimeoutMs;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
private MediaItem.LiveConfiguration liveConfiguration; private MediaItem.LiveConfiguration liveConfiguration;
@Nullable private TransferListener mediaTransferListener; @Nullable private TransferListener mediaTransferListener;
@ -457,7 +455,6 @@ public final class HlsMediaSource extends BaseMediaSource
MediaItem mediaItem, MediaItem mediaItem,
HlsDataSourceFactory dataSourceFactory, HlsDataSourceFactory dataSourceFactory,
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
@Nullable SubtitleParser.Factory subtitleParserFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
@Nullable CmcdConfiguration cmcdConfiguration, @Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
@ -472,7 +469,6 @@ public final class HlsMediaSource extends BaseMediaSource
this.liveConfiguration = mediaItem.liveConfiguration; this.liveConfiguration = mediaItem.liveConfiguration;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.extractorFactory = extractorFactory; this.extractorFactory = extractorFactory;
this.subtitleParserFactory = subtitleParserFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
@ -547,8 +543,7 @@ public final class HlsMediaSource extends BaseMediaSource
metadataType, metadataType,
useSessionKeys, useSessionKeys,
getPlayerId(), getPlayerId(),
timestampAdjusterInitializationTimeoutMs, timestampAdjusterInitializationTimeoutMs);
subtitleParserFactory);
} }
@Override @Override

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.exoplayer.hls; package androidx.media3.exoplayer.hls;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -73,6 +74,8 @@ public final class HlsMediaPeriodTest {
createSubtitleFormat("eng"), createSubtitleFormat("gsw"))); createSubtitleFormat("eng"), createSubtitleFormat("gsw")));
FilterableManifestMediaPeriodFactory<HlsPlaylist> mediaPeriodFactory = FilterableManifestMediaPeriodFactory<HlsPlaylist> mediaPeriodFactory =
(playlist, periodIndex) -> { (playlist, periodIndex) -> {
HlsExtractorFactory mockHlsExtractorFactory = mock(HlsExtractorFactory.class);
when(mockHlsExtractorFactory.getOutputTextFormat(any())).thenCallRealMethod();
HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class); HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class);
when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class)); when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class));
HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class);
@ -80,7 +83,7 @@ public final class HlsMediaPeriodTest {
.thenReturn((HlsMultivariantPlaylist) playlist); .thenReturn((HlsMultivariantPlaylist) playlist);
MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object()); MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object());
return new HlsMediaPeriod( return new HlsMediaPeriod(
mock(HlsExtractorFactory.class), mockHlsExtractorFactory,
mockPlaylistTracker, mockPlaylistTracker,
mockDataSourceFactory, mockDataSourceFactory,
mock(TransferListener.class), mock(TransferListener.class),
@ -97,8 +100,7 @@ public final class HlsMediaPeriodTest {
HlsMediaSource.METADATA_TYPE_ID3, HlsMediaSource.METADATA_TYPE_ID3,
/* useSessionKeys= */ false, /* useSessionKeys= */ false,
PlayerId.UNSET, PlayerId.UNSET,
/* timestampAdjusterInitializationTimeoutMs= */ 0, /* timestampAdjusterInitializationTimeoutMs= */ 0);
/* subtitleParserFactory= */ null);
}; };
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(

View File

@ -23,6 +23,7 @@ import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.UriUtil; import androidx.media3.common.util.UriUtil;
@ -82,6 +83,31 @@ public class DefaultSsChunkSource implements SsChunkSource {
return this; return this;
} }
/**
* {@inheritDoc}
*
* <p>This implementation performs transcoding of the original format to {@link
* MimeTypes#APPLICATION_MEDIA3_CUES} if it is supported by {@link SubtitleParser.Factory}.
*/
@Override
public Format getOutputTextFormat(Format sourceFormat) {
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(sourceFormat)) {
@Format.CueReplacementBehavior
int cueReplacementBehavior = subtitleParserFactory.getCueReplacementBehavior(sourceFormat);
return sourceFormat
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(cueReplacementBehavior)
.setCodecs(
sourceFormat.sampleMimeType
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.build();
} else {
return sourceFormat;
}
}
@Override @Override
public SsChunkSource createChunkSource( public SsChunkSource createChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,

View File

@ -16,6 +16,9 @@
package androidx.media3.exoplayer.smoothstreaming; package androidx.media3.exoplayer.smoothstreaming;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest; import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest;
@ -23,6 +26,7 @@ import androidx.media3.exoplayer.source.chunk.ChunkSource;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.Extractor;
/** A {@link ChunkSource} for SmoothStreaming. */ /** A {@link ChunkSource} for SmoothStreaming. */
@UnstableApi @UnstableApi
@ -50,6 +54,26 @@ public interface SsChunkSource extends ChunkSource {
ExoTrackSelection trackSelection, ExoTrackSelection trackSelection,
@Nullable TransferListener transferListener, @Nullable TransferListener transferListener,
@Nullable CmcdConfiguration cmcdConfiguration); @Nullable CmcdConfiguration cmcdConfiguration);
/**
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples}
* which were originally in {@code sourceFormat}.
*
* <p>In many cases, where an {@link Extractor} emits samples from the source without mutation,
* this method simply returns {@code sourceFormat}. In other cases, such as an {@link Extractor}
* that transcodes subtitles from the {@code sourceFormat} to {@link
* MimeTypes#APPLICATION_MEDIA3_CUES}, the format is updated to indicate the transcoding that is
* taking place.
*
* <p>Non-text source formats are always returned without mutation.
*
* @param sourceFormat The original text-based format.
* @return The {@link Format} that will be associated with a {@linkplain C#TRACK_TYPE_TEXT text
* track}.
*/
default Format getOutputTextFormat(Format sourceFormat) {
return sourceFormat;
}
} }
/** /**

View File

@ -20,7 +20,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey; import androidx.media3.common.StreamKey;
import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.NullableType; import androidx.media3.common.util.NullableType;
@ -42,7 +41,6 @@ import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -79,8 +77,7 @@ import java.util.List;
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,
Allocator allocator, Allocator allocator) {
@Nullable SubtitleParser.Factory subtitleParserFactory) {
this.manifest = manifest; this.manifest = manifest;
this.chunkSourceFactory = chunkSourceFactory; this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener; this.transferListener = transferListener;
@ -92,7 +89,7 @@ import java.util.List;
this.mediaSourceEventDispatcher = mediaSourceEventDispatcher; this.mediaSourceEventDispatcher = mediaSourceEventDispatcher;
this.allocator = allocator; this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
trackGroups = buildTrackGroups(manifest, drmSessionManager, subtitleParserFactory); trackGroups = buildTrackGroups(manifest, drmSessionManager, chunkSourceFactory);
sampleStreams = newSampleStreamArray(0); sampleStreams = newSampleStreamArray(0);
compositeSequenceableLoader = compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
@ -270,30 +267,19 @@ import java.util.List;
private static TrackGroupArray buildTrackGroups( private static TrackGroupArray buildTrackGroups(
SsManifest manifest, SsManifest manifest,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
@Nullable SubtitleParser.Factory subtitleParserFactory) { SsChunkSource.Factory chunkSourceFactory) {
TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length]; TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length];
for (int i = 0; i < manifest.streamElements.length; i++) { for (int i = 0; i < manifest.streamElements.length; i++) {
Format[] manifestFormats = manifest.streamElements[i].formats; Format[] manifestFormats = manifest.streamElements[i].formats;
Format[] exposedFormats = new Format[manifestFormats.length]; Format[] exposedFormats = new Format[manifestFormats.length];
for (int j = 0; j < manifestFormats.length; j++) { for (int j = 0; j < manifestFormats.length; j++) {
Format manifestFormat = manifestFormats[j]; Format manifestFormat = manifestFormats[j];
Format.Builder updatedFormat = Format updatedFormatWithDrm =
manifestFormat manifestFormat
.buildUpon() .buildUpon()
.setCryptoType(drmSessionManager.getCryptoType(manifestFormat)); .setCryptoType(drmSessionManager.getCryptoType(manifestFormat))
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(manifestFormat)) { .build();
updatedFormat exposedFormats[j] = chunkSourceFactory.getOutputTextFormat(updatedFormatWithDrm);
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(
subtitleParserFactory.getCueReplacementBehavior(manifestFormat))
.setCodecs(
manifestFormat.sampleMimeType
+ (manifestFormat.codecs != null ? " " + manifestFormat.codecs : ""))
// Reset this value to the default. All non-default timestamp adjustments are done
// by SubtitleTranscodingExtractor and there are no 'subsamples' after transcoding.
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE);
}
exposedFormats[j] = updatedFormat.build();
} }
trackGroups[i] = new TrackGroup(/* id= */ Integer.toString(i), exposedFormats); trackGroups[i] = new TrackGroup(/* id= */ Integer.toString(i), exposedFormats);
} }

View File

@ -305,7 +305,6 @@ public final class SsMediaSource extends BaseMediaSource
cmcdConfiguration, cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
subtitleParserFactory,
livePresentationDelayMs); livePresentationDelayMs);
} }
@ -343,7 +342,6 @@ public final class SsMediaSource extends BaseMediaSource
cmcdConfiguration, cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
subtitleParserFactory,
livePresentationDelayMs); livePresentationDelayMs);
} }
@ -387,7 +385,6 @@ public final class SsMediaSource extends BaseMediaSource
private long manifestLoadStartTimestamp; private long manifestLoadStartTimestamp;
private SsManifest manifest; private SsManifest manifest;
private Handler manifestRefreshHandler; private Handler manifestRefreshHandler;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
@GuardedBy("this") @GuardedBy("this")
private MediaItem mediaItem; private MediaItem mediaItem;
@ -402,7 +399,6 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable CmcdConfiguration cmcdConfiguration, @Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@Nullable SubtitleParser.Factory subtitleParserFactory,
long livePresentationDelayMs) { long livePresentationDelayMs) {
Assertions.checkState(manifest == null || !manifest.isLive); Assertions.checkState(manifest == null || !manifest.isLive);
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
@ -419,7 +415,6 @@ public final class SsMediaSource extends BaseMediaSource
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.subtitleParserFactory = subtitleParserFactory;
this.livePresentationDelayMs = livePresentationDelayMs; this.livePresentationDelayMs = livePresentationDelayMs;
this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
sideloadedManifest = manifest != null; sideloadedManifest = manifest != null;
@ -487,8 +482,7 @@ public final class SsMediaSource extends BaseMediaSource
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
mediaSourceEventDispatcher, mediaSourceEventDispatcher,
manifestLoaderErrorThrower, manifestLoaderErrorThrower,
allocator, allocator);
subtitleParserFactory);
mediaPeriods.add(period); mediaPeriods.add(period);
return period; return period;
} }

View File

@ -18,7 +18,9 @@ package androidx.media3.exoplayer.smoothstreaming;
import static androidx.media3.exoplayer.smoothstreaming.SsTestUtils.createSsManifest; import static androidx.media3.exoplayer.smoothstreaming.SsTestUtils.createSsManifest;
import static androidx.media3.exoplayer.smoothstreaming.SsTestUtils.createStreamElement; import static androidx.media3.exoplayer.smoothstreaming.SsTestUtils.createStreamElement;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
@ -63,24 +65,13 @@ public class SsMediaPeriodTest {
createAudioFormat(/* bitrate= */ 96000)), createAudioFormat(/* bitrate= */ 96000)),
createStreamElement( createStreamElement(
/* name= */ "text", C.TRACK_TYPE_TEXT, createTextFormat(/* language= */ "eng"))); /* name= */ "text", C.TRACK_TYPE_TEXT, createTextFormat(/* language= */ "eng")));
SsChunkSource.Factory chunkSourceFactory = mock(SsChunkSource.Factory.class);
when(chunkSourceFactory.getOutputTextFormat(any())).thenCallRealMethod();
FilterableManifestMediaPeriodFactory<SsManifest> mediaPeriodFactory = FilterableManifestMediaPeriodFactory<SsManifest> mediaPeriodFactory =
(manifest, periodIndex) -> { (manifest, periodIndex) -> {
MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object()); MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object());
return new SsMediaPeriod( return createSsMediaPeriod(manifest, mediaPeriodId, chunkSourceFactory);
manifest,
mock(SsChunkSource.Factory.class),
mock(TransferListener.class),
mock(CompositeSequenceableLoaderFactory.class),
/* cmcdConfiguration= */ null,
mock(DrmSessionManager.class),
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoadErrorHandlingPolicy.class),
new MediaSourceEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoaderErrorThrower.class),
mock(Allocator.class),
/* subtitleParserFactory= */ null);
}; };
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
@ -127,27 +118,34 @@ public class SsMediaPeriodTest {
new FakeTimeline(/* windowCount= */ 2).getUidOfPeriod(/* periodIndex= */ 0), new FakeTimeline(/* windowCount= */ 2).getUidOfPeriod(/* periodIndex= */ 0),
/* windowSequenceNumber= */ 0); /* windowSequenceNumber= */ 0);
SsMediaPeriod period = SsChunkSource.Factory chunkSourceFactory = mock(SsChunkSource.Factory.class);
new SsMediaPeriod( // Default implementation of SsChunkSource.Factory.getOutputTextFormat doesn't transcode
testManifest, // DefaultSsChunkSource.Factory is final (not mockable) and has a null SubtitleParser.Factory
mock(SsChunkSource.Factory.class), when(chunkSourceFactory.getOutputTextFormat(any())).thenReturn(expectedSubtitleFormat);
mock(TransferListener.class), SsMediaPeriod period = createSsMediaPeriod(testManifest, mediaPeriodId, chunkSourceFactory);
mock(CompositeSequenceableLoaderFactory.class),
/* cmcdConfiguration= */ null,
mock(DrmSessionManager.class),
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoadErrorHandlingPolicy.class),
new MediaSourceEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoaderErrorThrower.class),
mock(Allocator.class),
subtitleParserFactory);
Format subtitleFormat = period.getTrackGroups().get(2).getFormat(0); Format subtitleFormat = period.getTrackGroups().get(2).getFormat(0);
assertThat(subtitleFormat).isEqualTo(expectedSubtitleFormat); assertThat(subtitleFormat).isEqualTo(expectedSubtitleFormat);
} }
private static SsMediaPeriod createSsMediaPeriod(
SsManifest manifest, MediaPeriodId mediaPeriodId, SsChunkSource.Factory chunkSourceFactory) {
return new SsMediaPeriod(
manifest,
chunkSourceFactory,
mock(TransferListener.class),
mock(CompositeSequenceableLoaderFactory.class),
/* cmcdConfiguration= */ null,
mock(DrmSessionManager.class),
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoadErrorHandlingPolicy.class),
new MediaSourceEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoaderErrorThrower.class),
mock(Allocator.class));
}
private static Format createVideoFormat(int bitrate) { private static Format createVideoFormat(int bitrate) {
return new Format.Builder() return new Format.Builder()
.setContainerMimeType(MimeTypes.VIDEO_MP4) .setContainerMimeType(MimeTypes.VIDEO_MP4)