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;
}
/**
* {@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
@Override
public ChunkExtractor createProgressiveMediaExtractor(

View File

@ -18,9 +18,11 @@ package androidx.media3.exoplayer.source.chunk;
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.exoplayer.analytics.PlayerId;
import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.TrackOutput;
import java.io.IOException;
@ -57,6 +59,26 @@ public interface ChunkExtractor {
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput,
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. */

View File

@ -19,6 +19,7 @@ import android.os.SystemClock;
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.datasource.TransferListener;
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.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.Extractor;
import java.util.List;
/** A {@link ChunkSource} for DASH streams. */
@ -74,6 +76,26 @@ public interface DashChunkSource extends ChunkSource {
@Nullable TransferListener transferListener,
PlayerId playerId,
@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.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.primitives.Ints;
@ -132,8 +131,7 @@ import java.util.regex.Pattern;
Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
PlayerEmsgCallback playerEmsgCallback,
PlayerId playerId,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
PlayerId playerId) {
this.id = id;
this.manifest = manifest;
this.baseUrlExclusionList = baseUrlExclusionList;
@ -160,7 +158,7 @@ import java.util.regex.Pattern;
eventStreams = period.eventStreams;
Pair<TrackGroupArray, TrackGroupInfo[]> result =
buildTrackGroups(
drmSessionManager, subtitleParserFactory, period.adaptationSets, eventStreams);
drmSessionManager, chunkSourceFactory, period.adaptationSets, eventStreams);
trackGroups = result.first;
trackGroupInfos = result.second;
}
@ -505,7 +503,7 @@ import java.util.regex.Pattern;
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
DrmSessionManager drmSessionManager,
@Nullable SubtitleParser.Factory subtitleParserFactory,
DashChunkSource.Factory chunkSourceFactory,
List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams) {
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
@ -528,7 +526,7 @@ import java.util.regex.Pattern;
int trackGroupCount =
buildPrimaryAndEmbeddedTrackGroupInfos(
drmSessionManager,
subtitleParserFactory,
chunkSourceFactory,
adaptationSets,
groupedAdaptationSetIndices,
primaryGroupCount,
@ -668,7 +666,7 @@ import java.util.regex.Pattern;
private static int buildPrimaryAndEmbeddedTrackGroupInfos(
DrmSessionManager drmSessionManager,
@Nullable SubtitleParser.Factory subtitleParserFactory,
DashChunkSource.Factory chunkSourceFactory,
List<AdaptationSet> adaptationSets,
int[][] groupedAdaptationSetIndices,
int primaryGroupCount,
@ -704,7 +702,7 @@ import java.util.regex.Pattern;
int closedCaptionTrackGroupIndex =
primaryGroupClosedCaptionTrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;
maybeUpdateFormatsForParsedText(subtitleParserFactory, formats);
maybeUpdateFormatsForParsedText(chunkSourceFactory, formats);
trackGroups[primaryTrackGroupIndex] = new TrackGroup(trackGroupId, formats);
trackGroupInfos[primaryTrackGroupIndex] =
TrackGroupInfo.primaryTrack(
@ -732,7 +730,7 @@ import java.util.regex.Pattern;
primaryTrackGroupIndex,
ImmutableList.copyOf(primaryGroupClosedCaptionTrackFormats[i]));
maybeUpdateFormatsForParsedText(
subtitleParserFactory, primaryGroupClosedCaptionTrackFormats[i]);
chunkSourceFactory, primaryGroupClosedCaptionTrackFormats[i]);
trackGroups[closedCaptionTrackGroupIndex] =
new TrackGroup(closedCaptionTrackGroupId, primaryGroupClosedCaptionTrackFormats[i]);
}
@ -928,22 +926,9 @@ import java.util.regex.Pattern;
* during extraction.
*/
private static void maybeUpdateFormatsForParsedText(
SubtitleParser.Factory subtitleParserFactory, Format[] formats) {
DashChunkSource.Factory chunkSourceFactory, Format[] formats) {
for (int i = 0; i < formats.length; i++) {
if (subtitleParserFactory == null || !subtitleParserFactory.supportsFormat(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();
formats[i] = chunkSourceFactory.getOutputTextFormat(formats[i]);
}
}

View File

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

View File

@ -166,6 +166,17 @@ public class DefaultDashChunkSource implements DashChunkSource {
playerId,
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;

View File

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

View File

@ -169,6 +169,35 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
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(
@FileTypes.Type int fileType, List<Integer> fileTypes) {
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 androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.analytics.PlayerId;
@ -60,4 +62,24 @@ public interface HlsExtractorFactory {
ExtractorInput sniffingExtractorInput,
PlayerId playerId)
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.LoadErrorHandlingPolicy;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.util.ArrayList;
@ -86,7 +85,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final PlayerId playerId;
private final HlsSampleStreamWrapper.Callback sampleStreamWrapperCallback;
private final long timestampAdjusterInitializationTimeoutMs;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
@Nullable private MediaPeriod.Callback mediaPeriodCallback;
private int pendingPrepareCount;
@ -141,8 +139,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@HlsMediaSource.MetadataType int metadataType,
boolean useSessionKeys,
PlayerId playerId,
long timestampAdjusterInitializationTimeoutMs,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
long timestampAdjusterInitializationTimeoutMs) {
this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory;
@ -167,7 +164,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleStreamWrappers = new HlsSampleStreamWrapper[0];
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
manifestUrlIndicesPerWrapper = new int[0][];
this.subtitleParserFactory = subtitleParserFactory;
}
public void release() {
@ -538,7 +534,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleStreamWrapper.prepareWithMultivariantPlaylistInfo(
new TrackGroup[] {
new TrackGroup(
sampleStreamWrapperUid, maybeUpdateFormatForParsedText(originalSubtitleFormat))
sampleStreamWrapperUid,
extractorFactory.getOutputTextFormat(originalSubtitleFormat))
},
/* primaryTrackGroupIndex= */ 0);
}
@ -686,7 +683,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < ccFormats.size(); i++) {
String ccId = sampleStreamWrapperUid + ":cc:" + i;
muxedTrackGroups.add(
new TrackGroup(ccId, maybeUpdateFormatForParsedText(ccFormats.get(i))));
new TrackGroup(ccId, extractorFactory.getOutputTextFormat(ccFormats.get(i))));
}
}
} else /* numberOfAudioCodecs > 0 */ {
@ -910,23 +907,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.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 {
@Override
public void onPrepared() {

View File

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

View File

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

View File

@ -23,6 +23,7 @@ import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.UriUtil;
@ -82,6 +83,31 @@ public class DefaultSsChunkSource implements SsChunkSource {
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
public SsChunkSource createChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower,

View File

@ -16,6 +16,9 @@
package androidx.media3.exoplayer.smoothstreaming;
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.datasource.TransferListener;
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.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.Extractor;
/** A {@link ChunkSource} for SmoothStreaming. */
@UnstableApi
@ -50,6 +54,26 @@ public interface SsChunkSource extends ChunkSource {
ExoTrackSelection trackSelection,
@Nullable TransferListener transferListener,
@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.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.TrackGroup;
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.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -79,8 +77,7 @@ import java.util.List;
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
LoaderErrorThrower manifestLoaderErrorThrower,
Allocator allocator,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
Allocator allocator) {
this.manifest = manifest;
this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener;
@ -92,7 +89,7 @@ import java.util.List;
this.mediaSourceEventDispatcher = mediaSourceEventDispatcher;
this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
trackGroups = buildTrackGroups(manifest, drmSessionManager, subtitleParserFactory);
trackGroups = buildTrackGroups(manifest, drmSessionManager, chunkSourceFactory);
sampleStreams = newSampleStreamArray(0);
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
@ -270,30 +267,19 @@ import java.util.List;
private static TrackGroupArray buildTrackGroups(
SsManifest manifest,
DrmSessionManager drmSessionManager,
@Nullable SubtitleParser.Factory subtitleParserFactory) {
SsChunkSource.Factory chunkSourceFactory) {
TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length];
for (int i = 0; i < manifest.streamElements.length; i++) {
Format[] manifestFormats = manifest.streamElements[i].formats;
Format[] exposedFormats = new Format[manifestFormats.length];
for (int j = 0; j < manifestFormats.length; j++) {
Format manifestFormat = manifestFormats[j];
Format.Builder updatedFormat =
Format updatedFormatWithDrm =
manifestFormat
.buildUpon()
.setCryptoType(drmSessionManager.getCryptoType(manifestFormat));
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(manifestFormat)) {
updatedFormat
.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();
.setCryptoType(drmSessionManager.getCryptoType(manifestFormat))
.build();
exposedFormats[j] = chunkSourceFactory.getOutputTextFormat(updatedFormatWithDrm);
}
trackGroups[i] = new TrackGroup(/* id= */ Integer.toString(i), exposedFormats);
}

View File

@ -305,7 +305,6 @@ public final class SsMediaSource extends BaseMediaSource
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
subtitleParserFactory,
livePresentationDelayMs);
}
@ -343,7 +342,6 @@ public final class SsMediaSource extends BaseMediaSource
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
subtitleParserFactory,
livePresentationDelayMs);
}
@ -387,7 +385,6 @@ public final class SsMediaSource extends BaseMediaSource
private long manifestLoadStartTimestamp;
private SsManifest manifest;
private Handler manifestRefreshHandler;
@Nullable private final SubtitleParser.Factory subtitleParserFactory;
@GuardedBy("this")
private MediaItem mediaItem;
@ -402,7 +399,6 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@Nullable SubtitleParser.Factory subtitleParserFactory,
long livePresentationDelayMs) {
Assertions.checkState(manifest == null || !manifest.isLive);
this.mediaItem = mediaItem;
@ -419,7 +415,6 @@ public final class SsMediaSource extends BaseMediaSource
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.subtitleParserFactory = subtitleParserFactory;
this.livePresentationDelayMs = livePresentationDelayMs;
this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
sideloadedManifest = manifest != null;
@ -487,8 +482,7 @@ public final class SsMediaSource extends BaseMediaSource
loadErrorHandlingPolicy,
mediaSourceEventDispatcher,
manifestLoaderErrorThrower,
allocator,
subtitleParserFactory);
allocator);
mediaPeriods.add(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.createStreamElement;
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.when;
import androidx.media3.common.C;
import androidx.media3.common.Format;
@ -63,24 +65,13 @@ public class SsMediaPeriodTest {
createAudioFormat(/* bitrate= */ 96000)),
createStreamElement(
/* name= */ "text", C.TRACK_TYPE_TEXT, createTextFormat(/* language= */ "eng")));
SsChunkSource.Factory chunkSourceFactory = mock(SsChunkSource.Factory.class);
when(chunkSourceFactory.getOutputTextFormat(any())).thenCallRealMethod();
FilterableManifestMediaPeriodFactory<SsManifest> mediaPeriodFactory =
(manifest, periodIndex) -> {
MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object());
return new SsMediaPeriod(
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);
return createSsMediaPeriod(manifest, mediaPeriodId, chunkSourceFactory);
};
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
@ -127,27 +118,34 @@ public class SsMediaPeriodTest {
new FakeTimeline(/* windowCount= */ 2).getUidOfPeriod(/* periodIndex= */ 0),
/* windowSequenceNumber= */ 0);
SsMediaPeriod period =
new SsMediaPeriod(
testManifest,
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);
SsChunkSource.Factory chunkSourceFactory = mock(SsChunkSource.Factory.class);
// Default implementation of SsChunkSource.Factory.getOutputTextFormat doesn't transcode
// DefaultSsChunkSource.Factory is final (not mockable) and has a null SubtitleParser.Factory
when(chunkSourceFactory.getOutputTextFormat(any())).thenReturn(expectedSubtitleFormat);
SsMediaPeriod period = createSsMediaPeriod(testManifest, mediaPeriodId, chunkSourceFactory);
Format subtitleFormat = period.getTrackGroups().get(2).getFormat(0);
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) {
return new Format.Builder()
.setContainerMimeType(MimeTypes.VIDEO_MP4)