mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add experimental opt-in to parse SS subtitles during extraction
PiperOrigin-RevId: 586331888
This commit is contained in:
parent
9add30e582
commit
8a8b875c72
@ -92,6 +92,9 @@
|
|||||||
* Parse "f800" as channel count of 5 for Dolby in DASH manifest
|
* Parse "f800" as channel count of 5 for Dolby in DASH manifest
|
||||||
([#688](https://github.com/androidx/media/issues/688)).
|
([#688](https://github.com/androidx/media/issues/688)).
|
||||||
* Smooth Streaming Extension:
|
* Smooth Streaming Extension:
|
||||||
|
* Add experimental support for parsing subtitles during extraction. You
|
||||||
|
can enable this using
|
||||||
|
`SsMediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`.
|
||||||
* RTSP Extension:
|
* RTSP Extension:
|
||||||
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||||
* MIDI decoder: Ignore SysEx event messages
|
* MIDI decoder: Ignore SysEx event messages
|
||||||
|
@ -47,9 +47,12 @@ import androidx.media3.exoplayer.upstream.CmcdData;
|
|||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection;
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
|
import androidx.media3.extractor.Extractor;
|
||||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||||
import androidx.media3.extractor.mp4.Track;
|
import androidx.media3.extractor.mp4.Track;
|
||||||
import androidx.media3.extractor.mp4.TrackEncryptionBox;
|
import androidx.media3.extractor.mp4.TrackEncryptionBox;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
|
import androidx.media3.extractor.text.SubtitleTranscodingExtractor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -60,11 +63,24 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
public static final class Factory implements SsChunkSource.Factory {
|
public static final class Factory implements SsChunkSource.Factory {
|
||||||
|
|
||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
@Nullable private SubtitleParser.Factory subtitleParserFactory;
|
||||||
|
|
||||||
public Factory(DataSource.Factory dataSourceFactory) {
|
public Factory(DataSource.Factory dataSourceFactory) {
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SubtitleParser.Factory} to be used for parsing subtitles during extraction,
|
||||||
|
* or {@code null} to parse subtitles during decoding.
|
||||||
|
*
|
||||||
|
* <p>This may only be used with {@link BundledChunkExtractor.Factory}.
|
||||||
|
*/
|
||||||
|
/* package */ Factory setSubtitleParserFactory(
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory) {
|
||||||
|
this.subtitleParserFactory = subtitleParserFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SsChunkSource createChunkSource(
|
public SsChunkSource createChunkSource(
|
||||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
@ -83,7 +99,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
streamElementIndex,
|
streamElementIndex,
|
||||||
trackSelection,
|
trackSelection,
|
||||||
dataSource,
|
dataSource,
|
||||||
cmcdConfiguration);
|
cmcdConfiguration,
|
||||||
|
subtitleParserFactory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +129,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
* @param trackSelection The track selection.
|
* @param trackSelection The track selection.
|
||||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||||
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
|
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
|
||||||
|
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
|
||||||
|
* extraction.
|
||||||
*/
|
*/
|
||||||
public DefaultSsChunkSource(
|
public DefaultSsChunkSource(
|
||||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
@ -119,7 +138,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
int streamElementIndex,
|
int streamElementIndex,
|
||||||
ExoTrackSelection trackSelection,
|
ExoTrackSelection trackSelection,
|
||||||
DataSource dataSource,
|
DataSource dataSource,
|
||||||
@Nullable CmcdConfiguration cmcdConfiguration) {
|
@Nullable CmcdConfiguration cmcdConfiguration,
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory) {
|
||||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.streamElementIndex = streamElementIndex;
|
this.streamElementIndex = streamElementIndex;
|
||||||
@ -152,12 +172,15 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
nalUnitLengthFieldLength,
|
nalUnitLengthFieldLength,
|
||||||
null,
|
null,
|
||||||
null);
|
null);
|
||||||
FragmentedMp4Extractor extractor =
|
Extractor extractor =
|
||||||
new FragmentedMp4Extractor(
|
new FragmentedMp4Extractor(
|
||||||
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
|
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
|
||||||
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX,
|
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX,
|
||||||
/* timestampAdjuster= */ null,
|
/* timestampAdjuster= */ null,
|
||||||
track);
|
track);
|
||||||
|
if (subtitleParserFactory != null) {
|
||||||
|
extractor = new SubtitleTranscodingExtractor(extractor, subtitleParserFactory);
|
||||||
|
}
|
||||||
chunkExtractors[i] = new BundledChunkExtractor(extractor, streamElement.type, format);
|
chunkExtractors[i] = new BundledChunkExtractor(extractor, streamElement.type, format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ 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;
|
||||||
@ -41,6 +42,7 @@ 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;
|
||||||
@ -77,7 +79,8 @@ 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;
|
||||||
@ -89,7 +92,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);
|
trackGroups = buildTrackGroups(manifest, drmSessionManager, subtitleParserFactory);
|
||||||
sampleStreams = newSampleStreamArray(0);
|
sampleStreams = newSampleStreamArray(0);
|
||||||
compositeSequenceableLoader =
|
compositeSequenceableLoader =
|
||||||
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
|
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
|
||||||
@ -265,15 +268,32 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static TrackGroupArray buildTrackGroups(
|
private static TrackGroupArray buildTrackGroups(
|
||||||
SsManifest manifest, DrmSessionManager drmSessionManager) {
|
SsManifest manifest,
|
||||||
|
DrmSessionManager drmSessionManager,
|
||||||
|
@Nullable SubtitleParser.Factory subtitleParserFactory) {
|
||||||
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];
|
||||||
exposedFormats[j] =
|
Format.Builder updatedFormat =
|
||||||
manifestFormat.copyWithCryptoType(drmSessionManager.getCryptoType(manifestFormat));
|
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();
|
||||||
}
|
}
|
||||||
trackGroups[i] = new TrackGroup(/* id= */ Integer.toString(i), exposedFormats);
|
trackGroups[i] = new TrackGroup(/* id= */ Integer.toString(i), exposedFormats);
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,8 @@ import androidx.media3.exoplayer.upstream.Loader;
|
|||||||
import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction;
|
import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction;
|
||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||||
|
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -91,6 +93,7 @@ public final class SsMediaSource extends BaseMediaSource
|
|||||||
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
|
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
|
||||||
private DrmSessionManagerProvider drmSessionManagerProvider;
|
private DrmSessionManagerProvider drmSessionManagerProvider;
|
||||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
|
@Nullable private SubtitleParser.Factory subtitleParserFactory;
|
||||||
private long livePresentationDelayMs;
|
private long livePresentationDelayMs;
|
||||||
@Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;
|
@Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;
|
||||||
|
|
||||||
@ -152,6 +155,40 @@ public final class SsMediaSource extends BaseMediaSource
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether subtitles should be parsed as part of extraction (before being added to the
|
||||||
|
* sample queue) or as part of rendering (when being taken from the sample queue). Defaults to
|
||||||
|
* {@code false} (i.e. subtitles will be parsed as part of rendering).
|
||||||
|
*
|
||||||
|
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
|
||||||
|
* in a future release.
|
||||||
|
*
|
||||||
|
* <p>This method may only be used with {@link DefaultSsChunkSource.Factory}.
|
||||||
|
*
|
||||||
|
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
|
||||||
|
* rendering.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
// TODO: b/289916598 - Flip the default of this to true (probably wired up to a single method on
|
||||||
|
// DefaultMediaSourceFactory via the MediaSource.Factory interface).
|
||||||
|
public Factory experimentalParseSubtitlesDuringExtraction(
|
||||||
|
boolean parseSubtitlesDuringExtraction) {
|
||||||
|
if (parseSubtitlesDuringExtraction) {
|
||||||
|
if (subtitleParserFactory == null) {
|
||||||
|
this.subtitleParserFactory = new DefaultSubtitleParserFactory();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.subtitleParserFactory = null;
|
||||||
|
}
|
||||||
|
if (chunkSourceFactory instanceof DefaultSsChunkSource.Factory) {
|
||||||
|
((DefaultSsChunkSource.Factory) chunkSourceFactory)
|
||||||
|
.setSubtitleParserFactory(subtitleParserFactory);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the duration in milliseconds by which the default start position should precede the end
|
* Sets the duration in milliseconds by which the default start position should precede the end
|
||||||
* of the live window for live playbacks. The default value is {@link
|
* of the live window for live playbacks. The default value is {@link
|
||||||
@ -273,6 +310,7 @@ public final class SsMediaSource extends BaseMediaSource
|
|||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
drmSessionManagerProvider.get(mediaItem),
|
drmSessionManagerProvider.get(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
|
subtitleParserFactory,
|
||||||
livePresentationDelayMs);
|
livePresentationDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,6 +348,7 @@ public final class SsMediaSource extends BaseMediaSource
|
|||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
drmSessionManagerProvider.get(mediaItem),
|
drmSessionManagerProvider.get(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
|
subtitleParserFactory,
|
||||||
livePresentationDelayMs);
|
livePresentationDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,6 +392,7 @@ 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;
|
||||||
@ -367,6 +407,7 @@ 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;
|
||||||
@ -383,6 +424,7 @@ 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;
|
||||||
@ -450,7 +492,8 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -297,6 +297,7 @@ public class DefaultSsChunkSourceTest {
|
|||||||
/* streamElementIndex= */ 0,
|
/* streamElementIndex= */ 0,
|
||||||
adaptiveTrackSelection,
|
adaptiveTrackSelection,
|
||||||
new FakeDataSource(),
|
new FakeDataSource(),
|
||||||
cmcdConfiguration);
|
cmcdConfiguration,
|
||||||
|
/* subtitleParserFactory= */ null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ 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 org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -32,6 +33,9 @@ import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
|||||||
import androidx.media3.exoplayer.upstream.Allocator;
|
import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
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.DefaultSubtitleParserFactory;
|
||||||
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
|
import androidx.media3.test.utils.FakeTimeline;
|
||||||
import androidx.media3.test.utils.MediaPeriodAsserts;
|
import androidx.media3.test.utils.MediaPeriodAsserts;
|
||||||
import androidx.media3.test.utils.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;
|
import androidx.media3.test.utils.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
@ -75,13 +79,75 @@ 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= */ null);
|
||||||
};
|
};
|
||||||
|
|
||||||
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
|
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
|
||||||
mediaPeriodFactory, testManifest);
|
mediaPeriodFactory, testManifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getTrackGroups_withSubtitleParserFactory_matchesFormat() {
|
||||||
|
SubtitleParser.Factory subtitleParserFactory = new DefaultSubtitleParserFactory();
|
||||||
|
|
||||||
|
Format originalSubtitleFormat =
|
||||||
|
new Format.Builder()
|
||||||
|
.setContainerMimeType(MimeTypes.APPLICATION_MP4)
|
||||||
|
.setSampleMimeType(MimeTypes.TEXT_VTT)
|
||||||
|
.setLanguage("eng")
|
||||||
|
.build();
|
||||||
|
Format expectedSubtitleFormat =
|
||||||
|
new Format.Builder()
|
||||||
|
.setContainerMimeType(originalSubtitleFormat.containerMimeType)
|
||||||
|
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
|
||||||
|
.setCodecs(originalSubtitleFormat.sampleMimeType)
|
||||||
|
.setCueReplacementBehavior(
|
||||||
|
subtitleParserFactory.getCueReplacementBehavior(originalSubtitleFormat))
|
||||||
|
.setLanguage(originalSubtitleFormat.language)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
SsManifest testManifest =
|
||||||
|
createSsManifest(
|
||||||
|
createStreamElement(
|
||||||
|
/* name= */ "video",
|
||||||
|
C.TRACK_TYPE_VIDEO,
|
||||||
|
createVideoFormat(/* bitrate= */ 200000),
|
||||||
|
createVideoFormat(/* bitrate= */ 400000),
|
||||||
|
createVideoFormat(/* bitrate= */ 800000)),
|
||||||
|
createStreamElement(
|
||||||
|
/* name= */ "audio",
|
||||||
|
C.TRACK_TYPE_AUDIO,
|
||||||
|
createAudioFormat(/* bitrate= */ 48000),
|
||||||
|
createAudioFormat(/* bitrate= */ 96000)),
|
||||||
|
createStreamElement(/* name= */ "text", C.TRACK_TYPE_TEXT, originalSubtitleFormat));
|
||||||
|
|
||||||
|
MediaPeriodId mediaPeriodId =
|
||||||
|
new MediaPeriodId(
|
||||||
|
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);
|
||||||
|
|
||||||
|
Format subtitleFormat = period.getTrackGroups().get(2).getFormat(0);
|
||||||
|
assertThat(subtitleFormat).isEqualTo(expectedSubtitleFormat);
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -16,16 +16,24 @@
|
|||||||
package androidx.media3.exoplayer.smoothstreaming;
|
package androidx.media3.exoplayer.smoothstreaming;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.StreamKey;
|
import androidx.media3.common.StreamKey;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.datasource.ByteArrayDataSource;
|
import androidx.media3.datasource.ByteArrayDataSource;
|
||||||
import androidx.media3.datasource.DataSource;
|
import androidx.media3.datasource.DataSource;
|
||||||
|
import androidx.media3.datasource.TransferListener;
|
||||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
|
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
||||||
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
|
import androidx.media3.test.utils.FakeDataSource;
|
||||||
import androidx.media3.test.utils.TestUtil;
|
import androidx.media3.test.utils.TestUtil;
|
||||||
import androidx.media3.test.utils.robolectric.RobolectricUtil;
|
import androidx.media3.test.utils.robolectric.RobolectricUtil;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
@ -94,6 +102,27 @@ public class SsMediaSourceTest {
|
|||||||
assertThat(canUpdateMediaItem).isFalse();
|
assertThat(canUpdateMediaItem).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
setExperimentalParseSubtitlesDuringExtraction_withNonDefaultChunkSourceFactory_setThrows() {
|
||||||
|
SsMediaSource.Factory ssMediaSourceFactory =
|
||||||
|
new SsMediaSource.Factory(
|
||||||
|
/* chunkSourceFactory= */ this::createSampleSsChunkSource,
|
||||||
|
/* manifestDataSourceFactory= */ () -> createSampleDataSource(SAMPLE_MANIFEST));
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() -> ssMediaSourceFactory.experimentalParseSubtitlesDuringExtraction(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
setExperimentalParseSubtitlesDuringExtraction_withDefaultChunkSourceFactory_setSucceeds() {
|
||||||
|
SsMediaSource.Factory ssMediaSourceFactory =
|
||||||
|
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST));
|
||||||
|
ssMediaSourceFactory.experimentalParseSubtitlesDuringExtraction(false);
|
||||||
|
ssMediaSourceFactory.experimentalParseSubtitlesDuringExtraction(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void canUpdateMediaItem_withChangedStreamKeys_returnsFalse() {
|
public void canUpdateMediaItem_withChangedStreamKeys_returnsFalse() {
|
||||||
MediaItem initialMediaItem =
|
MediaItem initialMediaItem =
|
||||||
@ -175,4 +204,21 @@ public class SsMediaSourceTest {
|
|||||||
}
|
}
|
||||||
return new ByteArrayDataSource(manifestData);
|
return new ByteArrayDataSource(manifestData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SsChunkSource createSampleSsChunkSource(
|
||||||
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
|
SsManifest manifest,
|
||||||
|
int streamElementIndex,
|
||||||
|
ExoTrackSelection trackSelection,
|
||||||
|
@Nullable TransferListener transferListener,
|
||||||
|
@Nullable CmcdConfiguration cmcdConfiguration) {
|
||||||
|
return new DefaultSsChunkSource(
|
||||||
|
manifestLoaderErrorThrower,
|
||||||
|
manifest,
|
||||||
|
streamElementIndex,
|
||||||
|
trackSelection,
|
||||||
|
new FakeDataSource(),
|
||||||
|
cmcdConfiguration,
|
||||||
|
/* subtitleParserFactory= */ null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user