Fix DASH CEA-608 parsing during extraction

This is similar to the HLS fix in 770ca66fbc

Similar to HLS, the original problem here was **not** modifying the
`Format` for caption tracks
embedded into the video stream. I tried just updating the format in
both places, but that caused new failures because the new
('transcoded') format was then fed into `FragmentedMp4Extractor` as
part of `closedCaptionFormats`, which resulted in the CEA-608 data
being emitted from `FragmentedMp4Extractor` with the incorrect
`application/x-media3-cues` MIME type (but the bytes were actually
CEA-608), meaning the transcoding wrapper passed it through without
transcoding and decoding failed (because obviously CEA-608 bytes can't
be decoded by `CueDecoder` which is expecting a `Bundle` from
`CuesWithTiming.toBundle`.

To resolve this we keep track of the 'original' caption formats inside
`TrackGroupInfo`, so we can feed them into `FragmentedMp4Extractor`.
For all other usages in `DashMediaPeriod` we use the 'transcoded'
caption formats.

PiperOrigin-RevId: 592866262
This commit is contained in:
ibaker 2023-12-21 08:21:13 -08:00 committed by Copybara-Service
parent 60c8273521
commit f36ab87b38
2 changed files with 102 additions and 37 deletions

View File

@ -59,6 +59,7 @@ import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.primitives.Ints;
import java.io.IOException;
@ -689,18 +690,6 @@ import java.util.regex.Pattern;
originalFormat
.buildUpon()
.setCryptoType(drmSessionManager.getCryptoType(originalFormat));
if (subtitleParserFactory != null && subtitleParserFactory.supportsFormat(originalFormat)) {
updatedFormat
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(
subtitleParserFactory.getCueReplacementBehavior(originalFormat))
.setCodecs(
originalFormat.sampleMimeType
+ (originalFormat.codecs != null ? " " + originalFormat.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);
}
formats[j] = updatedFormat.build();
}
@ -715,6 +704,7 @@ import java.util.regex.Pattern;
int closedCaptionTrackGroupIndex =
primaryGroupClosedCaptionTrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;
maybeUpdateFormatsForParsedText(subtitleParserFactory, formats);
trackGroups[primaryTrackGroupIndex] = new TrackGroup(trackGroupId, formats);
trackGroupInfos[primaryTrackGroupIndex] =
TrackGroupInfo.primaryTrack(
@ -736,10 +726,15 @@ import java.util.regex.Pattern;
}
if (closedCaptionTrackGroupIndex != C.INDEX_UNSET) {
String closedCaptionTrackGroupId = trackGroupId + ":cc";
trackGroupInfos[closedCaptionTrackGroupIndex] =
TrackGroupInfo.embeddedClosedCaptionTrack(
adaptationSetIndices,
primaryTrackGroupIndex,
ImmutableList.copyOf(primaryGroupClosedCaptionTrackFormats[i]));
maybeUpdateFormatsForParsedText(
subtitleParserFactory, primaryGroupClosedCaptionTrackFormats[i]);
trackGroups[closedCaptionTrackGroupIndex] =
new TrackGroup(closedCaptionTrackGroupId, primaryGroupClosedCaptionTrackFormats[i]);
trackGroupInfos[closedCaptionTrackGroupIndex] =
TrackGroupInfo.embeddedClosedCaptionTrack(adaptationSetIndices, primaryTrackGroupIndex);
}
}
return trackGroupCount;
@ -774,14 +769,12 @@ import java.util.regex.Pattern;
trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex);
embeddedTrackCount++;
}
boolean enableClosedCaptionTrack =
trackGroupInfo.embeddedClosedCaptionTrackGroupIndex != C.INDEX_UNSET;
TrackGroup embeddedClosedCaptionTrackGroup = null;
if (enableClosedCaptionTrack) {
embeddedClosedCaptionTrackGroup =
trackGroups.get(trackGroupInfo.embeddedClosedCaptionTrackGroupIndex);
embeddedTrackCount += embeddedClosedCaptionTrackGroup.length;
}
ImmutableList<Format> embeddedClosedCaptionOriginalFormats =
trackGroupInfo.embeddedClosedCaptionTrackGroupIndex != C.INDEX_UNSET
? trackGroupInfos[trackGroupInfo.embeddedClosedCaptionTrackGroupIndex]
.embeddedClosedCaptionTrackOriginalFormats
: ImmutableList.of();
embeddedTrackCount += embeddedClosedCaptionOriginalFormats.size();
Format[] embeddedTrackFormats = new Format[embeddedTrackCount];
int[] embeddedTrackTypes = new int[embeddedTrackCount];
@ -792,13 +785,11 @@ import java.util.regex.Pattern;
embeddedTrackCount++;
}
List<Format> embeddedClosedCaptionTrackFormats = new ArrayList<>();
if (enableClosedCaptionTrack) {
for (int i = 0; i < embeddedClosedCaptionTrackGroup.length; i++) {
embeddedTrackFormats[embeddedTrackCount] = embeddedClosedCaptionTrackGroup.getFormat(i);
embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT;
embeddedClosedCaptionTrackFormats.add(embeddedTrackFormats[embeddedTrackCount]);
embeddedTrackCount++;
}
for (int i = 0; i < embeddedClosedCaptionOriginalFormats.size(); i++) {
embeddedTrackFormats[embeddedTrackCount] = embeddedClosedCaptionOriginalFormats.get(i);
embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT;
embeddedClosedCaptionTrackFormats.add(embeddedTrackFormats[embeddedTrackCount]);
embeddedTrackCount++;
}
PlayerTrackEmsgHandler trackPlayerEmsgHandler =
@ -932,6 +923,30 @@ import java.util.regex.Pattern;
return formats;
}
/**
* Modifies the provided {@link Format} array if subtitle/caption parsing is configured to happen
* during extraction.
*/
private static void maybeUpdateFormatsForParsedText(
SubtitleParser.Factory subtitleParserFactory, Format[] formats) {
for (int i = 0; i < formats.length; i++) {
if (subtitleParserFactory == null || !subtitleParserFactory.supportsFormat(formats[i])) {
continue;
}
formats[i] =
formats[i]
.buildUpon()
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
.setCueReplacementBehavior(
subtitleParserFactory.getCueReplacementBehavior(formats[i]))
.setCodecs(
formats[i].sampleMimeType
+ (formats[i].codecs != null ? " " + formats[i].codecs : ""))
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
.build();
}
}
// We won't assign the array to a variable that erases the generic type, and then write into it.
@SuppressWarnings({"unchecked", "rawtypes"})
private static ChunkSampleStream<DashChunkSource>[] newSampleStreamArray(int length) {
@ -973,6 +988,9 @@ import java.util.regex.Pattern;
public final int embeddedEventMessageTrackGroupIndex;
public final int embeddedClosedCaptionTrackGroupIndex;
/** Only non-empty for track groups representing embedded caption tracks. */
public final ImmutableList<Format> embeddedClosedCaptionTrackOriginalFormats;
public static TrackGroupInfo primaryTrack(
int trackType,
int[] adaptationSetIndices,
@ -986,7 +1004,8 @@ import java.util.regex.Pattern;
primaryTrackGroupIndex,
embeddedEventMessageTrackGroupIndex,
embeddedClosedCaptionTrackGroupIndex,
/* eventStreamGroupIndex= */ -1);
/* eventStreamGroupIndex= */ -1,
/* embeddedClosedCaptionTrackOriginalFormats= */ ImmutableList.of());
}
public static TrackGroupInfo embeddedEmsgTrack(
@ -998,11 +1017,14 @@ import java.util.regex.Pattern;
primaryTrackGroupIndex,
C.INDEX_UNSET,
C.INDEX_UNSET,
/* eventStreamGroupIndex= */ -1);
/* eventStreamGroupIndex= */ -1,
/* embeddedClosedCaptionTrackOriginalFormats= */ ImmutableList.of());
}
public static TrackGroupInfo embeddedClosedCaptionTrack(
int[] adaptationSetIndices, int primaryTrackGroupIndex) {
int[] adaptationSetIndices,
int primaryTrackGroupIndex,
ImmutableList<Format> originalFormats) {
return new TrackGroupInfo(
C.TRACK_TYPE_TEXT,
CATEGORY_EMBEDDED,
@ -1010,7 +1032,8 @@ import java.util.regex.Pattern;
primaryTrackGroupIndex,
C.INDEX_UNSET,
C.INDEX_UNSET,
/* eventStreamGroupIndex= */ -1);
/* eventStreamGroupIndex= */ -1,
originalFormats);
}
public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) {
@ -1021,7 +1044,8 @@ import java.util.regex.Pattern;
/* primaryTrackGroupIndex= */ -1,
C.INDEX_UNSET,
C.INDEX_UNSET,
eventStreamIndex);
eventStreamIndex,
/* embeddedClosedCaptionTrackOriginalFormats= */ ImmutableList.of());
}
private TrackGroupInfo(
@ -1031,7 +1055,8 @@ import java.util.regex.Pattern;
int primaryTrackGroupIndex,
int embeddedEventMessageTrackGroupIndex,
int embeddedClosedCaptionTrackGroupIndex,
int eventStreamGroupIndex) {
int eventStreamGroupIndex,
ImmutableList<Format> embeddedClosedCaptionTrackOriginalFormats) {
this.trackType = trackType;
this.adaptationSetIndices = adaptationSetIndices;
this.trackGroupCategory = trackGroupCategory;
@ -1039,6 +1064,7 @@ import java.util.regex.Pattern;
this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex;
this.embeddedClosedCaptionTrackGroupIndex = embeddedClosedCaptionTrackGroupIndex;
this.eventStreamGroupIndex = eventStreamGroupIndex;
this.embeddedClosedCaptionTrackOriginalFormats = embeddedClosedCaptionTrackOriginalFormats;
}
}
}

View File

@ -167,14 +167,53 @@ public final class DashPlaybackTest {
applicationContext, playbackOutput, "playbackdumps/dash/ttml-in-mp4.dump");
}
/**
* This test and {@link #cea608_parseDuringExtraction()} use the same output dump file, to
* demonstrate the flag has no effect on the resulting subtitles.
*/
@Test
public void cea608() throws Exception {
public void cea608_parseDuringRendering() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext);
// TODO(b/181312195): Opt this test into the new subtitle parsing when it works.
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setMediaSourceFactory(
new DashMediaSource.Factory(new DefaultDataSource.Factory(applicationContext))
.experimentalParseSubtitlesDuringExtraction(false))
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
// Ensure the subtitle track is selected.
DefaultTrackSelector trackSelector =
checkNotNull((DefaultTrackSelector) player.getTrackSelector());
trackSelector.setParameters(trackSelector.buildUponParameters().setPreferredTextLanguage("en"));
player.setMediaItem(MediaItem.fromUri("asset:///media/dash/cea608/manifest.mpd"));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/dash/cea608.dump");
}
/**
* This test and {@link #cea608_parseDuringRendering()} use the same output dump file, to
* demonstrate the flag has no effect on the resulting subtitles.
*/
@Test
public void cea608_parseDuringExtraction() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory capturingRenderersFactory =
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)));