From 0352db9a375cff16b14e3e8a653e30dcfa589b74 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 16 May 2024 01:40:20 -0700 Subject: [PATCH] Default to parse subtitles while extracting, instead of while rendering To override this change, and go back to parsing during rendering, apps must make two method calls: 1. `MediaSource.Factory.experimentalParseSubtitlesDuringExtraction(false)` 2. `TextRenderer.experimentalSetLegacyDecodingEnabled(true)` PiperOrigin-RevId: 634262798 --- RELEASENOTES.md | 15 +++++ .../media3/exoplayer/ClippedPlaybackTest.java | 46 +++++++++++----- .../source/DefaultMediaSourceFactory.java | 3 + .../media3/exoplayer/source/MediaSource.java | 9 +-- .../media3/exoplayer/text/TextRenderer.java | 8 ++- .../exoplayer/e2etest/MkvPlaybackTest.java | 6 +- .../e2etest/PlaylistPlaybackTest.java | 6 -- .../exoplayer/e2etest/WebvttPlaybackTest.java | 55 +++++++++---------- .../exoplayer/dash/DashMediaSource.java | 2 + .../dash/e2etest/DashPlaybackTest.java | 18 ++---- .../media3/exoplayer/hls/HlsMediaSource.java | 2 + .../hls/e2etest/HlsPlaybackTest.java | 15 ++--- .../smoothstreaming/SsMediaSource.java | 2 + .../smoothstreaming/SsMediaSourceTest.java | 2 + .../extractor/DefaultExtractorsFactory.java | 5 +- .../media3/extractor/ExtractorsFactory.java | 6 +- .../DefaultExtractorsFactoryTest.java | 14 ++++- .../test/utils/CapturingRenderersFactory.java | 32 ++++++++++- 18 files changed, 160 insertions(+), 86 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b8f3336e85..b5088f821d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -67,6 +67,21 @@ * Text: * Fix issue where subtitles starting before a seek position are skipped. This issue was only introduced in Media3 1.4.0-alpha01. + * Change default subtitle parsing behavior so it happens during extraction + instead of during rendering (see + [ExoPlayer's architecture diagram](https://developer.android.com/media/media3/exoplayer/glossary#exoplayer) + for the difference between extraction and rendering). + * This change can be overridden by calling **both** + `MediaSource.Factory.experimentalParseSubtitlesDuringExtraction(false)` + and `TextRenderer.experimentalSetLegacyDecodingEnabled(true)`. See + the + [docs on customization](https://developer.android.com/media/media3/exoplayer/customization) + for how to plumb these components into an `ExoPlayer` instance. + These methods (and all support for legacy subtitle decoding) will be + removed in a future release. + * Apps with custom `SubtitleDecoder` implementations need to update + them to implement `SubtitleParser` instead (and + `SubtitleParser.Factory` instead of `SubtitleDecoderFactory`). * Metadata: * Fix mapping of MP4 to ID3 sort tags. Previously the 'album sort' (`soal`), 'artist sort' (`soar`) and 'album artist sort' (`soaa`) MP4 diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/ClippedPlaybackTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/ClippedPlaybackTest.java index 0cc3f5f2e6..88b846b211 100644 --- a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/ClippedPlaybackTest.java +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/ClippedPlaybackTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.net.Uri; +import android.os.Looper; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.SubtitleConfiguration; @@ -31,6 +32,8 @@ import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.ConditionVariable; import androidx.media3.exoplayer.source.ClippingMediaSource; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; +import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.exoplayer.text.TextRenderer; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import java.util.ArrayList; @@ -86,14 +89,8 @@ public final class ClippedPlaybackTest { getInstrumentation() .runOnMainSync( () -> { - Context context = getInstrumentation().getContext(); player.set( - new ExoPlayer.Builder(context) - .setMediaSourceFactory( - new DefaultMediaSourceFactory(context) - .experimentalParseSubtitlesDuringExtraction( - parseSubtitlesDuringExtraction)) - .build()); + buildPlayer(getInstrumentation().getContext(), parseSubtitlesDuringExtraction)); player.get().addListener(textCapturer); player.get().setMediaItem(mediaItem); player.get().prepare(); @@ -138,14 +135,8 @@ public final class ClippedPlaybackTest { getInstrumentation() .runOnMainSync( () -> { - Context context = getInstrumentation().getContext(); player.set( - new ExoPlayer.Builder(context) - .setMediaSourceFactory( - new DefaultMediaSourceFactory(context) - .experimentalParseSubtitlesDuringExtraction( - parseSubtitlesDuringExtraction)) - .build()); + buildPlayer(getInstrumentation().getContext(), parseSubtitlesDuringExtraction)); player.get().addListener(textCapturer); player.get().setMediaItems(mediaItems); player.get().prepare(); @@ -163,6 +154,33 @@ public final class ClippedPlaybackTest { .isEqualTo("This is the first subtitle."); } + // Using deprecated TextRenderer.experimentalSetLegacyDecodingEnabled() and + // MediaSource.Factory.experimentalParseSubtitlesDuringExtraction() methods to ensure legacy + // subtitle handling keeps working. + @SuppressWarnings("deprecation") + private static ExoPlayer buildPlayer(Context context, boolean parseSubtitlesDuringExtraction) { + return new ExoPlayer.Builder(context) + .setRenderersFactory( + new DefaultRenderersFactory(context) { + + @Override + protected void buildTextRenderers( + Context context, + TextOutput output, + Looper outputLooper, + @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + super.buildTextRenderers(context, output, outputLooper, extensionRendererMode, out); + ((TextRenderer) Iterables.getLast(out)) + .experimentalSetLegacyDecodingEnabled(!parseSubtitlesDuringExtraction); + } + }) + .setMediaSourceFactory( + new DefaultMediaSourceFactory(context) + .experimentalParseSubtitlesDuringExtraction(parseSubtitlesDuringExtraction)) + .build(); + } + private static void playWhenLoadingIsDone(Player player) { AtomicBoolean loadingStarted = new AtomicBoolean(false); player.addListener( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java index fb6a7548a8..1e711086da 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java @@ -187,10 +187,12 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { liveMaxOffsetMs = C.TIME_UNSET; liveMinSpeed = C.RATE_UNSET; liveMaxSpeed = C.RATE_UNSET; + parseSubtitlesDuringExtraction = true; } @CanIgnoreReturnValue @UnstableApi + @Deprecated @Override public DefaultMediaSourceFactory experimentalParseSubtitlesDuringExtraction( boolean parseSubtitlesDuringExtraction) { @@ -620,6 +622,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { this.subtitleParserFactory = subtitleParserFactory; mediaSourceFactorySuppliers = new HashMap<>(); mediaSourceFactories = new HashMap<>(); + parseSubtitlesDuringExtraction = true; } public @C.ContentType int[] getSupportedTypes() { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java index 8bed520e77..6bf749d83a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java @@ -102,24 +102,25 @@ public interface MediaSource { /** * 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). + * {@code true} (i.e. subtitles will be parsed during extraction). * *

This method is experimental and will be renamed or removed in a future release. * + * @deprecated This method (and all support for 'legacy' subtitle decoding during rendering) + * will be removed in a future release. * @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. @UnstableApi + @Deprecated default Factory experimentalParseSubtitlesDuringExtraction( boolean parseSubtitlesDuringExtraction) { return this; } /** - * Sets the {@link SubtitleParser.Factory} to be used for parsing subtitles during extraction if - * {@link #experimentalParseSubtitlesDuringExtraction} is enabled. + * Sets the {@link SubtitleParser.Factory} to be used for parsing subtitles during extraction. * * @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during * extraction. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java index f4d740f38b..8c65e9659b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java @@ -161,7 +161,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { finalStreamEndPositionUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET; lastRendererPositionUs = C.TIME_UNSET; - legacyDecodingEnabled = true; + legacyDecodingEnabled = false; } @Override @@ -277,11 +277,15 @@ public final class TextRenderer extends BaseRenderer implements Callback { * MimeTypes#APPLICATION_MEDIA3_CUES} (which have been parsed from their original format during * extraction), and will throw an exception if passed data of a different type. * - *

This is enabled by default. + *

This is disabled by default. * *

This method is experimental. It may change behavior, be renamed, or removed in a future * release. + * + * @deprecated This method (and all support for 'legacy' subtitle decoding during rendering) will + * be removed in a future release. */ + @Deprecated public void experimentalSetLegacyDecodingEnabled(boolean legacyDecodingEnabled) { this.legacyDecodingEnabled = legacyDecodingEnabled; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java index 53b01ac993..0f1498dfa8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java @@ -22,7 +22,6 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; -import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.test.utils.CapturingRenderersFactory; import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.FakeClock; @@ -66,11 +65,8 @@ public final class MkvPlaybackTest { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); - // TODO: b/289916598 - Remove this when transcoding is the default. - DefaultExtractorsFactory extractorsFactory = - new DefaultExtractorsFactory().setTextTrackTranscodingEnabled(true); DefaultMediaSourceFactory mediaSourceFactory = - new DefaultMediaSourceFactory(applicationContext, extractorsFactory); + new DefaultMediaSourceFactory(applicationContext); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory, mediaSourceFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/PlaylistPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/PlaylistPlaybackTest.java index 7c5d0dfb11..c594ca91f5 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/PlaylistPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/PlaylistPlaybackTest.java @@ -28,8 +28,6 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.Player; import androidx.media3.common.util.Clock; import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; -import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.test.utils.CapturingRenderersFactory; import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.FakeClock; @@ -107,13 +105,9 @@ public final class PlaylistPlaybackTest { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); - MediaSource.Factory mediaSourceFactory = - new DefaultMediaSourceFactory(applicationContext) - .experimentalParseSubtitlesDuringExtraction(true); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) - .setMediaSourceFactory(mediaSourceFactory) .build(); Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); player.setVideoSurface(surface); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/WebvttPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/WebvttPlaybackTest.java index 3f557b7490..fa170d4c78 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/WebvttPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/WebvttPlaybackTest.java @@ -22,7 +22,6 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.SurfaceTexture; import android.net.Uri; -import android.os.Looper; import android.view.Surface; import androidx.media3.common.C; import androidx.media3.common.MediaItem; @@ -35,11 +34,9 @@ import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; -import androidx.media3.exoplayer.text.TextOutput; import androidx.media3.exoplayer.text.TextRenderer; import androidx.media3.test.utils.CapturingRenderersFactory; import androidx.media3.test.utils.DumpFileAsserts; @@ -50,8 +47,6 @@ import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig; import androidx.media3.test.utils.robolectric.TestPlayerRunHelper; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import java.util.ArrayList; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Rule; @@ -78,13 +73,9 @@ public class WebvttPlaybackTest { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); - MediaSource.Factory mediaSourceFactory = - new DefaultMediaSourceFactory(applicationContext) - .experimentalParseSubtitlesDuringExtraction(true); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) - .setMediaSourceFactory(mediaSourceFactory) .build(); Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); player.setVideoSurface(surface); @@ -120,13 +111,9 @@ public class WebvttPlaybackTest { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); - MediaSource.Factory mediaSourceFactory = - new DefaultMediaSourceFactory(applicationContext) - .experimentalParseSubtitlesDuringExtraction(true); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) - .setMediaSourceFactory(mediaSourceFactory) .setLoadControl( new DefaultLoadControl.Builder() .setBackBuffer( @@ -167,11 +154,21 @@ public class WebvttPlaybackTest { applicationContext, playbackOutput, "playbackdumps/webvtt/" + inputFile + ".seek.dump"); } + // Using deprecated TextRenderer.experimentalSetLegacyDecodingEnabled() and + // MediaSource.Factory.experimentalParseSubtitlesDuringExtraction() methods to ensure legacy + // subtitle handling keeps working. + @SuppressWarnings("deprecation") @Test public void test_legacyParseInRenderer() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = - new CapturingRenderersFactory(applicationContext); + new CapturingRenderersFactory(applicationContext) + .setTextRendererFactory( + (textOutput, outputLooper) -> { + TextRenderer renderer = new TextRenderer(textOutput, outputLooper); + renderer.experimentalSetLegacyDecodingEnabled(true); + return renderer; + }); MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(applicationContext) .experimentalParseSubtitlesDuringExtraction(false); @@ -215,11 +212,21 @@ public class WebvttPlaybackTest { applicationContext, playbackOutput, "playbackdumps/webvtt/" + inputFile + ".dump"); } + // Using deprecated TextRenderer.experimentalSetLegacyDecodingEnabled() and + // MediaSource.Factory.experimentalParseSubtitlesDuringExtraction() methods to ensure legacy + // subtitle handling keeps working. + @SuppressWarnings("deprecation") @Test public void test_legacyParseInRendererWithSeek() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); CapturingRenderersFactory capturingRenderersFactory = - new CapturingRenderersFactory(applicationContext); + new CapturingRenderersFactory(applicationContext) + .setTextRendererFactory( + (textOutput, outputLooper) -> { + TextRenderer renderer = new TextRenderer(textOutput, outputLooper); + renderer.experimentalSetLegacyDecodingEnabled(true); + return renderer; + }); MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(applicationContext) .experimentalParseSubtitlesDuringExtraction(false); @@ -278,22 +285,12 @@ public class WebvttPlaybackTest { applicationContext, playbackOutput, "playbackdumps/webvtt/" + inputFile + ".seek.dump"); } + // Deliberately configuring legacy subtitle handling to check unconfigured TextRenderer fails. + @SuppressWarnings("deprecation") @Test - public void textRendererDoesntSupportLegacyDecoding_playbackFails() throws Exception { + public void textRenderer_doesntSupportLegacyDecodingByDefault_playbackFails() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); - RenderersFactory renderersFactory = - new DefaultRenderersFactory(applicationContext) { - @Override - protected void buildTextRenderers( - Context context, - TextOutput output, - Looper outputLooper, - @ExtensionRendererMode int extensionRendererMode, - ArrayList out) { - super.buildTextRenderers(context, output, outputLooper, extensionRendererMode, out); - ((TextRenderer) Iterables.getLast(out)).experimentalSetLegacyDecodingEnabled(false); - } - }; + RenderersFactory renderersFactory = new DefaultRenderersFactory(applicationContext); MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(applicationContext) .experimentalParseSubtitlesDuringExtraction(false); diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java index 7a8b305b1a..28b898b066 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java @@ -163,6 +163,7 @@ public final class DashMediaSource extends BaseMediaSource { fallbackTargetLiveOffsetMs = DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS; minLiveStartPositionUs = MIN_LIVE_DEFAULT_START_POSITION_US; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + experimentalParseSubtitlesDuringExtraction(true); } @CanIgnoreReturnValue @@ -205,6 +206,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override + @Deprecated @CanIgnoreReturnValue public Factory experimentalParseSubtitlesDuringExtraction( boolean parseSubtitlesDuringExtraction) { diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java index 066a1f018d..a378cd90db 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java @@ -58,9 +58,6 @@ public final class DashPlaybackTest { ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) - .setMediaSourceFactory( - new DashMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); @@ -87,9 +84,6 @@ public final class DashPlaybackTest { ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) - .setMediaSourceFactory( - new DashMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); @@ -116,9 +110,6 @@ public final class DashPlaybackTest { new CapturingRenderersFactory(applicationContext); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) - .setMediaSourceFactory( - new DashMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); @@ -145,9 +136,6 @@ public final class DashPlaybackTest { new CapturingRenderersFactory(applicationContext); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) - .setMediaSourceFactory( - new DashMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); @@ -171,6 +159,9 @@ public final class DashPlaybackTest { * This test and {@link #cea608_parseDuringExtraction()} use the same output dump file, to * demonstrate the flag has no effect on the resulting subtitles. */ + // Using deprecated MediaSource.Factory.experimentalParseSubtitlesDuringExtraction() method to + // ensure legacy subtitle handling keeps working. + @SuppressWarnings("deprecation") @Test public void cea608_parseDuringRendering() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); @@ -204,6 +195,9 @@ public final class DashPlaybackTest { * This test and {@link #cea608_parseDuringRendering()} use the same output dump file, to * demonstrate the flag has no effect on the resulting subtitles. */ + // Explicitly enable parsing during extraction (even though a) it's the default and b) currently + // all CEA-608 parsing happens during rendering) to make this test clearer & more future-proof. + @SuppressWarnings("deprecation") @Test public void cea608_parseDuringExtraction() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); 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 6a0d18cdc2..ccad73c1f7 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 @@ -169,6 +169,7 @@ public final class HlsMediaSource extends BaseMediaSource metadataType = METADATA_TYPE_ID3; elapsedRealTimeOffsetMs = C.TIME_UNSET; allowChunklessPreparation = true; + experimentalParseSubtitlesDuringExtraction(true); } /** @@ -206,6 +207,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override + @Deprecated @CanIgnoreReturnValue public Factory experimentalParseSubtitlesDuringExtraction( boolean parseSubtitlesDuringExtraction) { 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 060e6b0f8d..88fb55eb2c 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 @@ -53,9 +53,6 @@ public final class HlsPlaybackTest { new CapturingRenderersFactory(applicationContext); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) - .setMediaSourceFactory( - new HlsMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); @@ -79,9 +76,6 @@ public final class HlsPlaybackTest { new CapturingRenderersFactory(applicationContext); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) - .setMediaSourceFactory( - new HlsMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); @@ -102,6 +96,9 @@ public final class HlsPlaybackTest { * This test and {@link #cea608_parseDuringExtraction()} use the same output dump file, to * demonstrate the flag has no effect on the resulting subtitles. */ + // Using deprecated MediaSource.Factory.experimentalParseSubtitlesDuringExtraction() method to + // ensure legacy subtitle handling keeps working. + @SuppressWarnings("deprecation") @Test public void cea608_parseDuringRendering() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); @@ -131,6 +128,9 @@ public final class HlsPlaybackTest { * This test and {@link #cea608_parseDuringRendering()} use the same output dump file, to * demonstrate the flag has no effect on the resulting subtitles. */ + // Explicitly enable parsing during extraction (even though a) it's the default and b) currently + // all CEA-608 parsing happens during rendering) to make this test clearer & more future-proof. + @SuppressWarnings("deprecation") @Test public void cea608_parseDuringExtraction() throws Exception { Context applicationContext = ApplicationProvider.getApplicationContext(); @@ -164,9 +164,6 @@ public final class HlsPlaybackTest { new CapturingRenderersFactory(applicationContext); ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) - .setMediaSourceFactory( - new HlsMediaSource.Factory(new DefaultDataSource.Factory(applicationContext)) - .experimentalParseSubtitlesDuringExtraction(true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setLoadControl( new DefaultLoadControl.Builder() diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java index b198aaedae..7086deeb9f 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java @@ -139,6 +139,7 @@ public final class SsMediaSource extends BaseMediaSource loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + experimentalParseSubtitlesDuringExtraction(true); } @CanIgnoreReturnValue @@ -161,6 +162,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override + @Deprecated @CanIgnoreReturnValue public Factory experimentalParseSubtitlesDuringExtraction( boolean parseSubtitlesDuringExtraction) { diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSourceTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSourceTest.java index eba35a5dfc..01a51d532a 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSourceTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSourceTest.java @@ -103,6 +103,7 @@ public class SsMediaSourceTest { } @Test + @SuppressWarnings("deprecation") // Testing deprecated method public void setExperimentalParseSubtitlesDuringExtraction_withNonDefaultChunkSourceFactory_setSucceeds() { SsMediaSource.Factory ssMediaSourceFactory = @@ -113,6 +114,7 @@ public class SsMediaSourceTest { } @Test + @SuppressWarnings("deprecation") // Testing deprecated method public void setExperimentalParseSubtitlesDuringExtraction_withDefaultChunkSourceFactory_setSucceeds() { SsMediaSource.Factory ssMediaSourceFactory = diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java index 7a0615931d..fe31a0b5a4 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java @@ -161,6 +161,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { tsMode = TsExtractor.MODE_SINGLE_PMT; tsTimestampSearchBytes = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES; subtitleParserFactory = new DefaultSubtitleParserFactory(); + textTrackTranscodingEnabled = true; } /** @@ -361,7 +362,8 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { } /** - * @deprecated Use {@link #experimentalSetTextTrackTranscodingEnabled(boolean)} instead. + * @deprecated This method (and all support for 'legacy' subtitle decoding during rendering) will + * be removed in a future release. */ @Deprecated @CanIgnoreReturnValue @@ -370,6 +372,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return experimentalSetTextTrackTranscodingEnabled(textTrackTranscodingEnabled); } + @Deprecated @Override public synchronized DefaultExtractorsFactory experimentalSetTextTrackTranscodingEnabled( boolean textTrackTranscodingEnabled) { diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ExtractorsFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ExtractorsFactory.java index 525dbb7a79..f13f5ae70e 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ExtractorsFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ExtractorsFactory.java @@ -37,15 +37,17 @@ public interface ExtractorsFactory { * Enables transcoding of text track samples to {@link MimeTypes#APPLICATION_MEDIA3_CUES} before * the data is emitted to {@link TrackOutput}. * - *

Transcoding is disabled by default. + *

Transcoding is enabled by default. * *

This method is experimental and will be renamed or removed in a future release. * * @param textTrackTranscodingEnabled Whether to enable transcoding. * @return The factory, for convenience. + * @deprecated This method (and all support for 'legacy' subtitle decoding during rendering) will + * be removed in a future release. */ - // TODO: b/289916598 - Flip this to default to enabled and deprecate it. @CanIgnoreReturnValue + @Deprecated default ExtractorsFactory experimentalSetTextTrackTranscodingEnabled( boolean textTrackTranscodingEnabled) { return this; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java index ca86953501..7646225855 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java @@ -137,11 +137,23 @@ public final class DefaultExtractorsFactoryTest { } @Test - public void subtitleTranscoding_notEnabledByDefault() { + public void subtitleTranscoding_enabledByDefault() { DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); Extractor[] extractors = defaultExtractorsFactory.createExtractors(); + assertThat(stream(extractors).map(Object::getClass)) + .contains(SubtitleTranscodingExtractor.class); + } + + @SuppressWarnings("deprecation") // Testing legacy subtitle handling + @Test + public void subtitleTranscoding_noExtractorWrappingIfDisabled() { + DefaultExtractorsFactory defaultExtractorsFactory = + new DefaultExtractorsFactory().setTextTrackTranscodingEnabled(false); + + Extractor[] extractors = defaultExtractorsFactory.createExtractors(); + assertThat(stream(extractors).map(Object::getClass)) .doesNotContain(SubtitleTranscodingExtractor.class); } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java index a32ae01410..f847880d3a 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java @@ -23,11 +23,13 @@ import android.media.MediaCodec; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.PersistableBundle; import android.util.SparseArray; import android.view.Surface; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.media3.common.C; import androidx.media3.common.util.UnstableApi; import androidx.media3.decoder.CryptoInfo; import androidx.media3.exoplayer.DefaultRenderersFactory; @@ -50,6 +52,7 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -72,7 +75,9 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory; private final CapturingAudioSink audioSink; private final CapturingImageOutput imageOutput; + private ImageDecoder.Factory imageDecoderFactory; + private TextRendererFactory textRendererFactory; /** * Creates an instance. @@ -85,6 +90,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa this.audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder(context).build()); this.imageOutput = new CapturingImageOutput(); this.imageDecoderFactory = ImageDecoder.Factory.DEFAULT; + this.textRendererFactory = TextRenderer::new; } /** @@ -99,6 +105,18 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa return this; } + /** + * Sets the factory for {@link Renderer} instances that handle {@link C#TRACK_TYPE_TEXT} tracks. + * + * @param textRendererFactory The {@link TextRendererFactory}. + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + public CapturingRenderersFactory setTextRendererFactory(TextRendererFactory textRendererFactory) { + this.textRendererFactory = textRendererFactory; + return this; + } + @Override public Renderer[] createRenderers( Handler eventHandler, @@ -146,7 +164,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa eventHandler, audioRendererEventListener, audioSink)); - renderers.add(new TextRenderer(textRendererOutput, eventHandler.getLooper())); + renderers.add(textRendererFactory.create(textRendererOutput, eventHandler.getLooper())); renderers.add(new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper())); renderers.add(new ImageRenderer(imageDecoderFactory, imageOutput)); @@ -160,6 +178,18 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa imageOutput.dump(dumper); } + /** A factory for {@link Renderer} instances that handle {@link C#TRACK_TYPE_TEXT} tracks. */ + public interface TextRendererFactory { + + /** + * Creates a new {@link Renderer} instance for a {@link C#TRACK_TYPE_TEXT} track. + * + * @param textOutput A {@link TextOutput} to handle the parsed subtitles. + * @param outputLooper The looper used to invoke {@code textOutput}. + */ + Renderer create(TextOutput textOutput, Looper outputLooper); + } + /** * A {@link MediaCodecAdapter} that captures interactions and exposes them for test assertions via * {@link Dumper.Dumpable}.