538 lines
17 KiB
Java
538 lines
17 KiB
Java
package org.schabi.newpipe.streams;
|
|
|
|
import org.schabi.newpipe.streams.io.SharpStream;
|
|
|
|
import java.io.EOFException;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.NoSuchElementException;
|
|
|
|
/**
|
|
*
|
|
* @author kapodamy
|
|
*/
|
|
public class WebMReader {
|
|
private static final int ID_EMBL = 0x0A45DFA3;
|
|
private static final int ID_EMBL_READ_VERSION = 0x02F7;
|
|
private static final int ID_EMBL_DOC_TYPE = 0x0282;
|
|
private static final int ID_EMBL_DOC_TYPE_READ_VERSION = 0x0285;
|
|
|
|
private static final int ID_SEGMENT = 0x08538067;
|
|
|
|
private static final int ID_INFO = 0x0549A966;
|
|
private static final int ID_TIMECODE_SCALE = 0x0AD7B1;
|
|
private static final int ID_DURATION = 0x489;
|
|
|
|
private static final int ID_TRACKS = 0x0654AE6B;
|
|
private static final int ID_TRACK_ENTRY = 0x2E;
|
|
private static final int ID_TRACK_NUMBER = 0x57;
|
|
private static final int ID_TRACK_TYPE = 0x03;
|
|
private static final int ID_CODEC_ID = 0x06;
|
|
private static final int ID_CODEC_PRIVATE = 0x23A2;
|
|
private static final int ID_VIDEO = 0x60;
|
|
private static final int ID_AUDIO = 0x61;
|
|
private static final int ID_DEFAULT_DURATION = 0x3E383;
|
|
private static final int ID_FLAG_LACING = 0x1C;
|
|
private static final int ID_CODEC_DELAY = 0x16AA;
|
|
private static final int ID_SEEK_PRE_ROLL = 0x16BB;
|
|
|
|
private static final int ID_CLUSTER = 0x0F43B675;
|
|
private static final int ID_TIMECODE = 0x67;
|
|
private static final int ID_SIMPLE_BLOCK = 0x23;
|
|
private static final int ID_BLOCK = 0x21;
|
|
private static final int ID_GROUP_BLOCK = 0x20;
|
|
|
|
|
|
public enum TrackKind {
|
|
Audio/*2*/, Video/*1*/, Other
|
|
}
|
|
|
|
private final DataReader stream;
|
|
private Segment segment;
|
|
private WebMTrack[] tracks;
|
|
private int selectedTrack;
|
|
private boolean done;
|
|
private boolean firstSegment;
|
|
|
|
public WebMReader(final SharpStream source) {
|
|
this.stream = new DataReader(source);
|
|
}
|
|
|
|
public void parse() throws IOException {
|
|
Element elem = readElement(ID_EMBL);
|
|
if (!readEbml(elem, 1, 2)) {
|
|
throw new UnsupportedOperationException("Unsupported EBML data (WebM)");
|
|
}
|
|
ensure(elem);
|
|
|
|
elem = untilElement(null, ID_SEGMENT);
|
|
if (elem == null) {
|
|
throw new IOException("Fragment element not found");
|
|
}
|
|
segment = readSegment(elem, 0, true);
|
|
tracks = segment.tracks;
|
|
selectedTrack = -1;
|
|
done = false;
|
|
firstSegment = true;
|
|
}
|
|
|
|
public WebMTrack[] getAvailableTracks() {
|
|
return tracks;
|
|
}
|
|
|
|
public WebMTrack selectTrack(final int index) {
|
|
selectedTrack = index;
|
|
return tracks[index];
|
|
}
|
|
|
|
public Segment getNextSegment() throws IOException {
|
|
if (done) {
|
|
return null;
|
|
}
|
|
|
|
if (firstSegment && segment != null) {
|
|
firstSegment = false;
|
|
return segment;
|
|
}
|
|
|
|
ensure(segment.ref);
|
|
// WARNING: track cannot be the same or have different index in new segments
|
|
final Element elem = untilElement(null, ID_SEGMENT);
|
|
if (elem == null) {
|
|
done = true;
|
|
return null;
|
|
}
|
|
segment = readSegment(elem, 0, false);
|
|
|
|
return segment;
|
|
}
|
|
|
|
private long readNumber(final Element parent) throws IOException {
|
|
int length = (int) parent.contentSize;
|
|
long value = 0;
|
|
while (length-- > 0) {
|
|
final int read = stream.read();
|
|
if (read == -1) {
|
|
throw new EOFException();
|
|
}
|
|
value = (value << 8) | read;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private String readString(final Element parent) throws IOException {
|
|
return new String(readBlob(parent), StandardCharsets.UTF_8); // or use "utf-8"
|
|
}
|
|
|
|
private byte[] readBlob(final Element parent) throws IOException {
|
|
final long length = parent.contentSize;
|
|
final byte[] buffer = new byte[(int) length];
|
|
final int read = stream.read(buffer);
|
|
if (read < length) {
|
|
throw new EOFException();
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
private long readEncodedNumber() throws IOException {
|
|
int value = stream.read();
|
|
|
|
if (value > 0) {
|
|
byte size = 1;
|
|
int mask = 0x80;
|
|
|
|
while (size < 9) {
|
|
if ((value & mask) == mask) {
|
|
mask = 0xFF;
|
|
mask >>= size;
|
|
|
|
long number = value & mask;
|
|
|
|
for (int i = 1; i < size; i++) {
|
|
value = stream.read();
|
|
number <<= 8;
|
|
number |= value;
|
|
}
|
|
|
|
return number;
|
|
}
|
|
|
|
mask >>= 1;
|
|
size++;
|
|
}
|
|
}
|
|
|
|
throw new IOException("Invalid encoded length");
|
|
}
|
|
|
|
private Element readElement() throws IOException {
|
|
final Element elem = new Element();
|
|
elem.offset = stream.position();
|
|
elem.type = (int) readEncodedNumber();
|
|
elem.contentSize = readEncodedNumber();
|
|
elem.size = elem.contentSize + stream.position() - elem.offset;
|
|
|
|
return elem;
|
|
}
|
|
|
|
private Element readElement(final int expected) throws IOException {
|
|
final Element elem = readElement();
|
|
if (expected != 0 && elem.type != expected) {
|
|
throw new NoSuchElementException("expected " + elementID(expected)
|
|
+ " found " + elementID(elem.type));
|
|
}
|
|
|
|
return elem;
|
|
}
|
|
|
|
private Element untilElement(final Element ref, final int... expected) throws IOException {
|
|
Element elem;
|
|
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
|
elem = readElement();
|
|
if (expected.length < 1) {
|
|
return elem;
|
|
}
|
|
for (final int type : expected) {
|
|
if (elem.type == type) {
|
|
return elem;
|
|
}
|
|
}
|
|
|
|
ensure(elem);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private String elementID(final long type) {
|
|
return "0x".concat(Long.toHexString(type));
|
|
}
|
|
|
|
private void ensure(final Element ref) throws IOException {
|
|
final long skip = (ref.offset + ref.size) - stream.position();
|
|
|
|
if (skip == 0) {
|
|
return;
|
|
} else if (skip < 0) {
|
|
throw new EOFException(String.format(
|
|
"parser go beyond limits of the Element. type=%s offset=%s size=%s position=%s",
|
|
elementID(ref.type), ref.offset, ref.size, stream.position()
|
|
));
|
|
}
|
|
|
|
stream.skipBytes(skip);
|
|
}
|
|
|
|
private boolean readEbml(final Element ref, final int minReadVersion,
|
|
final int minDocTypeVersion) throws IOException {
|
|
Element elem = untilElement(ref, ID_EMBL_READ_VERSION);
|
|
if (elem == null) {
|
|
return false;
|
|
}
|
|
if (readNumber(elem) > minReadVersion) {
|
|
return false;
|
|
}
|
|
|
|
elem = untilElement(ref, ID_EMBL_DOC_TYPE);
|
|
if (elem == null) {
|
|
return false;
|
|
}
|
|
if (!readString(elem).equals("webm")) {
|
|
return false;
|
|
}
|
|
elem = untilElement(ref, ID_EMBL_DOC_TYPE_READ_VERSION);
|
|
|
|
return elem != null && readNumber(elem) <= minDocTypeVersion;
|
|
}
|
|
|
|
private Info readInfo(final Element ref) throws IOException {
|
|
Element elem;
|
|
final Info info = new Info();
|
|
|
|
while ((elem = untilElement(ref, ID_TIMECODE_SCALE, ID_DURATION)) != null) {
|
|
switch (elem.type) {
|
|
case ID_TIMECODE_SCALE:
|
|
info.timecodeScale = readNumber(elem);
|
|
break;
|
|
case ID_DURATION:
|
|
info.duration = readNumber(elem);
|
|
break;
|
|
}
|
|
ensure(elem);
|
|
}
|
|
|
|
if (info.timecodeScale == 0) {
|
|
throw new NoSuchElementException("Element Timecode not found");
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
private Segment readSegment(final Element ref, final int trackLacingExpected,
|
|
final boolean metadataExpected) throws IOException {
|
|
final Segment obj = new Segment(ref);
|
|
Element elem;
|
|
while ((elem = untilElement(ref, ID_INFO, ID_TRACKS, ID_CLUSTER)) != null) {
|
|
if (elem.type == ID_CLUSTER) {
|
|
obj.currentCluster = elem;
|
|
break;
|
|
}
|
|
switch (elem.type) {
|
|
case ID_INFO:
|
|
obj.info = readInfo(elem);
|
|
break;
|
|
case ID_TRACKS:
|
|
obj.tracks = readTracks(elem, trackLacingExpected);
|
|
break;
|
|
}
|
|
ensure(elem);
|
|
}
|
|
|
|
if (metadataExpected && (obj.info == null || obj.tracks == null)) {
|
|
throw new RuntimeException(
|
|
"Cluster element found without Info and/or Tracks element at position "
|
|
+ ref.offset);
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
private WebMTrack[] readTracks(final Element ref, final int lacingExpected) throws IOException {
|
|
final ArrayList<WebMTrack> trackEntries = new ArrayList<>(2);
|
|
Element elemTrackEntry;
|
|
|
|
while ((elemTrackEntry = untilElement(ref, ID_TRACK_ENTRY)) != null) {
|
|
final WebMTrack entry = new WebMTrack();
|
|
boolean drop = false;
|
|
Element elem;
|
|
while ((elem = untilElement(elemTrackEntry)) != null) {
|
|
switch (elem.type) {
|
|
case ID_TRACK_NUMBER:
|
|
entry.trackNumber = readNumber(elem);
|
|
break;
|
|
case ID_TRACK_TYPE:
|
|
entry.trackType = (int) readNumber(elem);
|
|
break;
|
|
case ID_CODEC_ID:
|
|
entry.codecId = readString(elem);
|
|
break;
|
|
case ID_CODEC_PRIVATE:
|
|
entry.codecPrivate = readBlob(elem);
|
|
break;
|
|
case ID_AUDIO:
|
|
case ID_VIDEO:
|
|
entry.bMetadata = readBlob(elem);
|
|
break;
|
|
case ID_DEFAULT_DURATION:
|
|
entry.defaultDuration = readNumber(elem);
|
|
break;
|
|
case ID_FLAG_LACING:
|
|
drop = readNumber(elem) != lacingExpected;
|
|
break;
|
|
case ID_CODEC_DELAY:
|
|
entry.codecDelay = readNumber(elem);
|
|
break;
|
|
case ID_SEEK_PRE_ROLL:
|
|
entry.seekPreRoll = readNumber(elem);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
ensure(elem);
|
|
}
|
|
if (!drop) {
|
|
trackEntries.add(entry);
|
|
}
|
|
ensure(elemTrackEntry);
|
|
}
|
|
|
|
final WebMTrack[] entries = trackEntries.toArray(new WebMTrack[0]);
|
|
|
|
for (final WebMTrack entry : entries) {
|
|
switch (entry.trackType) {
|
|
case 1:
|
|
entry.kind = TrackKind.Video;
|
|
break;
|
|
case 2:
|
|
entry.kind = TrackKind.Audio;
|
|
break;
|
|
default:
|
|
entry.kind = TrackKind.Other;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
private SimpleBlock readSimpleBlock(final Element ref) throws IOException {
|
|
final SimpleBlock obj = new SimpleBlock(ref);
|
|
obj.trackNumber = readEncodedNumber();
|
|
obj.relativeTimeCode = stream.readShort();
|
|
obj.flags = (byte) stream.read();
|
|
obj.dataSize = (int) ((ref.offset + ref.size) - stream.position());
|
|
obj.createdFromBlock = ref.type == ID_BLOCK;
|
|
|
|
// NOTE: lacing is not implemented, and will be mixed with the stream data
|
|
if (obj.dataSize < 0) {
|
|
throw new IOException(String.format(
|
|
"Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
private Cluster readCluster(final Element ref) throws IOException {
|
|
final Cluster obj = new Cluster(ref);
|
|
|
|
final Element elem = untilElement(ref, ID_TIMECODE);
|
|
if (elem == null) {
|
|
throw new NoSuchElementException("Cluster at " + ref.offset
|
|
+ " without Timecode element");
|
|
}
|
|
obj.timecode = readNumber(elem);
|
|
|
|
return obj;
|
|
}
|
|
|
|
static class Element {
|
|
int type;
|
|
long offset;
|
|
long contentSize;
|
|
long size;
|
|
}
|
|
|
|
public static class Info {
|
|
public long timecodeScale;
|
|
public long duration;
|
|
}
|
|
|
|
public static class WebMTrack {
|
|
public long trackNumber;
|
|
protected int trackType;
|
|
public String codecId;
|
|
public byte[] codecPrivate;
|
|
public byte[] bMetadata;
|
|
public TrackKind kind;
|
|
public long defaultDuration = -1;
|
|
public long codecDelay = -1;
|
|
public long seekPreRoll = -1;
|
|
}
|
|
|
|
public class Segment {
|
|
Segment(final Element ref) {
|
|
this.ref = ref;
|
|
this.firstClusterInSegment = true;
|
|
}
|
|
|
|
public Info info;
|
|
WebMTrack[] tracks;
|
|
private Element currentCluster;
|
|
private final Element ref;
|
|
boolean firstClusterInSegment;
|
|
|
|
public Cluster getNextCluster() throws IOException {
|
|
if (done) {
|
|
return null;
|
|
}
|
|
if (firstClusterInSegment && segment.currentCluster != null) {
|
|
firstClusterInSegment = false;
|
|
return readCluster(segment.currentCluster);
|
|
}
|
|
ensure(segment.currentCluster);
|
|
|
|
final Element elem = untilElement(segment.ref, ID_CLUSTER);
|
|
if (elem == null) {
|
|
return null;
|
|
}
|
|
|
|
segment.currentCluster = elem;
|
|
|
|
return readCluster(segment.currentCluster);
|
|
}
|
|
}
|
|
|
|
public static class SimpleBlock {
|
|
public InputStream data;
|
|
public boolean createdFromBlock;
|
|
|
|
SimpleBlock(final Element ref) {
|
|
this.ref = ref;
|
|
}
|
|
|
|
public long trackNumber;
|
|
public short relativeTimeCode;
|
|
public long absoluteTimeCodeNs;
|
|
public byte flags;
|
|
public int dataSize;
|
|
private final Element ref;
|
|
|
|
public boolean isKeyframe() {
|
|
return (flags & 0x80) == 0x80;
|
|
}
|
|
}
|
|
|
|
public class Cluster {
|
|
Element ref;
|
|
SimpleBlock currentSimpleBlock = null;
|
|
Element currentBlockGroup = null;
|
|
public long timecode;
|
|
|
|
Cluster(final Element ref) {
|
|
this.ref = ref;
|
|
}
|
|
|
|
boolean insideClusterBounds() {
|
|
return stream.position() >= (ref.offset + ref.size);
|
|
}
|
|
|
|
public SimpleBlock getNextSimpleBlock() throws IOException {
|
|
if (insideClusterBounds()) {
|
|
return null;
|
|
}
|
|
|
|
if (currentBlockGroup != null) {
|
|
ensure(currentBlockGroup);
|
|
currentBlockGroup = null;
|
|
currentSimpleBlock = null;
|
|
} else if (currentSimpleBlock != null) {
|
|
ensure(currentSimpleBlock.ref);
|
|
}
|
|
|
|
while (!insideClusterBounds()) {
|
|
Element elem = untilElement(ref, ID_SIMPLE_BLOCK, ID_GROUP_BLOCK);
|
|
if (elem == null) {
|
|
return null;
|
|
}
|
|
|
|
if (elem.type == ID_GROUP_BLOCK) {
|
|
currentBlockGroup = elem;
|
|
elem = untilElement(currentBlockGroup, ID_BLOCK);
|
|
|
|
if (elem == null) {
|
|
ensure(currentBlockGroup);
|
|
currentBlockGroup = null;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
currentSimpleBlock = readSimpleBlock(elem);
|
|
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
|
currentSimpleBlock.data = stream.getView(currentSimpleBlock.dataSize);
|
|
|
|
// calculate the timestamp in nanoseconds
|
|
currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode
|
|
+ this.timecode;
|
|
currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale;
|
|
|
|
return currentSimpleBlock;
|
|
}
|
|
|
|
ensure(elem);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|