diff --git a/RELEASENOTES.md b/RELEASENOTES.md index df3c2503e0..30dc304d25 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -156,6 +156,7 @@ `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as the main adaptation sets to which they refer. Trick play tracks are marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. + * Enable support for embedded CEA-708. * Fix assertion failure in `SampleQueue` when playing DASH streams with EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). * MP3: diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 7be4f86254..f0ab422f5e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -69,7 +69,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; SequenceableLoader.Callback>, ChunkSampleStream.ReleaseCallback { + // Defined by ANSI/SCTE 214-1 2016 7.2.3. private static final Pattern CEA608_SERVICE_DESCRIPTOR_REGEX = Pattern.compile("CC([1-4])=(.+)"); + // Defined by ANSI/SCTE 214-1 2016 7.2.2. + private static final Pattern CEA708_SERVICE_DESCRIPTOR_REGEX = + Pattern.compile("([1-4])=lang:(\\w+)(,.+)?"); /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; @@ -835,49 +839,52 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (int j = 0; j < descriptors.size(); j++) { Descriptor descriptor = descriptors.get(j); if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { - @Nullable String value = descriptor.value; - if (value == null) { - // There are embedded CEA-608 tracks, but service information is not declared. - return new Format[] {buildCea608TrackFormat(adaptationSet.id)}; - } - String[] services = Util.split(value, ";"); - Format[] formats = new Format[services.length]; - for (int k = 0; k < services.length; k++) { - Matcher matcher = CEA608_SERVICE_DESCRIPTOR_REGEX.matcher(services[k]); - if (!matcher.matches()) { - // If we can't parse service information for all services, assume a single track. - return new Format[] {buildCea608TrackFormat(adaptationSet.id)}; - } - formats[k] = - buildCea608TrackFormat( - adaptationSet.id, - /* language= */ matcher.group(2), - /* accessibilityChannel= */ Integer.parseInt(matcher.group(1))); - } - return formats; + Format cea608Format = + new Format.Builder() + .setSampleMimeType(MimeTypes.APPLICATION_CEA608) + .setId(adaptationSet.id + ":cea608") + .build(); + return parseClosedCaptionDescriptor( + descriptor, CEA608_SERVICE_DESCRIPTOR_REGEX, cea608Format); + } else if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri)) { + Format cea708Format = + new Format.Builder() + .setSampleMimeType(MimeTypes.APPLICATION_CEA708) + .setId(adaptationSet.id + ":cea708") + .build(); + return parseClosedCaptionDescriptor( + descriptor, CEA708_SERVICE_DESCRIPTOR_REGEX, cea708Format); } } } return new Format[0]; } - private static Format buildCea608TrackFormat(int adaptationSetId) { - return buildCea608TrackFormat( - adaptationSetId, /* language= */ null, /* accessibilityChannel= */ Format.NO_VALUE); - } - - private static Format buildCea608TrackFormat( - int adaptationSetId, @Nullable String language, int accessibilityChannel) { - String id = - adaptationSetId - + ":cea608" - + (accessibilityChannel != Format.NO_VALUE ? ":" + accessibilityChannel : ""); - return new Format.Builder() - .setId(id) - .setSampleMimeType(MimeTypes.APPLICATION_CEA608) - .setLanguage(language) - .setAccessibilityChannel(accessibilityChannel) - .build(); + private static Format[] parseClosedCaptionDescriptor( + Descriptor descriptor, Pattern serviceDescriptorRegex, Format baseFormat) { + @Nullable String value = descriptor.value; + if (value == null) { + // There are embedded closed caption tracks, but service information is not declared. + return new Format[] {baseFormat}; + } + String[] services = Util.split(value, ";"); + Format[] formats = new Format[services.length]; + for (int i = 0; i < services.length; i++) { + Matcher matcher = serviceDescriptorRegex.matcher(services[i]); + if (!matcher.matches()) { + // If we can't parse service information for all services, assume a single track. + return new Format[] {baseFormat}; + } + int accessibilityChannel = Integer.parseInt(matcher.group(1)); + formats[i] = + baseFormat + .buildUpon() + .setId(baseFormat.id + ":" + accessibilityChannel) + .setAccessibilityChannel(accessibilityChannel) + .setLanguage(matcher.group(2)) + .build(); + } + return formats; } // We won't assign the array to a variable that erases the generic type, and then write into it. diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index 5a5318c670..4a4438edd6 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -256,6 +256,93 @@ public final class DashMediaPeriodTest { MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); } + @Test + public void cea608AccessibilityDescriptor_createsCea608TrackGroup() { + Descriptor descriptor = + new Descriptor("urn:scte:dash:cc:cea-608:2015", "CC1=eng;CC3=deu", /* id= */ null); + DashManifest manifest = + createDashManifest( + createPeriod( + new AdaptationSet( + /* id= */ 123, + C.TRACK_TYPE_VIDEO, + Arrays.asList( + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + /* accessibilityDescriptors= */ Collections.singletonList(descriptor), + /* essentialProperties= */ Collections.emptyList(), + /* supplementalProperties= */ Collections.emptyList()))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect two adaptation sets. The first containing the video representations, and the second + // containing the embedded CEA-608 tracks. + Format.Builder cea608FormatBuilder = + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608); + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format), + new TrackGroup( + cea608FormatBuilder + .setId("123:cea608:1") + .setLanguage("eng") + .setAccessibilityChannel(1) + .build(), + cea608FormatBuilder + .setId("123:cea608:3") + .setLanguage("deu") + .setAccessibilityChannel(3) + .build())); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + @Test + public void cea708AccessibilityDescriptor_createsCea708TrackGroup() { + Descriptor descriptor = + new Descriptor( + "urn:scte:dash:cc:cea-708:2015", "1=lang:eng;2=lang:deu,war:1,er:1", /* id= */ null); + DashManifest manifest = + createDashManifest( + createPeriod( + new AdaptationSet( + /* id= */ 123, + C.TRACK_TYPE_VIDEO, + Arrays.asList( + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + /* accessibilityDescriptors= */ Collections.singletonList(descriptor), + /* essentialProperties= */ Collections.emptyList(), + /* supplementalProperties= */ Collections.emptyList()))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect two adaptation sets. The first containing the video representations, and the second + // containing the embedded CEA-708 tracks. + Format.Builder cea608FormatBuilder = + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA708); + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format), + new TrackGroup( + cea608FormatBuilder + .setId("123:cea708:1") + .setLanguage("eng") + .setAccessibilityChannel(1) + .build(), + cea608FormatBuilder + .setId("123:cea708:2") + .setLanguage("deu") + .setAccessibilityChannel(2) + .build())); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + private static DashMediaPeriod createDashMediaPeriod(DashManifest manifest, int periodIndex) { return new DashMediaPeriod( /* id= */ periodIndex,