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
This commit is contained in:
ibaker 2024-05-16 01:40:20 -07:00 committed by Copybara-Service
parent 5c6f48ecaf
commit 0352db9a37
18 changed files with 160 additions and 86 deletions

View File

@ -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

View File

@ -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<Renderer> 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(

View File

@ -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() {

View File

@ -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).
*
* <p>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.

View File

@ -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.
*
* <p>This is enabled by default.
* <p>This is disabled by default.
*
* <p>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;
}

View File

@ -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))

View File

@ -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);

View File

@ -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<Renderer> 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);

View File

@ -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) {

View File

@ -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();

View File

@ -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) {

View File

@ -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()

View File

@ -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) {

View File

@ -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 =

View File

@ -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) {

View File

@ -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}.
*
* <p>Transcoding is disabled by default.
* <p>Transcoding is enabled by default.
*
* <p>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;

View File

@ -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);
}

View File

@ -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}.