blob: a1c49777456235c248ac68aa792d5db26b1ed14b [file] [log] [blame]
/*
* Copyright (C) 2016 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.extractor.rawcc;
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.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** Extracts data from the RawCC container format. */
public final class RawCcExtractor implements Extractor {
private static final int SCRATCH_SIZE = 9;
private static final int HEADER_SIZE = 8;
private static final int HEADER_ID = 0x52434301;
private static final int TIMESTAMP_SIZE_V0 = 4;
private static final int TIMESTAMP_SIZE_V1 = 8;
// Parser states.
private static final int STATE_READING_HEADER = 0;
private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1;
private static final int STATE_READING_SAMPLES = 2;
private final Format format;
private final ParsableByteArray dataScratch;
private @MonotonicNonNull TrackOutput trackOutput;
private int parserState;
private int version;
private long timestampUs;
private int remainingSampleCount;
private int sampleBytesWritten;
public RawCcExtractor(Format format) {
this.format = format;
dataScratch = new ParsableByteArray(SCRATCH_SIZE);
parserState = STATE_READING_HEADER;
}
@Override
public void init(ExtractorOutput output) {
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
trackOutput.format(format);
output.endTracks();
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
dataScratch.reset(/* limit= */ HEADER_SIZE);
input.peekFully(dataScratch.getData(), 0, HEADER_SIZE);
return dataScratch.readInt() == HEADER_ID;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
Assertions.checkStateNotNull(trackOutput); // Asserts that init has been called.
while (true) {
switch (parserState) {
case STATE_READING_HEADER:
if (parseHeader(input)) {
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
} else {
return RESULT_END_OF_INPUT;
}
break;
case STATE_READING_TIMESTAMP_AND_COUNT:
if (parseTimestampAndSampleCount(input)) {
parserState = STATE_READING_SAMPLES;
} else {
parserState = STATE_READING_HEADER;
return RESULT_END_OF_INPUT;
}
break;
case STATE_READING_SAMPLES:
parseSamples(input);
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
return RESULT_CONTINUE;
default:
throw new IllegalStateException();
}
}
}
@Override
public void seek(long position, long timeUs) {
parserState = STATE_READING_HEADER;
}
@Override
public void release() {
// Do nothing
}
private boolean parseHeader(ExtractorInput input) throws IOException {
dataScratch.reset(/* limit= */ HEADER_SIZE);
if (input.readFully(dataScratch.getData(), 0, HEADER_SIZE, true)) {
if (dataScratch.readInt() != HEADER_ID) {
throw new IOException("Input not RawCC");
}
version = dataScratch.readUnsignedByte();
// no versions use the flag fields yet
return true;
} else {
return false;
}
}
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException {
if (version == 0) {
dataScratch.reset(/* limit= */ TIMESTAMP_SIZE_V0 + 1);
if (!input.readFully(dataScratch.getData(), 0, TIMESTAMP_SIZE_V0 + 1, true)) {
return false;
}
// version 0 timestamps are 45kHz, so we need to convert them into us
timestampUs = dataScratch.readUnsignedInt() * 1000 / 45;
} else if (version == 1) {
dataScratch.reset(/* limit= */ TIMESTAMP_SIZE_V1 + 1);
if (!input.readFully(dataScratch.getData(), 0, TIMESTAMP_SIZE_V1 + 1, true)) {
return false;
}
timestampUs = dataScratch.readLong();
} else {
throw ParserException.createForMalformedContainer(
"Unsupported version number: " + version, /* cause= */ null);
}
remainingSampleCount = dataScratch.readUnsignedByte();
sampleBytesWritten = 0;
return true;
}
@RequiresNonNull("trackOutput")
private void parseSamples(ExtractorInput input) throws IOException {
for (; remainingSampleCount > 0; remainingSampleCount--) {
dataScratch.reset(/* limit= */ 3);
input.readFully(dataScratch.getData(), 0, 3);
trackOutput.sampleData(dataScratch, 3);
sampleBytesWritten += 3;
}
if (sampleBytesWritten > 0) {
trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
}
}
}