mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add Cea608DecoderTest
When debugging and fixing Issue: google/ExoPlayer#10666 I wanted to write a regression test, but needed to add a test first... This is just a small bit of coverage to start with. It checks the field/channel filtering works correctly, but doesn't check any styling info. It also doesn't test 'pop on' subtitles (i.e. when the subtitle isn't shown until a 'end of subtitle' signal is received). PiperOrigin-RevId: 480644568 (cherry picked from commit 706b1299049b23dbc71a7407ffbaf8598f56b610)
This commit is contained in:
parent
cc67241062
commit
3575b68020
@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.extractor.text.cea;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
import androidx.media3.extractor.text.SubtitleDecoderException;
|
||||
import androidx.media3.extractor.text.SubtitleInputBuffer;
|
||||
import androidx.media3.extractor.text.SubtitleOutputBuffer;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.UnsignedBytes;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests for {@link Cea608Decoder}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class Cea608DecoderTest {
|
||||
|
||||
@Test
|
||||
public void paintOnEmitsSubtitlesImmediately() throws Exception {
|
||||
Cea608Decoder decoder =
|
||||
new Cea608Decoder(
|
||||
MimeTypes.APPLICATION_CEA608,
|
||||
/* accessibilityChannel= */ 1,
|
||||
Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS);
|
||||
byte[] sample1 =
|
||||
Bytes.concat(
|
||||
// 'paint on' control character
|
||||
createPacket(0xFC, 0x14, 0x29),
|
||||
createPacket(0xFC, 't', 'e'),
|
||||
createPacket(0xFC, 's', 't'),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFC, 'u', 'b'),
|
||||
createPacket(0xFC, 't', 'i'),
|
||||
createPacket(0xFC, 't', 'l'),
|
||||
createPacket(0xFC, 'e', ','),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFC, 'p', 'a'));
|
||||
byte[] sample2 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, 'n', 's'),
|
||||
createPacket(0xFC, ' ', '2'),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFC, 'a', 'm'),
|
||||
createPacket(0xFC, 'p', 'l'),
|
||||
createPacket(0xFC, 'e', 's'));
|
||||
|
||||
Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1));
|
||||
Subtitle secondSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample2));
|
||||
|
||||
assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test subtitle, spa");
|
||||
assertThat(getOnlyCue(secondSubtitle).text.toString())
|
||||
.isEqualTo("test subtitle, spans 2 samples");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollUpEmitsSubtitlesImmediately() throws Exception {
|
||||
Cea608Decoder decoder =
|
||||
new Cea608Decoder(
|
||||
MimeTypes.APPLICATION_CEA608,
|
||||
/* accessibilityChannel= */ 1, // field 1, channel 1
|
||||
Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS);
|
||||
byte[] sample1 =
|
||||
Bytes.concat(
|
||||
// 'roll up 2 rows' control character
|
||||
createPacket(0xFC, 0x14, 0x25),
|
||||
createPacket(0xFC, 't', 'e'),
|
||||
createPacket(0xFC, 's', 't'),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFC, 'u', 'b'),
|
||||
createPacket(0xFC, 't', 'i'),
|
||||
createPacket(0xFC, 't', 'l'),
|
||||
createPacket(0xFC, 'e', ','),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFC, 'p', 'a'));
|
||||
byte[] sample2 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, 'n', 's'),
|
||||
createPacket(0xFC, ' ', '3'),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFC, 'a', 'm'),
|
||||
createPacket(0xFC, 'p', 'l'),
|
||||
createPacket(0xFC, 'e', 's'),
|
||||
// Carriage return control character
|
||||
createPacket(0xFC, 0x14, 0x2D),
|
||||
createPacket(0xFC, 'w', 'i'),
|
||||
createPacket(0xFC, 't', 'h'),
|
||||
createPacket(0xFC, ' ', 'n'));
|
||||
byte[] sample3 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, 'e', 'w'),
|
||||
createPacket(0xFC, 'l', 'i'),
|
||||
createPacket(0xFC, 'n', 'e'),
|
||||
createPacket(0xFC, 's', 0x0));
|
||||
|
||||
Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1));
|
||||
Subtitle secondSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample2));
|
||||
Subtitle thirdSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample3));
|
||||
|
||||
assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test subtitle, spa");
|
||||
assertThat(getOnlyCue(secondSubtitle).text.toString())
|
||||
.isEqualTo("test subtitle, spans 3 samples\nwith n");
|
||||
assertThat(getOnlyCue(thirdSubtitle).text.toString())
|
||||
.isEqualTo("test subtitle, spans 3 samples\nwith newlines");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlySelectedFieldIsUsed() 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 subtitle'
|
||||
// field 2 (0xfC header): 'wrong field!'
|
||||
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'),
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFD, 'g', ' '),
|
||||
createPacket(0xFC, 'u', 'b'),
|
||||
createPacket(0xFD, 'f', 'i'));
|
||||
byte[] sample2 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, 't', 'i'),
|
||||
createPacket(0xFD, 'e', 'l'),
|
||||
createPacket(0xFC, 't', 'l'),
|
||||
createPacket(0xFD, 'd', '!'),
|
||||
createPacket(0xFC, 'e', 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0));
|
||||
|
||||
Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1));
|
||||
Subtitle secondSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample2));
|
||||
|
||||
assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test sub");
|
||||
assertThat(getOnlyCue(secondSubtitle).text.toString()).isEqualTo("test subtitle");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlySelectedChannelIsUsed() throws Exception {
|
||||
Cea608Decoder decoder =
|
||||
new Cea608Decoder(
|
||||
MimeTypes.APPLICATION_CEA608,
|
||||
/* accessibilityChannel= */ 2, // field 1, channel 2
|
||||
Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS);
|
||||
// field 1 (0xfC header), channel 1: 'wrong channel'
|
||||
// field 1 (0xfC header), channel 2: 'test subtitle'
|
||||
// field 2 (0xfC header), channel 1: 'wrong field!'
|
||||
byte[] sample1 =
|
||||
Bytes.concat(
|
||||
// 'paint on' control character
|
||||
createPacket(0xFC, 0x14, 0x29),
|
||||
createPacket(0xFD, 0x15, 0x29),
|
||||
createPacket(0xFC, 'w', 'r'),
|
||||
createPacket(0xFD, 'w', 'r'),
|
||||
createPacket(0xFC, 'o', 'n'),
|
||||
createPacket(0xFD, 'o', 'n'),
|
||||
// Switch to channel 2 & 'paint on' control character
|
||||
createPacket(0xFC, 0x14 | 0x08, 0x29),
|
||||
createPacket(0xFD, 'g', ' '),
|
||||
createPacket(0xFC, 't', 'e'),
|
||||
createPacket(0xFD, 'f', 'i'));
|
||||
byte[] sample2 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, 's', 't'),
|
||||
createPacket(0xFD, 'e', 'l'),
|
||||
// Switch to channel 1
|
||||
createPacket(0xFC, 0x14, 0x0),
|
||||
createPacket(0xFD, 'd', '!'),
|
||||
createPacket(0xFC, 'g', ' '),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 'c', 'h'),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
// Switch to channel 2
|
||||
createPacket(0xFC, 0x14 | 0x08, 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0));
|
||||
byte[] sample3 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, ' ', 's'),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 'u', 'b'),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
// Switch to channel 1
|
||||
createPacket(0xFC, 0x14, 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 'a', 'n'),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 'n', 'e'),
|
||||
createPacket(0xFD, 0x0, 0x0));
|
||||
byte[] sample4 =
|
||||
Bytes.concat(
|
||||
// Switch to channel 2
|
||||
createPacket(0xFC, 0x14 | 0x08, 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 't', 'i'),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 't', 'l'),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
// Switch to channel 1
|
||||
createPacket(0xFC, 0x14, 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 'l', 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0));
|
||||
byte[] sample5 =
|
||||
Bytes.concat(
|
||||
createPacket(0xFC, 0x0, 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
// Switch to channel 2
|
||||
createPacket(0xFC, 0x14 | 0x08, 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0),
|
||||
createPacket(0xFC, 'e', 0x0),
|
||||
createPacket(0xFD, 0x0, 0x0));
|
||||
|
||||
Subtitle firstSubtitle = /*checkNotNull(*/ decodeSampleAndCopyResult(decoder, sample1) /*)*/;
|
||||
Subtitle secondSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample2));
|
||||
Subtitle thirdSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample3));
|
||||
Subtitle fourthSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample4));
|
||||
Subtitle fifthSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample5));
|
||||
|
||||
assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("te");
|
||||
assertThat(getOnlyCue(secondSubtitle).text.toString()).isEqualTo("test");
|
||||
assertThat(getOnlyCue(thirdSubtitle).text.toString()).isEqualTo("test sub");
|
||||
assertThat(getOnlyCue(fourthSubtitle).text.toString()).isEqualTo("test subtitl");
|
||||
assertThat(getOnlyCue(fifthSubtitle).text.toString()).isEqualTo("test subtitle");
|
||||
}
|
||||
|
||||
private static byte[] createPacket(int header, int cc1, int cc2) {
|
||||
return new byte[] {
|
||||
UnsignedBytes.checkedCast(header),
|
||||
ensureUnsignedByteOddParity(cc1),
|
||||
ensureUnsignedByteOddParity(cc2)
|
||||
};
|
||||
}
|
||||
|
||||
private static byte ensureUnsignedByteOddParity(int input) {
|
||||
checkArgument(input >= 0);
|
||||
checkArgument(input < 128);
|
||||
|
||||
return UnsignedBytes.checkedCast(Integer.bitCount(input) % 2 == 0 ? input | 0x80 : input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues {@code sample} to {@code decoder} and dequeues the result, then copies and returns it if
|
||||
* it's non-null.
|
||||
*
|
||||
* <p>Fails if {@link Cea608Decoder#dequeueInputBuffer()} returns {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
private static Subtitle decodeSampleAndCopyResult(Cea608Decoder decoder, byte[] sample)
|
||||
throws SubtitleDecoderException {
|
||||
SubtitleInputBuffer inputBuffer = checkNotNull(decoder.dequeueInputBuffer());
|
||||
inputBuffer.data = ByteBuffer.wrap(sample);
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
@Nullable SubtitleOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
|
||||
if (outputBuffer == null) {
|
||||
return null;
|
||||
}
|
||||
SimpleSubtitle subtitle = SimpleSubtitle.copyOf(outputBuffer);
|
||||
outputBuffer.release();
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
private static Cue getOnlyCue(Subtitle subtitle) {
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(1);
|
||||
return Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
}
|
||||
|
||||
private static final class SimpleSubtitle implements Subtitle {
|
||||
|
||||
private final ImmutableList<Long> eventTimesUs;
|
||||
private final ImmutableList<ImmutableList<Cue>> events;
|
||||
|
||||
private SimpleSubtitle(
|
||||
ImmutableList<Long> eventTimesUs, ImmutableList<ImmutableList<Cue>> events) {
|
||||
this.eventTimesUs = eventTimesUs;
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public static SimpleSubtitle copyOf(Subtitle subtitle) {
|
||||
ImmutableList.Builder<Long> eventTimesUs = ImmutableList.builder();
|
||||
ImmutableList.Builder<ImmutableList<Cue>> events = ImmutableList.builder();
|
||||
for (int i = 0; i < subtitle.getEventTimeCount(); i++) {
|
||||
long eventTimeUs = subtitle.getEventTime(i);
|
||||
eventTimesUs.add(eventTimeUs);
|
||||
events.add(ImmutableList.copyOf(subtitle.getCues(eventTimeUs)));
|
||||
}
|
||||
return new SimpleSubtitle(eventTimesUs.build(), events.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextEventTimeIndex(long timeUs) {
|
||||
int index = Util.binarySearchCeil(eventTimesUs, timeUs, /* inclusive= */ false, false);
|
||||
return index != eventTimesUs.size() ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEventTimeCount() {
|
||||
return eventTimesUs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEventTime(int index) {
|
||||
return eventTimesUs.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<Cue> getCues(long timeUs) {
|
||||
return events.get(
|
||||
Util.binarySearchFloor(
|
||||
eventTimesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user