blob: be3a582a3d5007cecadbc4e93eccb6a0352f468c [file] [log] [blame]
/*
* 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.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.IndexSeekMap;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import com.google.common.primitives.Ints;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Generic extractor for extracting subtitles from various subtitle formats. */
public class SubtitleExtractor implements Extractor {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_CREATED,
STATE_INITIALIZED,
STATE_EXTRACTING,
STATE_SEEKING,
STATE_FINISHED,
STATE_RELEASED
})
private @interface State {}
/** The extractor has been created. */
private static final int STATE_CREATED = 0;
/** The extractor has been initialized. */
private static final int STATE_INITIALIZED = 1;
/** The extractor is reading from the input and writing to the output. */
private static final int STATE_EXTRACTING = 2;
/** The extractor has received a seek() operation after it has already finished extracting. */
private static final int STATE_SEEKING = 3;
/** The extractor has finished extracting the input. */
private static final int STATE_FINISHED = 4;
/** The extractor has been released. */
private static final int STATE_RELEASED = 5;
private static final int DEFAULT_BUFFER_SIZE = 1024;
private final SubtitleDecoder subtitleDecoder;
private final CueEncoder cueEncoder;
private final ParsableByteArray subtitleData;
private final Format format;
private final List<Long> timestamps;
private final List<ParsableByteArray> samples;
private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput;
private int bytesRead;
@State private int state;
private long seekTimeUs;
/**
* @param subtitleDecoder The decoder used for decoding the subtitle data. The extractor will
* release the decoder in {@link SubtitleExtractor#release()}.
* @param format Format that describes subtitle data.
*/
public SubtitleExtractor(SubtitleDecoder subtitleDecoder, Format format) {
this.subtitleDecoder = subtitleDecoder;
cueEncoder = new CueEncoder();
subtitleData = new ParsableByteArray();
this.format =
format
.buildUpon()
.setSampleMimeType(MimeTypes.TEXT_EXOPLAYER_CUES)
.setCodecs(format.sampleMimeType)
.build();
timestamps = new ArrayList<>();
samples = new ArrayList<>();
state = STATE_CREATED;
seekTimeUs = C.TIME_UNSET;
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
// TODO: Implement sniff() according to the Extractor interface documentation. For now sniff()
// can safely return true because we plan to use this class in an ExtractorFactory that returns
// exactly one Extractor implementation.
return true;
}
@Override
public void init(ExtractorOutput output) {
checkState(state == STATE_CREATED);
extractorOutput = output;
trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_TEXT);
extractorOutput.endTracks();
extractorOutput.seekMap(
new IndexSeekMap(
/* positions= */ new long[] {0},
/* timesUs= */ new long[] {0},
/* durationUs= */ C.TIME_UNSET));
trackOutput.format(format);
state = STATE_INITIALIZED;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
checkState(state != STATE_CREATED && state != STATE_RELEASED);
if (state == STATE_INITIALIZED) {
subtitleData.reset(
input.getLength() != C.LENGTH_UNSET
? Ints.checkedCast(input.getLength())
: DEFAULT_BUFFER_SIZE);
bytesRead = 0;
state = STATE_EXTRACTING;
}
if (state == STATE_EXTRACTING) {
boolean inputFinished = readFromInput(input);
if (inputFinished) {
decode();
writeToOutput();
state = STATE_FINISHED;
}
}
if (state == STATE_SEEKING) {
boolean inputFinished = skipInput(input);
if (inputFinished) {
writeToOutput();
state = STATE_FINISHED;
}
}
if (state == STATE_FINISHED) {
return RESULT_END_OF_INPUT;
}
return RESULT_CONTINUE;
}
@Override
public void seek(long position, long timeUs) {
checkState(state != STATE_CREATED && state != STATE_RELEASED);
seekTimeUs = timeUs;
if (state == STATE_EXTRACTING) {
state = STATE_INITIALIZED;
}
if (state == STATE_FINISHED) {
state = STATE_SEEKING;
}
}
/** Releases the extractor's resources, including the {@link SubtitleDecoder}. */
@Override
public void release() {
if (state == STATE_RELEASED) {
return;
}
subtitleDecoder.release();
state = STATE_RELEASED;
}
/** Returns whether the input has been fully skipped. */
private boolean skipInput(ExtractorInput input) throws IOException {
return input.skip(
input.getLength() != C.LENGTH_UNSET
? Ints.checkedCast(input.getLength())
: DEFAULT_BUFFER_SIZE)
== C.RESULT_END_OF_INPUT;
}
/** Returns whether reading has been finished. */
private boolean readFromInput(ExtractorInput input) throws IOException {
if (subtitleData.capacity() == bytesRead) {
subtitleData.ensureCapacity(bytesRead + DEFAULT_BUFFER_SIZE);
}
int readResult =
input.read(subtitleData.getData(), bytesRead, subtitleData.capacity() - bytesRead);
if (readResult != C.RESULT_END_OF_INPUT) {
bytesRead += readResult;
}
long inputLength = input.getLength();
return (inputLength != C.LENGTH_UNSET && bytesRead == inputLength)
|| readResult == C.RESULT_END_OF_INPUT;
}
/** Decodes the subtitle data and stores the samples in the memory of the extractor. */
private void decode() throws IOException {
try {
@Nullable SubtitleInputBuffer inputBuffer = subtitleDecoder.dequeueInputBuffer();
while (inputBuffer == null) {
Thread.sleep(5);
inputBuffer = subtitleDecoder.dequeueInputBuffer();
}
inputBuffer.ensureSpaceForWrite(bytesRead);
inputBuffer.data.put(subtitleData.getData(), /* offset= */ 0, bytesRead);
inputBuffer.data.limit(bytesRead);
subtitleDecoder.queueInputBuffer(inputBuffer);
@Nullable SubtitleOutputBuffer outputBuffer = subtitleDecoder.dequeueOutputBuffer();
while (outputBuffer == null) {
Thread.sleep(5);
outputBuffer = subtitleDecoder.dequeueOutputBuffer();
}
for (int i = 0; i < outputBuffer.getEventTimeCount(); i++) {
List<Cue> cues = outputBuffer.getCues(outputBuffer.getEventTime(i));
byte[] cuesSample = cueEncoder.encode(cues);
timestamps.add(outputBuffer.getEventTime(i));
samples.add(new ParsableByteArray(cuesSample));
}
outputBuffer.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException();
} catch (SubtitleDecoderException e) {
throw ParserException.createForMalformedContainer("SubtitleDecoder failed.", e);
}
}
private void writeToOutput() {
checkStateNotNull(this.trackOutput);
checkState(timestamps.size() == samples.size());
int index =
seekTimeUs == C.TIME_UNSET
? 0
: Util.binarySearchFloor(
timestamps, seekTimeUs, /* inclusive= */ true, /* stayInBounds= */ true);
for (int i = index; i < samples.size(); i++) {
ParsableByteArray sample = samples.get(i);
sample.setPosition(0);
int size = sample.getData().length;
trackOutput.sampleData(sample, size);
trackOutput.sampleMetadata(
/* timeUs= */ timestamps.get(i),
/* flags= */ C.BUFFER_FLAG_KEY_FRAME,
/* size= */ size,
/* offset= */ 0,
/* cryptoData= */ null);
}
}
}