From da402cfd64ef3ae60607491730f01138177a077b Mon Sep 17 00:00:00 2001 From: dancho Date: Tue, 25 Feb 2025 07:29:54 -0800 Subject: [PATCH] Add experimentalSetCodecsToParseWithinGopSampleDependencies to HLS HLS extractors were missing experimentalSetCodecsToParseWithinGopSampleDependencies prior to this patch. PiperOrigin-RevId: 730878460 --- demos/main/src/main/assets/media.exolist.json | 4 + .../hls/DefaultHlsExtractorFactory.java | 19 +- .../exoplayer/hls/HlsExtractorFactory.java | 20 ++ .../media3/exoplayer/hls/HlsMediaSource.java | 11 + .../hls/e2etest/HlsPlaybackTest.java | 35 +++ .../hls/standalone-webvtt-optimized-seek.dump | 219 ++++++++++++++++++ 6 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 libraries/test_data/src/test/assets/playbackdumps/hls/standalone-webvtt-optimized-seek.dump diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index d3a7e2a953..5351c6f8b2 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -257,6 +257,10 @@ { "name": "Apple media playlist (AAC)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" + }, + { + "name": "Bitmovin (FMP4)", + "uri": "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s-fmp4/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8" } ] }, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java index 6b9048ead2..522aa83f05 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/DefaultHlsExtractorFactory.java @@ -22,6 +22,7 @@ import android.annotation.SuppressLint; import android.net.Uri; import android.text.TextUtils; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.FileTypes; import androidx.media3.common.Format; import androidx.media3.common.Metadata; @@ -71,6 +72,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { private SubtitleParser.Factory subtitleParserFactory; private boolean parseSubtitlesDuringExtraction; + private @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies; private final boolean exposeCea608WhenMissingDeclarations; @@ -179,6 +181,14 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { return this; } + @CanIgnoreReturnValue + @Override + public DefaultHlsExtractorFactory experimentalSetCodecsToParseWithinGopSampleDependencies( + @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) { + this.codecsToParseWithinGopSampleDependencies = codecsToParseWithinGopSampleDependencies; + return this; + } + /** * {@inheritDoc} * @@ -242,7 +252,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { parseSubtitlesDuringExtraction, timestampAdjuster, format, - muxedCaptionFormats); + muxedCaptionFormats, + codecsToParseWithinGopSampleDependencies); case FileTypes.TS: return createTsExtractor( payloadReaderFactoryFlags, @@ -313,7 +324,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { boolean parseSubtitlesDuringExtraction, TimestampAdjuster timestampAdjuster, Format format, - @Nullable List muxedCaptionFormats) { + @Nullable List muxedCaptionFormats, + @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) { // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid // creating a separate EMSG track for every audio track in a video stream. @FragmentedMp4Extractor.Flags @@ -322,6 +334,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { subtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED; flags |= FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA; } + flags |= + FragmentedMp4Extractor.codecsToParseWithinGopSampleDependenciesAsFlags( + codecsToParseWithinGopSampleDependencies); return new FragmentedMp4Extractor( subtitleParserFactory, flags, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsExtractorFactory.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsExtractorFactory.java index 7e301fe2f9..f51def1b24 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsExtractorFactory.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsExtractorFactory.java @@ -26,6 +26,7 @@ import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.PositionHolder; +import androidx.media3.extractor.mp4.FragmentedMp4Extractor; import androidx.media3.extractor.text.SubtitleParser; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; @@ -101,6 +102,25 @@ public interface HlsExtractorFactory { return this; } + /** + * Sets the set of video codecs for which within GOP sample dependency information should be + * parsed as part of extraction. Defaults to {@code 0} - empty set of codecs. + * + *

Having access to additional sample dependency information can speed up seeking. See {@link + * FragmentedMp4Extractor#FLAG_READ_WITHIN_GOP_SAMPLE_DEPENDENCIES}. + * + *

This method is experimental and will be renamed or removed in a future release. + * + * @param codecsToParseWithinGopSampleDependencies The set of codecs for which to parse within GOP + * sample dependency information. + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + default HlsExtractorFactory experimentalSetCodecsToParseWithinGopSampleDependencies( + @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) { + return this; + } + /** * Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples} which * were originally in {@code sourceFormat}. diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java index 89d02dd245..cd05e9252b 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java @@ -109,6 +109,7 @@ public final class HlsMediaSource extends BaseMediaSource @Nullable private HlsExtractorFactory extractorFactory; @Nullable private SubtitleParser.Factory subtitleParserFactoryOverride; private boolean parseSubtitlesDuringExtraction; + private @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies; private HlsPlaylistParserFactory playlistParserFactory; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -220,6 +221,14 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Override + @CanIgnoreReturnValue + public Factory experimentalSetCodecsToParseWithinGopSampleDependencies( + @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) { + this.codecsToParseWithinGopSampleDependencies = codecsToParseWithinGopSampleDependencies; + return this; + } + /** * Sets the factory from which playlist parsers will be obtained. * @@ -396,6 +405,8 @@ public final class HlsMediaSource extends BaseMediaSource extractorFactory.setSubtitleParserFactory(subtitleParserFactoryOverride); } extractorFactory.experimentalParseSubtitlesDuringExtraction(parseSubtitlesDuringExtraction); + extractorFactory.experimentalSetCodecsToParseWithinGopSampleDependencies( + codecsToParseWithinGopSampleDependencies); HlsExtractorFactory extractorFactory = this.extractorFactory; HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory; List streamKeys = mediaItem.localConfiguration.streamKeys; diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java index 3f9c881eb9..575526c01d 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/e2etest/HlsPlaybackTest.java @@ -29,6 +29,7 @@ import android.graphics.SurfaceTexture; import android.net.Uri; import android.view.Surface; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.datasource.DefaultDataSource; @@ -42,6 +43,7 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.upstream.CmcdConfiguration; +import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.test.utils.CapturingRenderersFactory; import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.FakeClock; @@ -464,6 +466,39 @@ public final class HlsPlaybackTest { assertThat(loadCompletedDataSpecUris).containsExactlyElementsIn(loadStartedUris); } + @Test + public void playVideo_usingWithinGopSampleDependencies_withSeek() throws Exception { + Context applicationContext = ApplicationProvider.getApplicationContext(); + CapturingRenderersFactory capturingRenderersFactory = + new CapturingRenderersFactory(applicationContext); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory(applicationContext, new DefaultExtractorsFactory()); + defaultMediaSourceFactory.experimentalSetCodecsToParseWithinGopSampleDependencies( + C.VIDEO_CODEC_FLAG_H264); + ExoPlayer player = + new ExoPlayer.Builder( + applicationContext, capturingRenderersFactory, defaultMediaSourceFactory) + .setClock(new FakeClock(/* isAutoAdvancing= */ true)) + .build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); + + player.setMediaItem( + MediaItem.fromUri("asset:///media/hls/standalone-webvtt/multivariant_playlist.m3u8")); + player.seekTo(500L); + player.prepare(); + player.play(); + advance(player).untilState(Player.STATE_ENDED); + player.release(); + surface.release(); + + DumpFileAsserts.assertOutput( + applicationContext, + playbackOutput, + "playbackdumps/hls/standalone-webvtt-optimized-seek.dump"); + } + private static class AnalyticsListenerImpl implements AnalyticsListener { @Nullable private LoadEventInfo loadErrorEventInfo; diff --git a/libraries/test_data/src/test/assets/playbackdumps/hls/standalone-webvtt-optimized-seek.dump b/libraries/test_data/src/test/assets/playbackdumps/hls/standalone-webvtt-optimized-seek.dump new file mode 100644 index 0000000000..fab9cc4a23 --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/hls/standalone-webvtt-optimized-seek.dump @@ -0,0 +1,219 @@ +MediaCodecAdapter (exotest.video.avc): + inputBuffers: + count = 24 + input buffer #0: + timeUs = 1000000000000 + contents = length 36692, hash D216076E + input buffer #1: + timeUs = 1000000066733 + contents = length 5312, hash D45D3CA0 + input buffer #2: + timeUs = 1000000200200 + contents = length 7735, hash 4490F110 + input buffer #3: + timeUs = 1000000133466 + contents = length 987, hash 560B5036 + input buffer #4: + timeUs = 1000000333666 + contents = length 6061, hash 736C72B2 + input buffer #5: + timeUs = 1000000266933 + contents = length 992, hash FE132F23 + input buffer #6: + timeUs = 1000000433766 + contents = length 4899, hash F72F86A1 + input buffer #7: + timeUs = 1000000400400 + contents = length 568, hash 519A8E50 + input buffer #8: + timeUs = 1000000567233 + contents = length 5450, hash F06EC4AA + input buffer #9: + timeUs = 1000000500500 + contents = length 1051, hash 92DFA63A + input buffer #10: + timeUs = 1000000533866 + contents = length 781, hash 36BE495B + input buffer #11: + timeUs = 1000000700700 + contents = length 4725, hash AC0C8CD3 + input buffer #12: + timeUs = 1000000633966 + contents = length 1022, hash 5D8BFF34 + input buffer #13: + timeUs = 1000000600600 + contents = length 790, hash 99413A99 + input buffer #14: + timeUs = 1000000667333 + contents = length 610, hash 5E129290 + input buffer #15: + timeUs = 1000000834166 + contents = length 2751, hash 769974CB + input buffer #16: + timeUs = 1000000767433 + contents = length 745, hash B78A477A + input buffer #17: + timeUs = 1000000734066 + contents = length 621, hash CF741E7A + input buffer #18: + timeUs = 1000000800800 + contents = length 505, hash 1DB4894E + input buffer #19: + timeUs = 1000000967633 + contents = length 1268, hash C15348DC + input buffer #20: + timeUs = 1000000900900 + contents = length 880, hash C2DE85D0 + input buffer #21: + timeUs = 1000000867533 + contents = length 530, hash C98BC6A8 + input buffer #22: + timeUs = 1000000934266 + contents = length 568, hash 4FE5C8EA + input buffer #23: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 23 + output buffer #0: + timeUs = 1000000000000 + size = 36692 + rendered = false + output buffer #1: + timeUs = 1000000066733 + size = 5312 + rendered = false + output buffer #2: + timeUs = 1000000200200 + size = 7735 + rendered = false + output buffer #3: + timeUs = 1000000133466 + size = 987 + rendered = false + output buffer #4: + timeUs = 1000000333666 + size = 6061 + rendered = false + output buffer #5: + timeUs = 1000000266933 + size = 992 + rendered = false + output buffer #6: + timeUs = 1000000433766 + size = 4899 + rendered = false + output buffer #7: + timeUs = 1000000400400 + size = 568 + rendered = false + output buffer #8: + timeUs = 1000000567233 + size = 5450 + rendered = true + output buffer #9: + timeUs = 1000000500500 + size = 1051 + rendered = true + output buffer #10: + timeUs = 1000000533866 + size = 781 + rendered = true + output buffer #11: + timeUs = 1000000700700 + size = 4725 + rendered = true + output buffer #12: + timeUs = 1000000633966 + size = 1022 + rendered = true + output buffer #13: + timeUs = 1000000600600 + size = 790 + rendered = true + output buffer #14: + timeUs = 1000000667333 + size = 610 + rendered = true + output buffer #15: + timeUs = 1000000834166 + size = 2751 + rendered = true + output buffer #16: + timeUs = 1000000767433 + size = 745 + rendered = true + output buffer #17: + timeUs = 1000000734066 + size = 621 + rendered = true + output buffer #18: + timeUs = 1000000800800 + size = 505 + rendered = true + output buffer #19: + timeUs = 1000000967633 + size = 1268 + rendered = true + output buffer #20: + timeUs = 1000000900900 + size = 880 + rendered = true + output buffer #21: + timeUs = 1000000867533 + size = 530 + rendered = true + output buffer #22: + timeUs = 1000000934266 + size = 568 + rendered = true +TextOutput: + Subtitle[0]: + presentationTimeUs = 500000 + Cues = [] + Subtitle[1]: + presentationTimeUs = 567000 + Cue[0]: + text = This is the first subtitle. + textAlignment = ALIGN_CENTER + line = -1.0 + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[2]: + presentationTimeUs = 667000 + Cue[0]: + text = This is the first subtitle. + textAlignment = ALIGN_CENTER + line = -1.0 + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Cue[1]: + text = This is the second subtitle. + textAlignment = ALIGN_CENTER + line = -2.0 + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[3]: + presentationTimeUs = 801000 + Cue[0]: + text = This is the second subtitle. + textAlignment = ALIGN_CENTER + line = -1.0 + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[4]: + presentationTimeUs = 951000 + Cues = []