mirror of
https://github.com/samsonjs/media.git
synced 2026-04-24 14:37:45 +00:00
Add ExoplayerCuesDecoder that decodes text/x-exoplayer-cues
PiperOrigin-RevId: 393723394
This commit is contained in:
parent
75a6908206
commit
3183183d54
2 changed files with 342 additions and 0 deletions
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue