diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9b36cdd98e..0d6bad7a66 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,9 @@ Release notes a non-empty but invalid license URL. * Fix `setMediaDrmSession failed: session not opened` error when switching between DRM schemes in a playlist (e.g. Widevine to ClearKey). +* Text: + * CEA-608: Ensure service switch commands on field 2 are handled correctly + ([#10666](https://github.com/google/ExoPlayer/issues/10666)). * DASH: * Parse `EventStream.presentationTimeOffset` from manifests ([#10460](https://github.com/google/ExoPlayer/issues/10460)). diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java index b73e303825..9a975939af 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java @@ -874,8 +874,8 @@ private static boolean isXdsControlCode(byte cc1) { } private static boolean isServiceSwitchCommand(byte cc1) { - // cc1 - 0|0|0|1|C|1|0|0 - return (cc1 & 0xF7) == 0x14; + // cc1 - 0|0|0|1|C|1|0|F + return (cc1 & 0xF6) == 0x14; } private static final class CueBuilder { diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java index 2f74fbf954..c1ea977886 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java @@ -255,6 +255,65 @@ public void onlySelectedChannelIsUsed() throws Exception { assertThat(getOnlyCue(fifthSubtitle).text.toString()).isEqualTo("test subtitle"); } + @Test + public void serviceSwitchOnField1Handled() throws Exception { + Cea608Decoder decoder = + new Cea608Decoder( + MimeTypes.APPLICATION_CEA608, + /* accessibilityChannel= */ 1, // field 1, channel 1 + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); + // field 1 (0xFC header): 'test' then service switch + // field 2 (0xFD header): 'wrong!' + byte[] sample1 = + Bytes.concat( + // 'paint on' control character + createPacket(0xFC, 0x14, 0x29), + createPacket(0xFD, 0x15, 0x29), + createPacket(0xFC, 't', 'e'), + createPacket(0xFD, 'w', 'r'), + createPacket(0xFC, 's', 't'), + createPacket(0xFD, 'o', 'n'), + // Enter TEXT service + createPacket(0xFC, 0x14, 0x2A), + createPacket(0xFD, 'g', '!'), + createPacket(0xFC, 'X', 'X'), + createPacket(0xFD, 0x0, 0x0)); + + Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1)); + + assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test"); + } + + // https://github.com/google/ExoPlayer/issues/10666 + @Test + public void serviceSwitchOnField2Handled() throws Exception { + Cea608Decoder decoder = + new Cea608Decoder( + MimeTypes.APPLICATION_CEA608, + /* accessibilityChannel= */ 3, // field 2, channel 1 + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); + // field 1 (0xFC header): 'wrong!' + // field 2 (0xFD header): 'test' then service switch + byte[] sample1 = + Bytes.concat( + // 'paint on' control character + createPacket(0xFC, 0x14, 0x29), + createPacket(0xFD, 0x15, 0x29), + createPacket(0xFC, 'w', 'r'), + createPacket(0xFD, 't', 'e'), + createPacket(0xFC, 'o', 'n'), + createPacket(0xFD, 's', 't'), + createPacket(0xFC, 'g', '!'), + // Enter TEXT service + createPacket(0xFD, 0x15, 0x2A), + createPacket(0xFC, 0x0, 0x0), + createPacket(0xFD, 'X', 'X')); + + Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1)); + + assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test"); + } + private static byte[] createPacket(int header, int cc1, int cc2) { return new byte[] { UnsignedBytes.checkedCast(header),