mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
f8dbbc82e2
commit
966b710897
@ -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(
|
||||||
|
@ -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. */
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,10 +118,21 @@ 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);
|
||||||
|
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(TransferListener.class),
|
||||||
mock(CompositeSequenceableLoaderFactory.class),
|
mock(CompositeSequenceableLoaderFactory.class),
|
||||||
/* cmcdConfiguration= */ null,
|
/* cmcdConfiguration= */ null,
|
||||||
@ -141,11 +143,7 @@ public class SsMediaPeriodTest {
|
|||||||
new MediaSourceEventListener.EventDispatcher()
|
new MediaSourceEventListener.EventDispatcher()
|
||||||
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
|
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
|
||||||
mock(LoaderErrorThrower.class),
|
mock(LoaderErrorThrower.class),
|
||||||
mock(Allocator.class),
|
mock(Allocator.class));
|
||||||
subtitleParserFactory);
|
|
||||||
|
|
||||||
Format subtitleFormat = period.getTrackGroups().get(2).getFormat(0);
|
|
||||||
assertThat(subtitleFormat).isEqualTo(expectedSubtitleFormat);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Format createVideoFormat(int bitrate) {
|
private static Format createVideoFormat(int bitrate) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user