/*
* Copyright (C) 2014 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.exoplayer.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Stack;
/**
* Default implementation of {@link EbmlReader}.
*/
/* package */ final class DefaultEbmlReader implements EbmlReader {
private static final int ELEMENT_STATE_READ_ID = 0;
private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;
private static final int ELEMENT_STATE_READ_CONTENT = 2;
private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
private final byte[] scratch = new byte[8];
private final Stack<MasterElement> masterElementsStack = new Stack<>();
private final VarintReader varintReader = new VarintReader();
private EbmlReaderOutput output;
private int elementState;
private int elementId;
private long elementContentSize;
@Override
public void init(EbmlReaderOutput eventHandler) {
this.output = eventHandler;
}
@Override
public void reset() {
elementState = ELEMENT_STATE_READ_ID;
masterElementsStack.clear();
varintReader.reset();
}
@Override
public boolean read(ExtractorInput input) throws IOException, InterruptedException {
Assertions.checkState(output != null);
while (true) {
if (!masterElementsStack.isEmpty()
&& input.getPosition() >= masterElementsStack.peek().elementEndPosition) {
output.endMasterElement(masterElementsStack.pop().elementId);
return true;
}
if (elementState == ELEMENT_STATE_READ_ID) {
long result = varintReader.readUnsignedVarint(input, true, false);
if (result == -1) {
return false;
}
// Element IDs are at most 4 bytes, so we can cast to integers.
elementId = (int) result;
elementState = ELEMENT_STATE_READ_CONTENT_SIZE;
}
if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {
elementContentSize = varintReader.readUnsignedVarint(input, false, true);
elementState = ELEMENT_STATE_READ_CONTENT;
}
int type = output.getElementType(elementId);
switch (type) {
case TYPE_MASTER:
long elementContentPosition = input.getPosition();
long elementEndPosition = elementContentPosition + elementContentSize;
masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size: " + elementContentSize);
}
output.integerElement(elementId, readInteger(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid float size: " + elementContentSize);
}
output.floatElement(elementId, readFloat(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException("String element size: " + elementContentSize);
}
output.stringElement(elementId, readString(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_BINARY:
output.binaryElement(elementId, (int) elementContentSize, input);
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_UNKNOWN:
input.skipFully((int) elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
}
}
/**
* Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the integer being read.
* @return The read integer value.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private long readInteger(ExtractorInput input, int byteLength)
throws IOException, InterruptedException {
input.readFully(scratch, 0, byteLength);
long value = 0;
for (int i = 0; i < byteLength; i++) {
value = (value << 8) | (scratch[i] & 0xFF);
}
return value;
}
/**
* Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the float being read.
* @return The read float value.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private double readFloat(ExtractorInput input, int byteLength)
throws IOException, InterruptedException {
long integerValue = readInteger(input, byteLength);
double floatValue;
if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
floatValue = Float.intBitsToFloat((int) integerValue);
} else {
floatValue = Double.longBitsToDouble(integerValue);
}
return floatValue;
}
/**
* Reads and returns a string of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the float being read.
* @return The read string value.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private String readString(ExtractorInput input, int byteLength)
throws IOException, InterruptedException {
byte[] stringBytes = new byte[byteLength];
input.readFully(stringBytes, 0, byteLength);
return new String(stringBytes, Charset.forName(C.UTF8_NAME));
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends, so that
* {@link EbmlReaderOutput#endMasterElement(int)} can be called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndPosition;
private MasterElement(int elementId, long elementEndPosition) {
this.elementId = elementId;
this.elementEndPosition = elementEndPosition;
}
}
}