Commit 3183183d by apodob Committed by kim-vde

Add ExoplayerCuesDecoder that decodes text/x-exoplayer-cues

PiperOrigin-RevId: 393723394
parent 75a69082
/*
* Copyright 2021 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
*
* http://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 com.google.android.exoplayer2.text;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* A {@link SubtitleDecoder} that decodes subtitle samples of type {@link
* MimeTypes#TEXT_EXOPLAYER_CUES}
*/
public final class ExoplayerCuesDecoder implements SubtitleDecoder {
@IntDef(value = {INPUT_BUFFER_AVAILABLE, INPUT_BUFFER_DEQUEUED, INPUT_BUFFER_QUEUED})
@Retention(RetentionPolicy.SOURCE)
private @interface InputBufferState {}
private static final int INPUT_BUFFER_AVAILABLE = 0;
private static final int INPUT_BUFFER_DEQUEUED = 1;
private static final int INPUT_BUFFER_QUEUED = 2;
private static final int OUTPUT_BUFFERS_COUNT = 2;
private final CueDecoder cueDecoder;
private final SubtitleInputBuffer inputBuffer;
private final Deque<SubtitleOutputBuffer> availableOutputBuffers;
@InputBufferState private int inputBufferState;
private boolean released;
public ExoplayerCuesDecoder() {
cueDecoder = new CueDecoder();
inputBuffer = new SubtitleInputBuffer();
availableOutputBuffers = new ArrayDeque<>();
for (int i = 0; i < OUTPUT_BUFFERS_COUNT; i++) {
availableOutputBuffers.addFirst(new SimpleSubtitleOutputBuffer(this::releaseOutputBuffer));
}
inputBufferState = INPUT_BUFFER_AVAILABLE;
}
@Override
public String getName() {
return "ExoplayerCuesDecoder";
}
@Nullable
@Override
public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {
checkState(!released);
if (inputBufferState != INPUT_BUFFER_AVAILABLE) {
return null;
}
inputBufferState = INPUT_BUFFER_DEQUEUED;
return inputBuffer;
}
@Override
public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
checkState(!released);
checkState(inputBufferState == INPUT_BUFFER_DEQUEUED);
checkArgument(this.inputBuffer == inputBuffer);
inputBufferState = INPUT_BUFFER_QUEUED;
}
@Nullable
@Override
public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
checkState(!released);
if (inputBufferState != INPUT_BUFFER_QUEUED || availableOutputBuffers.isEmpty()) {
return null;
}
SingleEventSubtitle subtitle =
new SingleEventSubtitle(
inputBuffer.timeUs, cueDecoder.decode(checkNotNull(inputBuffer.data).array()));
SubtitleOutputBuffer outputBuffer = availableOutputBuffers.removeFirst();
outputBuffer.setContent(inputBuffer.timeUs, subtitle, /* subsampleOffsetUs=*/ 0);
inputBuffer.clear();
inputBufferState = INPUT_BUFFER_AVAILABLE;
return outputBuffer;
}
@Override
public void flush() {
checkState(!released);
inputBuffer.clear();
inputBufferState = INPUT_BUFFER_AVAILABLE;
}
@Override
public void release() {
released = true;
}
@Override
public void setPositionUs(long positionUs) {
// Do nothing
}
private void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {
checkState(availableOutputBuffers.size() < OUTPUT_BUFFERS_COUNT);
checkArgument(!availableOutputBuffers.contains(outputBuffer));
outputBuffer.clear();
availableOutputBuffers.addFirst(outputBuffer);
}
private static final class SingleEventSubtitle implements Subtitle {
private final long timeUs;
private final ImmutableList<Cue> cues;
public SingleEventSubtitle(long timeUs, ImmutableList<Cue> cues) {
this.timeUs = timeUs;
this.cues = cues;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
return this.timeUs > timeUs ? 0 : C.INDEX_UNSET;
}
@Override
public int getEventTimeCount() {
return 1;
}
@Override
public long getEventTime(int index) {
checkArgument(index == 0);
return timeUs;
}
@Override
public List<Cue> getCues(long timeUs) {
return (timeUs >= this.timeUs) ? cues : ImmutableList.of();
}
}
}
/*
* Copyright 2021 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
*
* http://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 com.google.android.exoplayer2.text;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link ExoplayerCuesDecoder} */
@RunWith(AndroidJUnit4.class)
public class ExoplayerCuesDecoderTest {
private ExoplayerCuesDecoder decoder;
private static final byte[] ENCODED_CUES =
new CueEncoder().encode(ImmutableList.of(new Cue.Builder().setText("text").build()));
@Before
public void setUp() {
decoder = new ExoplayerCuesDecoder();
}
@After
public void tearDown() {
decoder.release();
}
@Test
public void decoder_outputsSubtitle() throws Exception {
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
SubtitleOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
assertThat(outputBuffer.getCues(/* timeUs=*/ 999)).isEmpty();
assertThat(outputBuffer.getCues(1001)).hasSize(1);
assertThat(outputBuffer.getCues(/* timeUs=*/ 1000)).hasSize(1);
assertThat(outputBuffer.getCues(/* timeUs=*/ 1000).get(0).text.toString()).isEqualTo("text");
outputBuffer.release();
}
@Test
public void dequeueOutputBuffer_returnsNullWhenInputBufferIsNotQueued() throws Exception {
// Returns null before input buffer has been dequeued
assertThat(decoder.dequeueOutputBuffer()).isNull();
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
// Returns null before input has been queued
assertThat(decoder.dequeueOutputBuffer()).isNull();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
// Returns buffer when the input buffer is queued and output buffer is available
assertThat(decoder.dequeueOutputBuffer()).isNotNull();
// Returns null before next input buffer is queued
assertThat(decoder.dequeueOutputBuffer()).isNull();
}
@Test
public void dequeueOutputBuffer_releasedOutputAndQueuedNextInput_returnsOutputBuffer()
throws Exception {
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
SubtitleOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
exhaustAllOutputBuffers(decoder);
assertThat(decoder.dequeueOutputBuffer()).isNull();
outputBuffer.release();
assertThat(decoder.dequeueOutputBuffer()).isNotNull();
}
@Test
public void dequeueInputBuffer_withQueuedInput_returnsNull() throws Exception {
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
assertThat(decoder.dequeueInputBuffer()).isNull();
}
@Test
public void queueInputBuffer_queueingInputBufferThatDoesNotComeFromDecoder_fails() {
assertThrows(
IllegalStateException.class, () -> decoder.queueInputBuffer(new SubtitleInputBuffer()));
}
@Test
public void queueInputBuffer_calledTwice_fails() throws Exception {
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
decoder.queueInputBuffer(inputBuffer);
assertThrows(IllegalStateException.class, () -> decoder.queueInputBuffer(inputBuffer));
}
@Test
public void releaseOutputBuffer_calledTwice_fails() throws Exception {
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
SubtitleOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
outputBuffer.release();
assertThrows(IllegalStateException.class, outputBuffer::release);
}
@Test
public void flush_doesNotInfluenceOutputBufferAvailability() throws Exception {
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
SubtitleOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
assertThat(outputBuffer).isNotNull();
exhaustAllOutputBuffers(decoder);
decoder.flush();
inputBuffer = decoder.dequeueInputBuffer();
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
assertThat(decoder.dequeueOutputBuffer()).isNull();
}
@Test
public void flush_makesAllInputBuffersAvailable() throws Exception {
List<SubtitleInputBuffer> inputBuffers = new ArrayList<>();
SubtitleInputBuffer inputBuffer = decoder.dequeueInputBuffer();
while (inputBuffer != null) {
inputBuffers.add(inputBuffer);
inputBuffer = decoder.dequeueInputBuffer();
}
for (int i = 0; i < inputBuffers.size(); i++) {
writeDataToInputBuffer(inputBuffers.get(i), /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffers.get(i));
}
decoder.flush();
for (int i = 0; i < inputBuffers.size(); i++) {
assertThat(decoder.dequeueInputBuffer().data.position()).isEqualTo(0);
}
}
private void exhaustAllOutputBuffers(ExoplayerCuesDecoder decoder)
throws SubtitleDecoderException {
SubtitleInputBuffer inputBuffer;
do {
inputBuffer = decoder.dequeueInputBuffer();
if (inputBuffer != null) {
writeDataToInputBuffer(inputBuffer, /* timeUs=*/ 1000, ENCODED_CUES);
decoder.queueInputBuffer(inputBuffer);
}
} while (decoder.dequeueOutputBuffer() != null);
}
private void writeDataToInputBuffer(SubtitleInputBuffer inputBuffer, long timeUs, byte[] data) {
inputBuffer.timeUs = timeUs;
inputBuffer.ensureSpaceForWrite(data.length);
inputBuffer.data.put(data);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment