diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java
new file mode 100644
index 000000000..d0e946eb7
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java
@@ -0,0 +1,103 @@
+package org.schabi.newpipe.streams;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+
+/**
+ * @author kapodamy
+ */
+public class DataReader {
+
+ public final static int SHORT_SIZE = 2;
+ public final static int LONG_SIZE = 8;
+ public final static int INTEGER_SIZE = 4;
+ public final static int FLOAT_SIZE = 4;
+
+ private long pos;
+ public final SharpStream stream;
+ private final boolean rewind;
+
+ public DataReader(SharpStream stream) {
+ this.rewind = stream.canRewind();
+ this.stream = stream;
+ this.pos = 0L;
+ }
+
+ public long position() {
+ return pos;
+ }
+
+ public final int readInt() throws IOException {
+ primitiveRead(INTEGER_SIZE);
+ return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
+ }
+
+ public final int read() throws IOException {
+ int value = stream.read();
+ if (value == -1) {
+ throw new EOFException();
+ }
+
+ pos++;
+ return value;
+ }
+
+ public final long skipBytes(long amount) throws IOException {
+ amount = stream.skip(amount);
+ pos += amount;
+ return amount;
+ }
+
+ public final long readLong() throws IOException {
+ primitiveRead(LONG_SIZE);
+ long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
+ long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
+ return high << 32 | low;
+ }
+
+ public final short readShort() throws IOException {
+ primitiveRead(SHORT_SIZE);
+ return (short) (primitive[0] << 8 | primitive[1]);
+ }
+
+ public final int read(byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ public final int read(byte[] buffer, int offset, int count) throws IOException {
+ int res = stream.read(buffer, offset, count);
+ pos += res;
+
+ return res;
+ }
+
+ public final boolean available() {
+ return stream.available() > 0;
+ }
+
+ public void rewind() throws IOException {
+ stream.rewind();
+ pos = 0;
+ }
+
+ public boolean canRewind() {
+ return rewind;
+ }
+
+ private short[] primitive = new short[LONG_SIZE];
+
+ private void primitiveRead(int amount) throws IOException {
+ byte[] buffer = new byte[amount];
+ int read = stream.read(buffer, 0, amount);
+ pos += read;
+ if (read != amount) {
+ throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
+ }
+
+ for (int i = 0; i < buffer.length; i++) {
+ primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
new file mode 100644
index 000000000..ec2419734
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
@@ -0,0 +1,817 @@
+package org.schabi.newpipe.streams;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+import java.nio.ByteBuffer;
+
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+
+/**
+ * @author kapodamy
+ */
+public class Mp4DashReader {
+
+ //
+ private static final int ATOM_MOOF = 0x6D6F6F66;
+ private static final int ATOM_MFHD = 0x6D666864;
+ private static final int ATOM_TRAF = 0x74726166;
+ private static final int ATOM_TFHD = 0x74666864;
+ private static final int ATOM_TFDT = 0x74666474;
+ private static final int ATOM_TRUN = 0x7472756E;
+ private static final int ATOM_MDIA = 0x6D646961;
+ private static final int ATOM_FTYP = 0x66747970;
+ private static final int ATOM_SIDX = 0x73696478;
+ private static final int ATOM_MOOV = 0x6D6F6F76;
+ private static final int ATOM_MDAT = 0x6D646174;
+ private static final int ATOM_MVHD = 0x6D766864;
+ private static final int ATOM_TRAK = 0x7472616B;
+ private static final int ATOM_MVEX = 0x6D766578;
+ private static final int ATOM_TREX = 0x74726578;
+ private static final int ATOM_TKHD = 0x746B6864;
+ private static final int ATOM_MFRA = 0x6D667261;
+ private static final int ATOM_TFRA = 0x74667261;
+ private static final int ATOM_MDHD = 0x6D646864;
+ private static final int BRAND_DASH = 0x64617368;
+ //
+
+ private final DataReader stream;
+
+ private Mp4Track[] tracks = null;
+
+ private Box box;
+ private Moof moof;
+
+ private boolean chunkZero = false;
+
+ private int selectedTrack = -1;
+
+ public enum TrackKind {
+ Audio, Video, Other
+ }
+
+ public Mp4DashReader(SharpStream source) {
+ this.stream = new DataReader(source);
+ }
+
+ public void parse() throws IOException, NoSuchElementException {
+ if (selectedTrack > -1) {
+ return;
+ }
+
+ box = readBox(ATOM_FTYP);
+ if (parse_ftyp() != BRAND_DASH) {
+ throw new NoSuchElementException("Main Brand is not dash");
+ }
+
+ Moov moov = null;
+ int i;
+
+ while (box.type != ATOM_MOOF) {
+ ensure(box);
+ box = readBox();
+
+ switch (box.type) {
+ case ATOM_MOOV:
+ moov = parse_moov(box);
+ break;
+ case ATOM_SIDX:
+ break;
+ case ATOM_MFRA:
+ break;
+ case ATOM_MDAT:
+ throw new IOException("Expected moof, found mdat");
+ }
+ }
+
+ if (moov == null) {
+ throw new IOException("The provided Mp4 doesn't have the 'moov' box");
+ }
+
+ tracks = new Mp4Track[moov.trak.length];
+
+ for (i = 0; i < tracks.length; i++) {
+ tracks[i] = new Mp4Track();
+ tracks[i].trak = moov.trak[i];
+
+ if (moov.mvex_trex != null) {
+ for (Trex mvex_trex : moov.mvex_trex) {
+ if (tracks[i].trak.tkhd.trackId == mvex_trex.trackId) {
+ tracks[i].trex = mvex_trex;
+ }
+ }
+ }
+
+ if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) {
+ tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
+ } else {
+ tracks[i].kind = TrackKind.Video;
+ }
+ }
+ }
+
+ public Mp4Track selectTrack(int index) {
+ selectedTrack = index;
+ return tracks[index];
+ }
+
+ /**
+ * Count all fragments present. This operation requires a seekable stream
+ *
+ * @return list with a basic info
+ * @throws IOException if the source stream is not seekeable
+ */
+ public int getFragmentsCount() throws IOException {
+ if (selectedTrack < 0) {
+ throw new IllegalStateException("track no selected");
+ }
+ if (!stream.canRewind()) {
+ throw new IOException("The provided stream doesn't allow seek");
+ }
+
+ Box tmp;
+ int count = 0;
+ long orig_offset = stream.position();
+
+ if (box.type == ATOM_MOOF) {
+ tmp = box;
+ } else {
+ ensure(box);
+ tmp = readBox();
+ }
+
+ do {
+ if (tmp.type == ATOM_MOOF) {
+ ensure(readBox(ATOM_MFHD));
+ Box traf;
+ while ((traf = untilBox(tmp, ATOM_TRAF)) != null) {
+ Box tfhd = readBox(ATOM_TFHD);
+ if (parse_tfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) {
+ count++;
+ break;
+ }
+ ensure(tfhd);
+ ensure(traf);
+ }
+ }
+ ensure(tmp);
+ } while (stream.available() && (tmp = readBox()) != null);
+
+ stream.rewind();
+ stream.skipBytes((int) orig_offset);
+
+ return count;
+ }
+
+ public Mp4Track[] getAvailableTracks() {
+ return tracks;
+ }
+
+ public Mp4TrackChunk getNextChunk() throws IOException {
+ Mp4Track track = tracks[selectedTrack];
+
+ while (stream.available()) {
+
+ if (chunkZero) {
+ ensure(box);
+ if (!stream.available()) {
+ break;
+ }
+ box = readBox();
+ } else {
+ chunkZero = true;
+ }
+
+ switch (box.type) {
+ case ATOM_MOOF:
+ if (moof != null) {
+ throw new IOException("moof found without mdat");
+ }
+
+ moof = parse_moof(box, track.trak.tkhd.trackId);
+
+ if (moof.traf != null) {
+
+ if (hasFlag(moof.traf.trun.bFlags, 0x0001)) {
+ moof.traf.trun.dataOffset -= box.size + 8;
+ if (moof.traf.trun.dataOffset < 0) {
+ throw new IOException("trun box has wrong data offset, points outside of concurrent mdat box");
+ }
+ }
+
+ if (moof.traf.trun.chunkSize < 1) {
+ if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
+ moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
+ } else {
+ moof.traf.trun.chunkSize = box.size - 8;
+ }
+ }
+ if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
+ if (hasFlag(moof.traf.tfhd.bFlags, 0x20)) {
+ moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration * moof.traf.trun.entryCount;
+ }
+ }
+ }
+ break;
+ case ATOM_MDAT:
+ if (moof == null) {
+ throw new IOException("mdat found without moof");
+ }
+
+ if (moof.traf == null) {
+ moof = null;
+ continue;// find another chunk
+ }
+
+ Mp4TrackChunk chunk = new Mp4TrackChunk();
+ chunk.moof = moof;
+ chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
+ moof = null;
+
+ stream.skipBytes(chunk.moof.traf.trun.dataOffset);
+ return chunk;
+ default:
+ }
+ }
+
+ return null;
+ }
+
+ //
+ private long readUint() throws IOException {
+ return stream.readInt() & 0xffffffffL;
+ }
+
+ public static boolean hasFlag(int flags, int mask) {
+ return (flags & mask) == mask;
+ }
+
+ private String boxName(Box ref) {
+ return boxName(ref.type);
+ }
+
+ private String boxName(int type) {
+ try {
+ return new String(ByteBuffer.allocate(4).putInt(type).array(), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ return "0x" + Integer.toHexString(type);
+ }
+ }
+
+ private Box readBox() throws IOException {
+ Box b = new Box();
+ b.offset = stream.position();
+ b.size = stream.readInt();
+ b.type = stream.readInt();
+
+ return b;
+ }
+
+ private Box readBox(int expected) throws IOException {
+ Box b = readBox();
+ if (b.type != expected) {
+ throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b));
+ }
+ return b;
+ }
+
+ private void ensure(Box ref) throws IOException {
+ 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 box. type=%s offset=%s size=%s position=%s",
+ boxName(ref), ref.offset, ref.size, stream.position()
+ ));
+ }
+
+ stream.skipBytes((int) skip);
+ }
+
+ private Box untilBox(Box ref, int... expected) throws IOException {
+ Box b;
+ while (stream.position() < (ref.offset + ref.size)) {
+ b = readBox();
+ for (int type : expected) {
+ if (b.type == type) {
+ return b;
+ }
+ }
+ ensure(b);
+ }
+
+ return null;
+ }
+
+ //
+
+ //
+
+ private Moof parse_moof(Box ref, int trackId) throws IOException {
+ Moof obj = new Moof();
+
+ Box b = readBox(ATOM_MFHD);
+ obj.mfhd_SequenceNumber = parse_mfhd();
+ ensure(b);
+
+ while ((b = untilBox(ref, ATOM_TRAF)) != null) {
+ obj.traf = parse_traf(b, trackId);
+ ensure(b);
+
+ if (obj.traf != null) {
+ return obj;
+ }
+ }
+
+ return obj;
+ }
+
+ private int parse_mfhd() throws IOException {
+ // version
+ // flags
+ stream.skipBytes(4);
+
+ return stream.readInt();
+ }
+
+ private Traf parse_traf(Box ref, int trackId) throws IOException {
+ Traf traf = new Traf();
+
+ Box b = readBox(ATOM_TFHD);
+ traf.tfhd = parse_tfhd(trackId);
+ ensure(b);
+
+ if (traf.tfhd == null) {
+ return null;
+ }
+
+ b = untilBox(ref, ATOM_TRUN, ATOM_TFDT);
+
+ if (b.type == ATOM_TFDT) {
+ traf.tfdt = parse_tfdt();
+ ensure(b);
+ b = readBox(ATOM_TRUN);
+ }
+
+ traf.trun = parse_trun();
+ ensure(b);
+
+ return traf;
+ }
+
+ private Tfhd parse_tfhd(int trackId) throws IOException {
+ Tfhd obj = new Tfhd();
+
+ obj.bFlags = stream.readInt();
+ obj.trackId = stream.readInt();
+
+ if (trackId != -1 && obj.trackId != trackId) {
+ return null;
+ }
+
+ if (hasFlag(obj.bFlags, 0x01)) {
+ stream.skipBytes(8);
+ }
+ if (hasFlag(obj.bFlags, 0x02)) {
+ stream.skipBytes(4);
+ }
+ if (hasFlag(obj.bFlags, 0x08)) {
+ obj.defaultSampleDuration = stream.readInt();
+ }
+ if (hasFlag(obj.bFlags, 0x10)) {
+ obj.defaultSampleSize = stream.readInt();
+ }
+ if (hasFlag(obj.bFlags, 0x20)) {
+ obj.defaultSampleFlags = stream.readInt();
+ }
+
+ return obj;
+ }
+
+ private long parse_tfdt() throws IOException {
+ int version = stream.read();
+ stream.skipBytes(3);// flags
+ return version == 0 ? readUint() : stream.readLong();
+ }
+
+ private Trun parse_trun() throws IOException {
+ Trun obj = new Trun();
+ obj.bFlags = stream.readInt();
+ obj.entryCount = stream.readInt();// unsigned int
+
+ obj.entries_rowSize = 0;
+ if (hasFlag(obj.bFlags, 0x0100)) {
+ obj.entries_rowSize += 4;
+ }
+ if (hasFlag(obj.bFlags, 0x0200)) {
+ obj.entries_rowSize += 4;
+ }
+ if (hasFlag(obj.bFlags, 0x0400)) {
+ obj.entries_rowSize += 4;
+ }
+ if (hasFlag(obj.bFlags, 0x0800)) {
+ obj.entries_rowSize += 4;
+ }
+ obj.bEntries = new byte[obj.entries_rowSize * obj.entryCount];
+
+ if (hasFlag(obj.bFlags, 0x0001)) {
+ obj.dataOffset = stream.readInt();
+ }
+ if (hasFlag(obj.bFlags, 0x0004)) {
+ obj.bFirstSampleFlags = stream.readInt();
+ }
+
+ stream.read(obj.bEntries);
+
+ for (int i = 0; i < obj.entryCount; i++) {
+ TrunEntry entry = obj.getEntry(i);
+ if (hasFlag(obj.bFlags, 0x0100)) {
+ obj.chunkDuration += entry.sampleDuration;
+ }
+ if (hasFlag(obj.bFlags, 0x0200)) {
+ obj.chunkSize += entry.sampleSize;
+ }
+ if (hasFlag(obj.bFlags, 0x0800)) {
+ if (!hasFlag(obj.bFlags, 0x0100)) {
+ obj.chunkDuration += entry.sampleCompositionTimeOffset;
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ private int parse_ftyp() throws IOException {
+ int brand = stream.readInt();
+ stream.skipBytes(4);// minor version
+
+ return brand;
+ }
+
+ private Mvhd parse_mvhd() throws IOException {
+ int version = stream.read();
+ stream.skipBytes(3);// flags
+
+ // creation entries_time
+ // modification entries_time
+ stream.skipBytes(2 * (version == 0 ? 4 : 8));
+
+ Mvhd obj = new Mvhd();
+ obj.timeScale = readUint();
+
+ // chunkDuration
+ stream.skipBytes(version == 0 ? 4 : 8);
+
+ // rate
+ // volume
+ // reserved
+ // matrix array
+ // predefined
+ stream.skipBytes(76);
+
+ obj.nextTrackId = readUint();
+
+ return obj;
+ }
+
+ private Tkhd parse_tkhd() throws IOException {
+ int version = stream.read();
+
+ Tkhd obj = new Tkhd();
+
+ // flags
+ // creation entries_time
+ // modification entries_time
+ stream.skipBytes(3 + (2 * (version == 0 ? 4 : 8)));
+
+ obj.trackId = stream.readInt();
+
+ stream.skipBytes(4);// reserved
+
+ obj.duration = version == 0 ? readUint() : stream.readLong();
+
+ stream.skipBytes(2 * 4);// reserved
+
+ obj.bLayer = stream.readShort();
+ obj.bAlternateGroup = stream.readShort();
+ obj.bVolume = stream.readShort();
+
+ stream.skipBytes(2);// reserved
+
+ obj.matrix = new byte[9 * 4];
+ stream.read(obj.matrix);
+
+ obj.bWidth = stream.readInt();
+ obj.bHeight = stream.readInt();
+
+ return obj;
+ }
+
+ private Trak parse_trak(Box ref) throws IOException {
+ Trak trak = new Trak();
+
+ Box b = readBox(ATOM_TKHD);
+ trak.tkhd = parse_tkhd();
+ ensure(b);
+
+ b = untilBox(ref, ATOM_MDIA);
+ trak.mdia = new byte[b.size];
+
+ ByteBuffer buffer = ByteBuffer.wrap(trak.mdia);
+ buffer.putInt(b.size);
+ buffer.putInt(ATOM_MDIA);
+ stream.read(trak.mdia, 8, b.size - 8);
+
+ trak.mdia_mdhd_timeScale = parse_mdia(buffer);
+
+ return trak;
+ }
+
+ private int parse_mdia(ByteBuffer data) {
+ while (data.hasRemaining()) {
+ int end = data.position() + data.getInt();
+ if (data.getInt() == ATOM_MDHD) {
+ byte version = data.get();
+ data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
+ return data.getInt();
+ }
+
+ data.position(end);
+ }
+
+ return 0;// this NEVER should happen
+ }
+
+ private Moov parse_moov(Box ref) throws IOException {
+ Box b = readBox(ATOM_MVHD);
+ Moov moov = new Moov();
+ moov.mvhd = parse_mvhd();
+ ensure(b);
+
+ ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId);
+ while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) {
+
+ switch (b.type) {
+ case ATOM_TRAK:
+ tmp.add(parse_trak(b));
+ break;
+ case ATOM_MVEX:
+ moov.mvex_trex = parse_mvex(b, (int) moov.mvhd.nextTrackId);
+ break;
+ }
+
+ ensure(b);
+ }
+
+ moov.trak = tmp.toArray(new Trak[tmp.size()]);
+
+ return moov;
+ }
+
+ private Trex[] parse_mvex(Box ref, int possibleTrackCount) throws IOException {
+ ArrayList tmp = new ArrayList<>(possibleTrackCount);
+
+ Box b;
+ while ((b = untilBox(ref, ATOM_TREX)) != null) {
+ tmp.add(parse_trex());
+ ensure(b);
+ }
+
+ return tmp.toArray(new Trex[tmp.size()]);
+ }
+
+ private Trex parse_trex() throws IOException {
+ // version
+ // flags
+ stream.skipBytes(4);
+
+ Trex obj = new Trex();
+ obj.trackId = stream.readInt();
+ obj.defaultSampleDescriptionIndex = stream.readInt();
+ obj.defaultSampleDuration = stream.readInt();
+ obj.defaultSampleSize = stream.readInt();
+ obj.defaultSampleFlags = stream.readInt();
+
+ return obj;
+ }
+
+ private Tfra parse_tfra() throws IOException {
+ int version = stream.read();
+
+ stream.skipBytes(3);// flags
+
+ Tfra tfra = new Tfra();
+ tfra.trackId = stream.readInt();
+
+ stream.skipBytes(3);// reserved
+ int bFlags = stream.read();
+ int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
+
+ tfra.entries_time = new int[stream.readInt()];
+
+ for (int i = 0; i < tfra.entries_time.length; i++) {
+ tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
+ stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
+ }
+
+ return tfra;
+ }
+
+ private Sidx parse_sidx() throws IOException {
+ int version = stream.read();
+
+ stream.skipBytes(3);// flags
+
+ Sidx obj = new Sidx();
+ obj.referenceId = stream.readInt();
+ obj.timescale = stream.readInt();
+
+ // earliest presentation entries_time
+ // first offset
+ // reserved
+ stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
+
+ obj.entries_subsegmentDuration = new int[stream.readShort()];
+
+ for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) {
+ // reference type
+ // referenced size
+ stream.skipBytes(4);
+ obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
+
+ // starts with SAP
+ // SAP type
+ // SAP delta entries_time
+ stream.skipBytes(4);
+ }
+
+ return obj;
+ }
+
+ private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
+ ArrayList tmp = new ArrayList<>(trackCount);
+ long limit = ref.offset + ref.size;
+
+ while (stream.position() < limit) {
+ box = readBox();
+
+ if (box.type == ATOM_TFRA) {
+ tmp.add(parse_tfra());
+ }
+
+ ensure(box);
+ }
+
+ return tmp.toArray(new Tfra[tmp.size()]);
+ }
+
+ //
+
+ //
+ class Box {
+
+ int type;
+ long offset;
+ int size;
+ }
+
+ class Sidx {
+
+ int timescale;
+ int referenceId;
+ int[] entries_subsegmentDuration;
+ }
+
+ public class Moof {
+
+ int mfhd_SequenceNumber;
+ public Traf traf;
+ }
+
+ public class Traf {
+
+ public Tfhd tfhd;
+ long tfdt;
+ public Trun trun;
+ }
+
+ public class Tfhd {
+
+ int bFlags;
+ public int trackId;
+ int defaultSampleDuration;
+ int defaultSampleSize;
+ int defaultSampleFlags;
+ }
+
+ public class TrunEntry {
+
+ public int sampleDuration;
+ public int sampleSize;
+ public int sampleFlags;
+ public int sampleCompositionTimeOffset;
+ }
+
+ public class Trun {
+
+ public int chunkDuration;
+ public int chunkSize;
+
+ public int bFlags;
+ int bFirstSampleFlags;
+ int dataOffset;
+
+ public int entryCount;
+ byte[] bEntries;
+ int entries_rowSize;
+
+ public TrunEntry getEntry(int i) {
+ ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entries_rowSize, entries_rowSize);
+ TrunEntry entry = new TrunEntry();
+
+ if (hasFlag(bFlags, 0x0100)) {
+ entry.sampleDuration = buffer.getInt();
+ }
+ if (hasFlag(bFlags, 0x0200)) {
+ entry.sampleSize = buffer.getInt();
+ }
+ if (hasFlag(bFlags, 0x0400)) {
+ entry.sampleFlags = buffer.getInt();
+ }
+ if (hasFlag(bFlags, 0x0800)) {
+ entry.sampleCompositionTimeOffset = buffer.getInt();
+ }
+
+ return entry;
+ }
+ }
+
+ public class Tkhd {
+
+ int trackId;
+ long duration;
+ short bVolume;
+ int bWidth;
+ int bHeight;
+ byte[] matrix;
+ short bLayer;
+ short bAlternateGroup;
+ }
+
+ public class Trak {
+
+ public Tkhd tkhd;
+ public int mdia_mdhd_timeScale;
+
+ byte[] mdia;
+ }
+
+ class Mvhd {
+
+ long timeScale;
+ long nextTrackId;
+ }
+
+ class Moov {
+
+ Mvhd mvhd;
+ Trak[] trak;
+ Trex[] mvex_trex;
+ }
+
+ class Tfra {
+
+ int trackId;
+ int[] entries_time;
+ }
+
+ public class Trex {
+
+ private int trackId;
+ int defaultSampleDescriptionIndex;
+ int defaultSampleDuration;
+ int defaultSampleSize;
+ int defaultSampleFlags;
+ }
+
+ public class Mp4Track {
+
+ public TrackKind kind;
+ public Trak trak;
+ public Trex trex;
+ }
+
+ public class Mp4TrackChunk {
+
+ public InputStream data;
+ public Moof moof;
+ }
+//
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java
new file mode 100644
index 000000000..babb2e24c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java
@@ -0,0 +1,623 @@
+package org.schabi.newpipe.streams;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+
+import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
+import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk;
+import org.schabi.newpipe.streams.Mp4DashReader.Trak;
+import org.schabi.newpipe.streams.Mp4DashReader.Trex;
+
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag;
+
+/**
+ *
+ * @author kapodamy
+ */
+public class Mp4DashWriter {
+
+ private final static byte DIMENSIONAL_FIVE = 5;
+ private final static byte DIMENSIONAL_TWO = 2;
+ private final static short DEFAULT_TIMESCALE = 1000;
+ private final static int BUFFER_SIZE = 8 * 1024;
+ private final static byte DEFAULT_TREX_SIZE = 32;
+ private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
+ private final static int EPOCH_OFFSET = 2082844800;
+
+ private Mp4Track[] infoTracks;
+ private SharpStream[] sourceTracks;
+
+ private Mp4DashReader[] readers;
+ private final long time;
+
+ private boolean done = false;
+ private boolean parsed = false;
+
+ private long written = 0;
+ private ArrayList> chunkTimes;
+ private ArrayList moofOffsets;
+ private ArrayList fragSizes;
+
+ public Mp4DashWriter(SharpStream... source) {
+ sourceTracks = source;
+ readers = new Mp4DashReader[sourceTracks.length];
+ infoTracks = new Mp4Track[sourceTracks.length];
+ time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
+ }
+
+ public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
+ if (!parsed) {
+ throw new IllegalStateException("All sources must be parsed first");
+ }
+
+ return readers[sourceIndex].getAvailableTracks();
+ }
+
+ public void parseSources() throws IOException, IllegalStateException {
+ if (done) {
+ throw new IllegalStateException("already done");
+ }
+ if (parsed) {
+ throw new IllegalStateException("already parsed");
+ }
+
+ try {
+ for (int i = 0; i < readers.length; i++) {
+ readers[i] = new Mp4DashReader(sourceTracks[i]);
+ readers[i].parse();
+ }
+
+ } finally {
+ parsed = true;
+ }
+ }
+
+ public void selectTracks(int... trackIndex) throws IOException {
+ if (done) {
+ throw new IOException("already done");
+ }
+ if (chunkTimes != null) {
+ throw new IOException("tracks already selected");
+ }
+
+ try {
+ chunkTimes = new ArrayList<>(readers.length);
+ moofOffsets = new ArrayList<>(32);
+ fragSizes = new ArrayList<>(32);
+
+ for (int i = 0; i < readers.length; i++) {
+ infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
+
+ chunkTimes.add(new ArrayList(32));
+ }
+
+ } finally {
+ parsed = true;
+ }
+ }
+
+ public long getBytesWritten() {
+ return written;
+ }
+
+ public void build(SharpStream out) throws IOException, RuntimeException {
+ if (done) {
+ throw new RuntimeException("already done");
+ }
+ if (!out.canWrite()) {
+ throw new IOException("the provided output is not writable");
+ }
+
+ long sidxOffsets = -1;
+ int maxFrags = 0;
+
+ for (SharpStream stream : sourceTracks) {
+ if (!stream.canRewind()) {
+ sidxOffsets = -2;// sidx not available
+ }
+ }
+
+ try {
+ dump(make_ftyp(), out);
+ dump(make_moov(), out);
+
+ if (sidxOffsets == -1 && out.canRewind()) {
+ //
+ int reserved = 0;
+ for (Mp4DashReader reader : readers) {
+ int count = reader.getFragmentsCount();
+ if (count > maxFrags) {
+ maxFrags = count;
+ }
+ reserved += 12 + calcSidxBodySize(count);
+ }
+ if (maxFrags > 0xFFFF) {
+ sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
+ } else {
+ sidxOffsets = written;
+ dump(make_free(reserved), out);
+ }
+ //
+ }
+ ArrayList chunks = new ArrayList<>(readers.length);
+ chunks.add(null);
+
+ int read;
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int sequenceNumber = 1;
+
+ while (true) {
+ chunks.clear();
+
+ for (int i = 0; i < readers.length; i++) {
+ Mp4TrackChunk chunk = readers[i].getNextChunk();
+ if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
+ continue;
+ }
+ chunk.moof.traf.tfhd.trackId = i + 1;
+ chunks.add(chunk);
+
+ if (sequenceNumber == 1) {
+ if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
+ chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
+ } else {
+ chunkTimes.get(i).add(0);
+ }
+ }
+
+ chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
+ }
+
+ if (chunks.size() < 1) {
+ break;
+ }
+
+ long offset = written;
+ moofOffsets.add(offset);
+
+ dump(make_moof(sequenceNumber++, chunks, offset), out);
+ dump(make_mdat(chunks), out);
+
+ for (Mp4TrackChunk chunk : chunks) {
+ while ((read = chunk.data.read(buffer)) > 0) {
+ out.write(buffer, 0, read);
+ written += read;
+ }
+ }
+
+ fragSizes.add((int) (written - offset));
+ }
+
+ dump(make_mfra(), out);
+
+ if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
+ long len = written;
+
+ out.rewind();
+ out.skip(sidxOffsets);
+
+ written = sidxOffsets;
+ sidxOffsets = moofOffsets.get(0);
+
+ for (int i = 0; i < readers.length; i++) {
+ dump(make_sidx(i, sidxOffsets - written), out);
+ }
+
+ written = len;
+ }
+ } finally {
+ done = true;
+ }
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+
+ public boolean isParsed() {
+ return parsed;
+ }
+
+ public void close() {
+ done = true;
+ parsed = true;
+
+ for (SharpStream src : sourceTracks) {
+ src.dispose();
+ }
+
+ sourceTracks = null;
+ readers = null;
+ infoTracks = null;
+ moofOffsets = null;
+ chunkTimes = null;
+ }
+
+ //
+ private void dump(byte[][] buffer, SharpStream stream) throws IOException {
+ for (byte[] buff : buffer) {
+ stream.write(buff);
+ written += buff.length;
+ }
+ }
+
+ private byte[][] lengthFor(byte[][] buffer) {
+ int length = 0;
+ for (byte[] buff : buffer) {
+ length += buff.length;
+ }
+
+ ByteBuffer.wrap(buffer[0]).putInt(length);
+
+ return buffer;
+ }
+
+ private int calcSidxBodySize(int entryCount) {
+ return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
+ }
+ //
+
+ //
+ private byte[][] make_moof(int sequence, ArrayList chunks, long referenceOffset) {
+ int pos = 2;
+ TrunExtra[] extra = new TrunExtra[chunks.size()];
+
+ byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
+ 0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
+ };
+ buffer[1] = new byte[4];
+ ByteBuffer.wrap(buffer[1]).putInt(sequence);
+
+ for (int i = 0; i < extra.length; i++) {
+ extra[i] = new TrunExtra();
+ for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
+ buffer[pos++] = buff;
+ }
+ }
+
+ lengthFor(buffer);
+
+ int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
+
+ for (int i = 0; i < extra.length; i++) {
+ extra[i].byteBuffer.putInt(offset);
+ offset += chunks.get(i).moof.traf.trun.chunkSize;
+ }
+
+ return buffer;
+ }
+
+ private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
+ byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
+ 0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
+ };
+
+ int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
+ byte tfhdBodySize = 8 + 8;
+ if (hasFlag(flags, 0x08)) {
+ tfhdBodySize += 4;
+ }
+ if (hasFlag(flags, 0x10)) {
+ tfhdBodySize += 4;
+ }
+ if (hasFlag(flags, 0x20)) {
+ tfhdBodySize += 4;
+ }
+ buffer[1] = new byte[tfhdBodySize];
+ ByteBuffer set = ByteBuffer.wrap(buffer[1]);
+ set.position(4);
+ set.putInt(chunk.moof.traf.tfhd.trackId);
+ set.putLong(moofOffset);
+ if (hasFlag(flags, 0x08)) {
+ set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
+ }
+ if (hasFlag(flags, 0x10)) {
+ set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
+ }
+ if (hasFlag(flags, 0x20)) {
+ set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
+ }
+ set.putInt(0, flags);
+ ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
+
+ buffer[2] = new byte[]{
+ 0x00, 0x00, 0x00, 0x14,
+ 0x74, 0x66, 0x64, 0x74,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ };
+
+ ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
+
+ buffer[3] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ };
+
+ buffer[4] = chunk.moof.traf.trun.bEntries;
+
+ lengthFor(buffer);
+
+ set = ByteBuffer.wrap(buffer[3]);
+ set.putInt(buffer[3].length + buffer[4].length);
+ set.position(8);
+ set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
+ set.putInt(chunk.moof.traf.trun.entryCount);
+ extra.byteBuffer = set;
+
+ return buffer;
+ }
+
+ private byte[][] make_mdat(ArrayList chunks) {
+ byte[][] buffer = new byte[][]{
+ {
+ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
+ }
+ };
+
+ int length = 0;
+
+ for (Mp4TrackChunk chunk : chunks) {
+ length += chunk.moof.traf.trun.chunkSize;
+ }
+
+ ByteBuffer.wrap(buffer[0]).putInt(length + 8);
+
+ return buffer;
+ }
+
+ private byte[][] make_ftyp() {
+ return new byte[][]{
+ {
+ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
+ 0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
+ }
+ };
+ }
+
+ private byte[][] make_mvhd() {
+ byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
+
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
+ };
+ buffer[1] = new byte[28];
+ buffer[2] = new byte[]{
+ 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
+ // default matrix
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00
+ };
+ buffer[3] = new byte[24];// predefined
+ buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
+
+ long longestTrack = 0;
+
+ for (Mp4Track track : infoTracks) {
+ long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
+ if (tmp > longestTrack) {
+ longestTrack = tmp;
+ }
+ }
+
+ ByteBuffer.wrap(buffer[1])
+ .putLong(time)
+ .putLong(time)
+ .putInt(DEFAULT_TIMESCALE)
+ .putLong(longestTrack);
+
+ return buffer;
+ }
+
+ private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
+ if (trak.tkhd.matrix.length != 36) {
+ throw new RuntimeException("bad track matrix length (expected 36)");
+ }
+
+ byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
+
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
+ 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
+ };
+ buffer[1] = new byte[48];
+ buffer[2] = trak.tkhd.matrix;
+ buffer[3] = new byte[8];
+ buffer[4] = trak.mdia;
+
+ ByteBuffer set = ByteBuffer.wrap(buffer[1]);
+ set.putLong(time);
+ set.putLong(time);
+ set.putInt(trackId);
+ set.position(24);
+ set.putLong(trak.tkhd.duration);
+ set.position(40);
+ set.putShort(trak.tkhd.bLayer);
+ set.putShort(trak.tkhd.bAlternateGroup);
+ set.putShort(trak.tkhd.bVolume);
+
+ ByteBuffer.wrap(buffer[3])
+ .putInt(trak.tkhd.bWidth)
+ .putInt(trak.tkhd.bHeight);
+
+ return lengthFor(buffer);
+ }
+
+ private byte[][] make_moov() throws RuntimeException {
+ int pos = 1;
+ byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
+
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
+ };
+
+ for (byte[] buff : make_mvhd()) {
+ buffer[pos++] = buff;
+ }
+
+ for (int i = 0; i < infoTracks.length; i++) {
+ for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
+ buffer[pos++] = buff;
+ }
+ }
+
+ buffer[pos] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
+ };
+
+ ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
+
+ for (int i = 0; i < infoTracks.length; i++) {
+ for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
+ buffer[pos++] = buff;
+ }
+ }
+
+ // default udta
+ buffer[pos] = new byte[]{
+ 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
+ 0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
+ };
+
+ return lengthFor(buffer);
+ }
+
+ private byte[][] make_trex(int trackId, Trex trex) {
+ byte[][] buffer = new byte[][]{
+ {
+ 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
+ },
+ new byte[20]
+ };
+
+ ByteBuffer.wrap(buffer[1])
+ .putInt(trackId)
+ .putInt(trex.defaultSampleDescriptionIndex)
+ .putInt(trex.defaultSampleDuration)
+ .putInt(trex.defaultSampleSize)
+ .putInt(trex.defaultSampleFlags);
+
+ return buffer;
+ }
+
+ private byte[][] make_tfra(int trackId, List times, List moofOffsets) {
+ int entryCount = times.size() - 1;
+ byte[][] buffer = new byte[DIMENSIONAL_TWO][];
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
+ };
+ buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
+
+ ByteBuffer set = ByteBuffer.wrap(buffer[1]);
+ set.putInt(trackId);
+ set.position(8);
+ set.putInt(entryCount);
+
+ long decodeTime = 0;
+
+ for (int i = 0; i < entryCount; i++) {
+ decodeTime += times.get(i);
+ set.putLong(decodeTime);
+ set.putLong(moofOffsets.get(i));
+ set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
+ }
+
+ return lengthFor(buffer);
+ }
+
+ private byte[][] make_mfra() {
+ byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
+ buffer[0] = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
+ };
+ int pos = 1;
+
+ for (int i = 0; i < infoTracks.length; i++) {
+ for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
+ buffer[pos++] = buff;
+ }
+ }
+
+ buffer[pos] = new byte[]{// mfro
+ 0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+
+ lengthFor(buffer);
+
+ ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
+ set.position(12);
+ set.put(buffer[0], 0, 4);
+
+ return buffer;
+
+ }
+
+ private byte[][] make_sidx(int internalTrackId, long firstOffset) {
+ List times = chunkTimes.get(internalTrackId);
+ int count = times.size() - 1;// the first item is ignored (composition time)
+
+ if (count > 65535) {
+ throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
+ }
+
+ byte[][] buffer = new byte[][]{
+ new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
+ },
+ new byte[calcSidxBodySize(count)]
+ };
+
+ lengthFor(buffer);
+
+ ByteBuffer set = ByteBuffer.wrap(buffer[1]);
+ set.putInt(internalTrackId + 1);
+ set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
+ set.putLong(0);
+ set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
+ set.putInt(0xFFFF & count);// unsigned
+
+ int i = 0;
+ while (i < count) {
+ set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
+ set.putInt(times.get(i + 1));
+ set.putInt(0x90000000);// default SAP settings
+ i++;
+ }
+
+ return buffer;
+ }
+
+ private byte[][] make_free(int totalSize) {
+ return lengthFor(new byte[][]{
+ new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
+ new byte[totalSize - 8]// this is waste of RAM
+ });
+
+ }
+
+//
+
+ class TrunExtra {
+
+ ByteBuffer byteBuffer;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java
new file mode 100644
index 000000000..26aaf49a5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java
@@ -0,0 +1,370 @@
+package org.schabi.newpipe.streams;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * @author kapodamy
+ */
+public class SubtitleConverter {
+ private static final String NEW_LINE = "\r\n";
+
+ public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
+ ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
+
+ final FrameWriter callback = new FrameWriter() {
+ int frameIndex = 0;
+ final Charset charset = Charset.forName("utf-8");
+
+ @Override
+ public void yield(SubtitleFrame frame) throws IOException {
+ if (ignoreEmptyFrames && frame.isEmptyText()) {
+ return;
+ }
+ out.write(String.valueOf(frameIndex++).getBytes(charset));
+ out.write(NEW_LINE.getBytes(charset));
+ out.write(getTime(frame.start, true).getBytes(charset));
+ out.write(" --> ".getBytes(charset));
+ out.write(getTime(frame.end, true).getBytes(charset));
+ out.write(NEW_LINE.getBytes(charset));
+ out.write(frame.text.getBytes(charset));
+ out.write(NEW_LINE.getBytes(charset));
+ out.write(NEW_LINE.getBytes(charset));
+ }
+ };
+
+ read_xml_based(in, callback, detectYoutubeDuplicateLines,
+ "tt", "xmlns", "http://www.w3.org/ns/ttml",
+ new String[]{"timedtext", "head", "wp"},
+ new String[]{"body", "div", "p"},
+ "begin", "end", true
+ );
+ }
+
+ private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
+ String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
+ String timeAttr, String durationAttr, boolean hasTimestamp
+ ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
+ /*
+ * XML based subtitles parser with BASIC support
+ * multiple CUE is not supported
+ * styling is not supported
+ * tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
+ * also TimestampTagOption enum is not applicable
+ * Language parsing is not supported
+ */
+
+ byte[] buffer = new byte[source.available()];
+ source.read(buffer);
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document xml = builder.parse(new ByteArrayInputStream(buffer));
+
+ String attr;
+
+ // get the format version or namespace
+ Element node = xml.getDocumentElement();
+
+ if (node == null) {
+ throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
+ } else if (!node.getNodeName().equals(root)) {
+ throw new ParseException("Invalid root", -1);
+ }
+
+ if (formatAttr.equals("xmlns")) {
+ if (!node.getNamespaceURI().equals(formatVersion)) {
+ throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
+ }
+ } else {
+ attr = node.getAttributeNS(formatVersion, formatAttr);
+ if (attr == null) {
+ throw new ParseException("Can't get the format attribute", -1);
+ }
+ if (!attr.equals(formatVersion)) {
+ throw new ParseException("Invalid format version : " + attr, -1);
+ }
+ }
+
+ NodeList node_list;
+
+ int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
+
+ if (!hasTimestamp) {
+ node_list = selectNodes(xml, cuePath, formatVersion);
+
+ if (node_list != null) {
+ // if the subtitle has multiple CUEs, use the highest value
+ for (int i = 0; i < node_list.getLength(); i++) {
+ try {
+ int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
+ if (tmp > line_break) {
+ line_break = tmp;
+ }
+ } catch (Exception err) {
+ }
+ }
+ }
+ }
+
+ // parse every frame
+ node_list = selectNodes(xml, framePath, formatVersion);
+
+ if (node_list == null) {
+ return;// no frames detected
+ }
+
+ int fs_ff = -1;// first timestamp of first frame
+ boolean limit_lines = false;
+
+ for (int i = 0; i < node_list.getLength(); i++) {
+ Element elem = (Element) node_list.item(i);
+ SubtitleFrame obj = new SubtitleFrame();
+ obj.text = elem.getTextContent();
+
+ attr = elem.getAttribute(timeAttr);// ¡this cant be null!
+ obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
+
+ attr = elem.getAttribute(durationAttr);
+ if (obj.text == null || attr == null) {
+ continue;// normally is a blank line (on auto-generated subtitles) ignore
+ }
+
+ if (hasTimestamp) {
+ obj.end = parseTimestamp(attr);
+
+ if (detectYoutubeDuplicateLines) {
+ if (limit_lines) {
+ int swap = obj.end;
+ obj.end = fs_ff;
+ fs_ff = swap;
+ } else {
+ if (fs_ff < 0) {
+ fs_ff = obj.end;
+ } else {
+ if (fs_ff < obj.start) {
+ limit_lines = true;// the subtitles has duplicated lines
+ } else {
+ detectYoutubeDuplicateLines = false;
+ }
+ }
+ }
+ }
+ } else {
+ obj.end = obj.start + Integer.parseInt(attr);
+ }
+
+ if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
+
+ // implement auto line breaking (once)
+ StringBuilder text = new StringBuilder(obj.text);
+ obj.text = null;
+
+ switch (text.charAt(line_break)) {
+ case ' ':
+ case '\t':
+ putBreakAt(line_break, text);
+ break;
+ default:// find the word start position
+ for (int j = line_break - 1; j > 0; j--) {
+ switch (text.charAt(j)) {
+ case ' ':
+ case '\t':
+ putBreakAt(j, text);
+ j = -1;
+ break;
+ case '\r':
+ case '\n':
+ j = -1;// long word, just ignore
+ break;
+ }
+ }
+ break;
+ }
+
+ obj.text = text.toString();// set the processed text
+ }
+
+ callback.yield(obj);
+ }
+ }
+
+ private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
+ Element ref = xml.getDocumentElement();
+
+ for (int i = 0; i < path.length - 1; i++) {
+ NodeList nodes = ref.getChildNodes();
+ if (nodes.getLength() < 1) {
+ return null;
+ }
+
+ Element elem;
+ for (int j = 0; j < nodes.getLength(); j++) {
+ if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
+ elem = (Element) nodes.item(j);
+ if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
+ ref = elem;
+ break;
+ }
+ }
+ }
+ }
+
+ return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
+ }
+
+ private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
+ if (multiImpl.length() < 1) {
+ return 0;
+ } else if (multiImpl.length() == 1) {
+ return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
+ }
+
+ // detect wallclock-time
+ if (multiImpl.startsWith("wallclock(")) {
+ throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
+ }
+
+ // detect offset-time
+ if (multiImpl.indexOf(':') < 0) {
+ int multiplier = 1000;
+ char metric = multiImpl.charAt(multiImpl.length() - 1);
+ switch (metric) {
+ case 'h':
+ multiplier *= 3600000;
+ break;
+ case 'm':
+ multiplier *= 60000;
+ break;
+ case 's':
+ if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
+ multiplier = 1;// ms
+ }
+ break;
+ default:
+ if (!Character.isDigit(metric)) {
+ throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
+ }
+ metric = '\0';
+ break;
+ }
+ try {
+ String offset_time = multiImpl;
+
+ if (multiplier == 1) {
+ offset_time = offset_time.substring(0, offset_time.length() - 2);
+ } else if (metric != '\0') {
+ offset_time = offset_time.substring(0, offset_time.length() - 1);
+ }
+
+ double time_metric_based = Double.parseDouble(offset_time);
+ if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
+ return (int) (time_metric_based * multiplier);
+ }
+ } catch (Exception err) {
+ throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
+ }
+ }
+
+ // detect clock-time
+ int time = 0;
+ String[] units = multiImpl.split(":");
+
+ if (units.length < 3) {
+ throw new ParseException("Invalid clock-time timestamp", -1);
+ }
+
+ time += Integer.parseInt(units[0]) * 3600000;// hours
+ time += Integer.parseInt(units[1]) * 60000;//minutes
+ time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
+
+ // frames and sub-frames are ignored (not implemented)
+ // time += units[3] * fps;
+ return time;
+ }
+
+ private static void putBreakAt(int idx, StringBuilder str) {
+ // this should be optimized at compile time
+
+ if (NEW_LINE.length() > 1) {
+ str.delete(idx, idx + 1);// remove after replace
+ str.insert(idx, NEW_LINE);
+ } else {
+ str.setCharAt(idx, NEW_LINE.charAt(0));
+ }
+ }
+
+ private static String getTime(int time, boolean comma) {
+ // cast every value to integer to avoid auto-round in ToString("00").
+ StringBuilder str = new StringBuilder(12);
+ str.append(numberToString(time / 1000 / 3600, 2));// hours
+ str.append(':');
+ str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
+ str.append(':');
+ str.append(numberToString(time / 1000 % 60, 2));// seconds
+ str.append(comma ? ',' : '.');
+ str.append(numberToString(time % 1000, 3));// miliseconds
+
+ return str.toString();
+ }
+
+ private static String numberToString(int nro, int pad) {
+ return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
+ }
+
+
+ /******************
+ * helper classes *
+ ******************/
+
+ private interface FrameWriter {
+
+ void yield(SubtitleFrame frame) throws IOException;
+ }
+
+ private static class SubtitleFrame {
+ //Java no support unsigned int
+
+ public int end;
+ public int start;
+ public String text = "";
+
+ private boolean isEmptyText() {
+ if (text == null) {
+ return true;
+ }
+
+ for (int i = 0; i < text.length(); i++) {
+ switch (text.charAt(i)) {
+ case ' ':
+ case '\t':
+ case '\r':
+ case '\n':
+ break;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java b/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java
new file mode 100644
index 000000000..86eb5ff4f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java
@@ -0,0 +1,65 @@
+package org.schabi.newpipe.streams;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+public class TrackDataChunk extends InputStream {
+
+ private final DataReader base;
+ private int size;
+
+ public TrackDataChunk(DataReader base, int size) {
+ this.base = base;
+ this.size = size;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (size < 1) {
+ return -1;
+ }
+
+ int res = base.read();
+
+ if (res >= 0) {
+ size--;
+ }
+
+ return res;
+ }
+
+ @Override
+ public int read(byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int count) throws IOException {
+ count = Math.min(size, count);
+ int read = base.read(buffer, offset, count);
+ size -= count;
+ return read;
+ }
+
+ @Override
+ public long skip(long amount) throws IOException {
+ long res = base.skipBytes(Math.min(amount, size));
+ size -= res;
+ return res;
+ }
+
+ @Override
+ public int available() {
+ return size;
+ }
+
+ @Override
+ public void close() {
+ size = 0;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
new file mode 100644
index 000000000..f61ef14c5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
@@ -0,0 +1,507 @@
+package org.schabi.newpipe.streams;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+
+/**
+ *
+ * @author kapodamy
+ */
+public class WebMReader {
+
+ //
+ private final static int ID_EMBL = 0x0A45DFA3;
+ private final static int ID_EMBLReadVersion = 0x02F7;
+ private final static int ID_EMBLDocType = 0x0282;
+ private final static int ID_EMBLDocTypeReadVersion = 0x0285;
+
+ private final static int ID_Segment = 0x08538067;
+
+ private final static int ID_Info = 0x0549A966;
+ private final static int ID_TimecodeScale = 0x0AD7B1;
+ private final static int ID_Duration = 0x489;
+
+ private final static int ID_Tracks = 0x0654AE6B;
+ private final static int ID_TrackEntry = 0x2E;
+ private final static int ID_TrackNumber = 0x57;
+ private final static int ID_TrackType = 0x03;
+ private final static int ID_CodecID = 0x06;
+ private final static int ID_CodecPrivate = 0x23A2;
+ private final static int ID_Video = 0x60;
+ private final static int ID_Audio = 0x61;
+ private final static int ID_DefaultDuration = 0x3E383;
+ private final static int ID_FlagLacing = 0x1C;
+
+ private final static int ID_Cluster = 0x0F43B675;
+ private final static int ID_Timecode = 0x67;
+ private final static int ID_SimpleBlock = 0x23;
+//
+
+ public enum TrackKind {
+ Audio/*2*/, Video/*1*/, Other
+ }
+
+ private DataReader stream;
+ private Segment segment;
+ private WebMTrack[] tracks;
+ private int selectedTrack;
+ private boolean done;
+ private boolean firstSegment;
+
+ public WebMReader(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(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);
+
+ Element elem = untilElement(null, ID_Segment);
+ if (elem == null) {
+ done = true;
+ return null;
+ }
+ segment = readSegment(elem, 0, false);
+
+ return segment;
+ }
+
+ //
+ private long readNumber(Element parent) throws IOException {
+ int length = (int) parent.contentSize;
+ long value = 0;
+ while (length-- > 0) {
+ int read = stream.read();
+ if (read == -1) {
+ throw new EOFException();
+ }
+ value = (value << 8) | read;
+ }
+ return value;
+ }
+
+ private String readString(Element parent) throws IOException {
+ return new String(readBlob(parent), "utf-8");
+ }
+
+ private byte[] readBlob(Element parent) throws IOException {
+ long length = parent.contentSize;
+ byte[] buffer = new byte[(int) length];
+ 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 {
+ 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(int expected) throws IOException {
+ Element elem = readElement();
+ if (expected != 0 && elem.type != expected) {
+ throw new NoSuchElementException("expected " + elementID(expected) + " found " + elementID(elem.type));
+ }
+
+ return elem;
+ }
+
+ private Element untilElement(Element ref, int... expected) throws IOException {
+ Element elem;
+ while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
+ elem = readElement();
+ for (int type : expected) {
+ if (elem.type == type) {
+ return elem;
+ }
+ }
+ ensure(elem);
+ }
+
+ return null;
+ }
+
+ private String elementID(long type) {
+ return "0x".concat(Long.toHexString(type));
+ }
+
+ private void ensure(Element ref) throws IOException {
+ 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(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException {
+ Element elem = untilElement(ref, ID_EMBLReadVersion);
+ if (elem == null) {
+ return false;
+ }
+ if (readNumber(elem) > minReadVersion) {
+ return false;
+ }
+
+ elem = untilElement(ref, ID_EMBLDocType);
+ if (elem == null) {
+ return false;
+ }
+ if (!readString(elem).equals("webm")) {
+ return false;
+ }
+ elem = untilElement(ref, ID_EMBLDocTypeReadVersion);
+
+ return elem != null && readNumber(elem) <= minDocTypeVersion;
+ }
+
+ private Info readInfo(Element ref) throws IOException {
+ Element elem;
+ Info info = new Info();
+
+ while ((elem = untilElement(ref, ID_TimecodeScale, ID_Duration)) != null) {
+ switch (elem.type) {
+ case ID_TimecodeScale:
+ 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(Element ref, int trackLacingExpected, boolean metadataExpected) throws IOException {
+ 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 " + String.valueOf(ref.offset));
+ }
+
+ return obj;
+ }
+
+ private WebMTrack[] readTracks(Element ref, int lacingExpected) throws IOException {
+ ArrayList trackEntries = new ArrayList<>(2);
+ Element elem_trackEntry;
+
+ while ((elem_trackEntry = untilElement(ref, ID_TrackEntry)) != null) {
+ WebMTrack entry = new WebMTrack();
+ boolean drop = false;
+ Element elem;
+ while ((elem = untilElement(elem_trackEntry,
+ ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
+ )) != null) {
+ switch (elem.type) {
+ case ID_TrackNumber:
+ entry.trackNumber = readNumber(elem);
+ break;
+ case ID_TrackType:
+ entry.trackType = (int)readNumber(elem);
+ break;
+ case ID_CodecID:
+ entry.codecId = readString(elem);
+ break;
+ case ID_CodecPrivate:
+ entry.codecPrivate = readBlob(elem);
+ break;
+ case ID_Audio:
+ case ID_Video:
+ entry.bMetadata = readBlob(elem);
+ break;
+ case ID_DefaultDuration:
+ entry.defaultDuration = readNumber(elem);
+ break;
+ case ID_FlagLacing:
+ drop = readNumber(elem) != lacingExpected;
+ break;
+ default:
+ System.out.println();
+ break;
+ }
+ ensure(elem);
+ }
+ if (!drop) {
+ trackEntries.add(entry);
+ }
+ ensure(elem_trackEntry);
+ }
+
+ WebMTrack[] entries = new WebMTrack[trackEntries.size()];
+ trackEntries.toArray(entries);
+
+ for (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(Element ref) throws IOException {
+ SimpleBlock obj = new SimpleBlock(ref);
+ obj.dataSize = stream.position();
+ obj.trackNumber = readEncodedNumber();
+ obj.relativeTimeCode = stream.readShort();
+ obj.flags = (byte) stream.read();
+ obj.dataSize = (ref.offset + ref.size) - stream.position();
+
+ if (obj.dataSize < 0) {
+ throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
+ }
+ return obj;
+ }
+
+ private Cluster readCluster(Element ref) throws IOException {
+ Cluster obj = new Cluster(ref);
+
+ Element elem = untilElement(ref, ID_Timecode);
+ if (elem == null) {
+ throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + " without Timecode element");
+ }
+ obj.timecode = readNumber(elem);
+
+ return obj;
+ }
+//
+
+ //
+ class Element {
+
+ int type;
+ long offset;
+ long contentSize;
+ long size;
+ }
+
+ public class Info {
+
+ public long timecodeScale;
+ public long duration;
+ }
+
+ public class WebMTrack {
+
+ public long trackNumber;
+ protected int trackType;
+ public String codecId;
+ public byte[] codecPrivate;
+ public byte[] bMetadata;
+ public TrackKind kind;
+ public long defaultDuration;
+ }
+
+ public class Segment {
+
+ Segment(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);
+
+ Element elem = untilElement(segment.ref, ID_Cluster);
+ if (elem == null) {
+ return null;
+ }
+
+ segment.currentCluster = elem;
+
+ return readCluster(segment.currentCluster);
+ }
+ }
+
+ public class SimpleBlock {
+
+ public TrackDataChunk data;
+
+ SimpleBlock(Element ref) {
+ this.ref = ref;
+ }
+
+ public long trackNumber;
+ public short relativeTimeCode;
+ public byte flags;
+ public long dataSize;
+ private final Element ref;
+
+ public boolean isKeyframe() {
+ return (flags & 0x80) == 0x80;
+ }
+ }
+
+ public class Cluster {
+
+ Element ref;
+ SimpleBlock currentSimpleBlock = null;
+ public long timecode;
+
+ Cluster(Element ref) {
+ this.ref = ref;
+ }
+
+ boolean check() {
+ return stream.position() >= (ref.offset + ref.size);
+ }
+
+ public SimpleBlock getNextSimpleBlock() throws IOException {
+ if (check()) {
+ return null;
+ }
+ if (currentSimpleBlock != null) {
+ ensure(currentSimpleBlock.ref);
+ }
+
+ while (!check()) {
+ Element elem = untilElement(ref, ID_SimpleBlock);
+ if (elem == null) {
+ return null;
+ }
+
+ currentSimpleBlock = readSimpleBlock(elem);
+ if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
+ currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
+ return currentSimpleBlock;
+ }
+
+ ensure(elem);
+ }
+
+ return null;
+ }
+
+ }
+//
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
new file mode 100644
index 000000000..ea038c607
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
@@ -0,0 +1,728 @@
+package org.schabi.newpipe.streams;
+
+import org.schabi.newpipe.streams.WebMReader.Cluster;
+import org.schabi.newpipe.streams.WebMReader.Segment;
+import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
+import org.schabi.newpipe.streams.WebMReader.WebMTrack;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+
+/**
+ *
+ * @author kapodamy
+ */
+public class WebMWriter {
+
+ private final static int BUFFER_SIZE = 8 * 1024;
+ private final static int DEFAULT_TIMECODE_SCALE = 1000000;
+ private final static int INTERV = 100;// 100ms on 1000000us timecode scale
+ private final static int DEFAULT_CUES_EACH_MS = 5000;// 100ms on 1000000us timecode scale
+
+ private WebMReader.WebMTrack[] infoTracks;
+ private SharpStream[] sourceTracks;
+
+ private WebMReader[] readers;
+
+ private boolean done = false;
+ private boolean parsed = false;
+
+ private long written = 0;
+
+ private Segment[] readersSegment;
+ private Cluster[] readersCluter;
+
+ private int[] predefinedDurations;
+
+ private byte[] outBuffer;
+
+ public WebMWriter(SharpStream... source) {
+ sourceTracks = source;
+ readers = new WebMReader[sourceTracks.length];
+ infoTracks = new WebMTrack[sourceTracks.length];
+ outBuffer = new byte[BUFFER_SIZE];
+ }
+
+ public WebMTrack[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
+ if (done) {
+ throw new IllegalStateException("already done");
+ }
+ if (!parsed) {
+ throw new IllegalStateException("All sources must be parsed first");
+ }
+
+ return readers[sourceIndex].getAvailableTracks();
+ }
+
+ public void parseSources() throws IOException, IllegalStateException {
+ if (done) {
+ throw new IllegalStateException("already done");
+ }
+ if (parsed) {
+ throw new IllegalStateException("already parsed");
+ }
+
+ try {
+ for (int i = 0; i < readers.length; i++) {
+ readers[i] = new WebMReader(sourceTracks[i]);
+ readers[i].parse();
+ }
+
+ } finally {
+ parsed = true;
+ }
+ }
+
+ public void selectTracks(int... trackIndex) throws IOException {
+ try {
+ readersSegment = new Segment[readers.length];
+ readersCluter = new Cluster[readers.length];
+ predefinedDurations = new int[readers.length];
+
+ for (int i = 0; i < readers.length; i++) {
+ infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
+ predefinedDurations[i] = -1;
+ readersSegment[i] = readers[i].getNextSegment();
+ }
+ } finally {
+ parsed = true;
+ }
+ }
+
+ public long getBytesWritten() {
+ return written;
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+
+ public boolean isParsed() {
+ return parsed;
+ }
+
+ public void close() {
+ done = true;
+ parsed = true;
+
+ for (SharpStream src : sourceTracks) {
+ src.dispose();
+ }
+
+ sourceTracks = null;
+ readers = null;
+ infoTracks = null;
+ readersSegment = null;
+ readersCluter = null;
+ outBuffer = null;
+ }
+
+ public void build(SharpStream out) throws IOException, RuntimeException {
+ if (!out.canRewind()) {
+ throw new IOException("The output stream must be allow seek");
+ }
+
+ makeEBML(out);
+
+ long offsetSegmentSizeSet = written + 5;
+ long offsetInfoDurationSet = written + 94;
+ long offsetClusterSet = written + 58;
+ long offsetCuesSet = written + 75;
+
+ ArrayList listBuffer = new ArrayList<>(4);
+
+ /* segment */
+ listBuffer.add(new byte[]{
+ 0x18, 0x53, (byte) 0x80, 0x67, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
+ });
+
+ long baseSegmentOffset = written + listBuffer.get(0).length;
+
+ /* seek head */
+ listBuffer.add(new byte[]{
+ 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
+ 0x4d, (byte) 0xbb, (byte) 0x8b,
+ 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
+ (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
+ 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
+ (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
+ /*tracks offset*/ 0x6a,
+ 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
+ 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
+ 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
+ (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
+ });
+
+ /* info */
+ listBuffer.add(new byte[]{
+ 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
+ });
+ listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
+ listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
+ 0x00, 0x00, 0x00, 0x00,// info.duration
+
+ /* MuxingApp */
+ 0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
+ 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
+
+ /* WritingApp */
+ 0x57, 0x41, (byte) 0x87, 0x4E,
+ 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
+ });
+
+ /* tracks */
+ listBuffer.addAll(makeTracks());
+
+ for (byte[] buff : listBuffer) {
+ dump(buff, out);
+ }
+
+ // reserve space for Cues element, but is a waste of space (actually is 64 KiB)
+ // TODO: better Cue maker
+ long cueReservedOffset = written;
+ dump(new byte[]{(byte) 0xec, 0x20, (byte) 0xff, (byte) 0xfb}, out);
+ int reserved = (1024 * 63) - 4;
+ while (reserved > 0) {
+ int write = Math.min(reserved, outBuffer.length);
+ out.write(outBuffer, 0, write);
+ reserved -= write;
+ written += write;
+ }
+
+ // Select a track for the cue
+ int cuesForTrackId = selectTrackForCue();
+ long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
+ ArrayList keyFrames = new ArrayList<>(32);
+
+ //ArrayList chunks = new ArrayList<>(readers.length);
+ ArrayList clusterOffsets = new ArrayList<>(32);
+ ArrayList clusterSizes = new ArrayList<>(32);
+
+ long duration = 0;
+ int durationFromTrackId = 0;
+
+ byte[] bTimecode = makeTimecode(0);
+
+ int firstClusterOffset = (int) written;
+ long currentClusterOffset = makeCluster(out, bTimecode, 0, clusterOffsets, clusterSizes);
+
+ long baseTimecode = 0;
+ long limitTimecode = -1;
+ int limitTimecodeByTrackId = cuesForTrackId;
+
+ int blockWritten = Integer.MAX_VALUE;
+
+ int newClusterByTrackId = -1;
+
+ while (blockWritten > 0) {
+ blockWritten = 0;
+ int i = 0;
+ while (i < readers.length) {
+ Block bloq = getNextBlockFrom(i);
+ if (bloq == null) {
+ i++;
+ continue;
+ }
+
+ if (bloq.data == null) {
+ blockWritten = 1;// fake block
+ newClusterByTrackId = i;
+ i++;
+ continue;
+ }
+
+ if (newClusterByTrackId == i) {
+ limitTimecodeByTrackId = i;
+ newClusterByTrackId = -1;
+ baseTimecode = bloq.absoluteTimecode;
+ limitTimecode = baseTimecode + INTERV;
+ bTimecode = makeTimecode(baseTimecode);
+ currentClusterOffset = makeCluster(out, bTimecode, currentClusterOffset, clusterOffsets, clusterSizes);
+ }
+
+ if (cuesForTrackId == i) {
+ if ((nextCueTime > -1 && bloq.absoluteTimecode >= nextCueTime) || (nextCueTime < 0 && bloq.isKeyframe())) {
+ if (nextCueTime > -1) {
+ nextCueTime += DEFAULT_CUES_EACH_MS;
+ }
+ keyFrames.add(
+ new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
+ );
+ }
+ }
+
+ writeBlock(out, bloq, baseTimecode);
+ blockWritten++;
+
+ if (bloq.absoluteTimecode > duration) {
+ duration = bloq.absoluteTimecode;
+ durationFromTrackId = bloq.trackNumber;
+ }
+
+ if (limitTimecode < 0) {
+ limitTimecode = bloq.absoluteTimecode + INTERV;
+ continue;
+ }
+
+ if (bloq.absoluteTimecode >= limitTimecode) {
+ if (limitTimecodeByTrackId != i) {
+ limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode);
+ }
+ i++;
+ }
+ }
+ }
+
+ makeCluster(out, null, currentClusterOffset, null, clusterSizes);
+
+ long segmentSize = written - offsetSegmentSizeSet - 7;
+
+ // final step write offsets and sizes
+ out.rewind();
+ written = 0;
+
+ skipTo(out, offsetSegmentSizeSet);
+ writeLong(out, segmentSize);
+
+ if (predefinedDurations[durationFromTrackId] > -1) {
+ duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
+ }
+ skipTo(out, offsetInfoDurationSet);
+ writeFloat(out, duration);
+
+ firstClusterOffset -= baseSegmentOffset;
+ skipTo(out, offsetClusterSet);
+ writeInt(out, firstClusterOffset);
+
+ skipTo(out, cueReservedOffset);
+
+ /* Cue */
+ dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
+
+ for (KeyFrame keyFrame : keyFrames) {
+ for (byte[] buffer : makeCuePoint(cuesForTrackId, keyFrame)) {
+ dump(buffer, out);
+ if (written >= (cueReservedOffset + 65535 - 16)) {
+ throw new IOException("Too many Cues");
+ }
+ }
+ }
+ short cueSize = (short) (written - cueReservedOffset - 7);
+
+ /* EBML Void */
+ ByteBuffer voidBuffer = ByteBuffer.allocate(4);
+ voidBuffer.putShort((short) 0xec20);
+ voidBuffer.putShort((short) (firstClusterOffset - written - 4));
+ dump(voidBuffer.array(), out);
+
+ out.rewind();
+ written = 0;
+
+ skipTo(out, offsetCuesSet);
+ writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
+
+ skipTo(out, cueReservedOffset + 5);
+ writeShort(out, cueSize);
+
+ for (int i = 0; i < clusterSizes.size(); i++) {
+ skipTo(out, clusterOffsets.get(i));
+ byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
+ out.write(size, 1, 3);
+ written += 3;
+ }
+ }
+
+ private Block getNextBlockFrom(int internalTrackId) throws IOException {
+ if (readersSegment[internalTrackId] == null) {
+ readersSegment[internalTrackId] = readers[internalTrackId].getNextSegment();
+ if (readersSegment[internalTrackId] == null) {
+ return null;// no more blocks in the selected track
+ }
+ }
+
+ if (readersCluter[internalTrackId] == null) {
+ readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
+ if (readersCluter[internalTrackId] == null) {
+ readersSegment[internalTrackId] = null;
+ return getNextBlockFrom(internalTrackId);
+ }
+ }
+
+ SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
+ if (res == null) {
+ readersCluter[internalTrackId] = null;
+ return new Block();// fake block to indicate the end of the cluster
+ }
+
+ Block bloq = new Block();
+ bloq.data = res.data;
+ bloq.dataSize = (int) res.dataSize;
+ bloq.trackNumber = internalTrackId;
+ bloq.flags = res.flags;
+ bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE);
+ bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
+
+ return bloq;
+ }
+
+ private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
+ return (short) (time * (newTimeScale / oldTimeScale));
+ }
+
+ private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
+ absoluteOffset -= written;
+ written += absoluteOffset;
+ stream.skip(absoluteOffset);
+ }
+
+ private void writeLong(SharpStream stream, long number) throws IOException {
+ byte[] buffer = ByteBuffer.allocate(DataReader.LONG_SIZE).putLong(number).array();
+ stream.write(buffer, 1, buffer.length - 1);
+ written += buffer.length - 1;
+ }
+
+ private void writeFloat(SharpStream stream, float number) throws IOException {
+ byte[] buffer = ByteBuffer.allocate(DataReader.FLOAT_SIZE).putFloat(number).array();
+ dump(buffer, stream);
+ }
+
+ private void writeShort(SharpStream stream, short number) throws IOException {
+ byte[] buffer = ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort(number).array();
+ dump(buffer, stream);
+ }
+
+ private void writeInt(SharpStream stream, int number) throws IOException {
+ byte[] buffer = ByteBuffer.allocate(DataReader.INTEGER_SIZE).putInt(number).array();
+ dump(buffer, stream);
+ }
+
+ private void writeBlock(SharpStream stream, Block bloq, long clusterTimecode) throws IOException {
+ long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode;
+
+ if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) {
+ throw new IndexOutOfBoundsException("SimpleBlock timecode overflow.");
+ }
+
+ ArrayList listBuffer = new ArrayList<>(5);
+ listBuffer.add(new byte[]{(byte) 0xa3});
+ listBuffer.add(null);// block size
+ listBuffer.add(encode(bloq.trackNumber + 1, false));
+ listBuffer.add(ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort((short) relativeTimeCode).array());
+ listBuffer.add(new byte[]{bloq.flags});
+
+ int blockSize = bloq.dataSize;
+ for (int i = 2; i < listBuffer.size(); i++) {
+ blockSize += listBuffer.get(i).length;
+ }
+ listBuffer.set(1, encode(blockSize, false));
+
+ for (byte[] buff : listBuffer) {
+ dump(buff, stream);
+ }
+
+ int read;
+ while ((read = bloq.data.read(outBuffer)) > 0) {
+ stream.write(outBuffer, 0, read);
+ written += read;
+ }
+ }
+
+ private byte[] makeTimecode(long timecode) {
+ ByteBuffer buffer = ByteBuffer.allocate(9);
+ buffer.put((byte) 0xe7);
+ buffer.put(encode(timecode, true));
+
+ byte[] res = new byte[buffer.position()];
+ System.arraycopy(buffer.array(), 0, res, 0, res.length);
+
+ return res;
+ }
+
+ private long makeCluster(SharpStream stream, byte[] bTimecode, long startOffset, ArrayList clusterOffsets, ArrayList clusterSizes) throws IOException {
+ if (startOffset > 0) {
+ clusterSizes.add((int) (written - startOffset));// size for last offset
+ }
+
+ if (clusterOffsets != null) {
+ /* cluster */
+ dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
+ clusterOffsets.add(written);// warning: max cluster size is 256 MiB
+ dump(new byte[]{0x20, 0x00, 0x00}, stream);
+
+ startOffset = written;// size for the this cluster
+
+ dump(bTimecode, stream);
+
+ return startOffset;
+ }
+
+ return -1;
+ }
+
+ private void makeEBML(SharpStream stream) throws IOException {
+ // deafult values
+ dump(new byte[]{
+ 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
+ 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
+ 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
+ 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
+ 0x42, (byte) 0x85, (byte) 0x81, 0x02
+ }, stream);
+ }
+
+ private ArrayList makeTracks() {
+ ArrayList buffer = new ArrayList<>(1);
+ buffer.add(new byte[]{0x16, 0x54, (byte) 0xae, 0x6b});
+ buffer.add(null);
+
+ for (int i = 0; i < infoTracks.length; i++) {
+ buffer.addAll(makeTrackEntry(i, infoTracks[i]));
+ }
+
+ return lengthFor(buffer);
+ }
+
+ private ArrayList makeTrackEntry(int internalTrackId, WebMTrack track) {
+ byte[] id = encode(internalTrackId + 1, true);
+ ArrayList buffer = new ArrayList<>(12);
+
+ /* track */
+ buffer.add(new byte[]{(byte) 0xae});
+ buffer.add(null);
+
+ /* track number */
+ buffer.add(new byte[]{(byte) 0xd7});
+ buffer.add(id);
+
+ /* track uid */
+ buffer.add(new byte[]{0x73, (byte) 0xc5});
+ buffer.add(id);
+
+ /* flag lacing */
+ buffer.add(new byte[]{(byte) 0x9c, (byte) 0x81, 0x00});
+
+ /* lang */
+ buffer.add(new byte[]{0x22, (byte) 0xb5, (byte) 0x9c, (byte) 0x83, 0x75, 0x6e, 0x64});
+
+ /* codec id */
+ buffer.add(new byte[]{(byte) 0x86});
+ buffer.addAll(encode(track.codecId));
+
+ /* type */
+ buffer.add(new byte[]{(byte) 0x83});
+ buffer.add(encode(track.trackType, true));
+
+ /* default duration */
+ if (track.defaultDuration != 0) {
+ predefinedDurations[internalTrackId] = (int) Math.ceil(track.defaultDuration / (float) DEFAULT_TIMECODE_SCALE);
+ buffer.add(new byte[]{0x23, (byte) 0xe3, (byte) 0x83});
+ buffer.add(encode(track.defaultDuration, true));
+ }
+
+ /* audio/video */
+ if ((track.trackType == 1 || track.trackType == 2) && valid(track.bMetadata)) {
+ buffer.add(new byte[]{(byte) (track.trackType == 1 ? 0xe0 : 0xe1)});
+ buffer.add(encode(track.bMetadata.length, false));
+ buffer.add(track.bMetadata);
+ }
+
+ /* codec private*/
+ if (valid(track.codecPrivate)) {
+ buffer.add(new byte[]{0x63, (byte) 0xa2});
+ buffer.add(encode(track.codecPrivate.length, false));
+ buffer.add(track.codecPrivate);
+ }
+
+ return lengthFor(buffer);
+
+ }
+
+ private ArrayList makeCuePoint(int internalTrackId, KeyFrame keyFrame) {
+ ArrayList buffer = new ArrayList<>(5);
+
+ /* CuePoint */
+ buffer.add(new byte[]{(byte) 0xbb});
+ buffer.add(null);
+
+ /* CueTime */
+ buffer.add(new byte[]{(byte) 0xb3});
+ buffer.add(encode(keyFrame.atTimecode, true));
+
+ /* CueTrackPosition */
+ buffer.addAll(makeCueTrackPosition(internalTrackId, keyFrame));
+
+ return lengthFor(buffer);
+ }
+
+ private ArrayList makeCueTrackPosition(int internalTrackId, KeyFrame keyFrame) {
+ ArrayList buffer = new ArrayList<>(8);
+
+ /* CueTrackPositions */
+ buffer.add(new byte[]{(byte) 0xb7});
+ buffer.add(null);
+
+ /* CueTrack */
+ buffer.add(new byte[]{(byte) 0xf7});
+ buffer.add(encode(internalTrackId + 1, true));
+
+ /* CueClusterPosition */
+ buffer.add(new byte[]{(byte) 0xf1});
+ buffer.add(encode(keyFrame.atCluster, true));
+
+ /* CueRelativePosition */
+ if (keyFrame.atBlock > 0) {
+ buffer.add(new byte[]{(byte) 0xf0});
+ buffer.add(encode(keyFrame.atBlock, true));
+ }
+
+ return lengthFor(buffer);
+ }
+
+ private void dump(byte[] buffer, SharpStream stream) throws IOException {
+ stream.write(buffer);
+ written += buffer.length;
+ }
+
+ private ArrayList lengthFor(ArrayList buffer) {
+ long size = 0;
+ for (int i = 2; i < buffer.size(); i++) {
+ size += buffer.get(i).length;
+ }
+ buffer.set(1, encode(size, false));
+ return buffer;
+ }
+
+ private byte[] encode(long number, boolean withLength) {
+ int length = -1;
+ for (int i = 1; i <= 7; i++) {
+ if (number < Math.pow(2, 7 * i)) {
+ length = i;
+ break;
+ }
+ }
+
+ if (length < 1) {
+ throw new ArithmeticException("Can't encode a number of bigger than 7 bytes");
+ }
+
+ if (number == (Math.pow(2, 7 * length)) - 1) {
+ length++;
+ }
+
+ int offset = withLength ? 1 : 0;
+ byte[] buffer = new byte[offset + length];
+ long marker = (long) Math.floor((length - 1) / 8);
+
+ for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
+ long b = (long) Math.floor(number / mul);
+ if (!withLength && i == marker) {
+ b = b | (0x80 >> (length - 1));
+ }
+ buffer[offset + i] = (byte) b;
+ }
+
+ if (withLength) {
+ buffer[0] = (byte) (0x80 | length);
+ }
+
+ return buffer;
+ }
+
+ private ArrayList encode(String value) {
+ byte[] str;
+ try {
+ str = value.getBytes("utf-8");
+ } catch (UnsupportedEncodingException err) {
+ str = value.getBytes();
+ }
+
+ ArrayList buffer = new ArrayList<>(2);
+ buffer.add(encode(str.length, false));
+ buffer.add(str);
+
+ return buffer;
+ }
+
+ private boolean valid(byte[] buffer) {
+ return buffer != null && buffer.length > 0;
+ }
+
+ private int selectTrackForCue() {
+ int i = 0;
+ int videoTracks = 0;
+ int audioTracks = 0;
+
+ for (; i < infoTracks.length; i++) {
+ switch (infoTracks[i].trackType) {
+ case 1:
+ videoTracks++;
+ break;
+ case 2:
+ audioTracks++;
+ break;
+ }
+ }
+
+ int kind;
+ if (audioTracks == infoTracks.length) {
+ kind = 2;
+ } else if (videoTracks == infoTracks.length) {
+ kind = 1;
+ } else if (videoTracks > 0) {
+ kind = 1;
+ } else if (audioTracks > 0) {
+ kind = 2;
+ } else {
+ return 0;
+ }
+
+ // TODO: in the adove code, find and select the shortest track for the desired kind
+ for (i = 0; i < infoTracks.length; i++) {
+ if (kind == infoTracks[i].trackType) {
+ return i;
+ }
+ }
+
+ return 0;
+ }
+
+ class KeyFrame {
+
+ KeyFrame(long segment, long cluster, long block, int bTimecodeLength, long timecode) {
+ atCluster = cluster - segment;
+ if ((block - bTimecodeLength) > cluster) {
+ atBlock = (int) (block - cluster);
+ }
+ atTimecode = timecode;
+ }
+
+ long atCluster;
+ int atBlock;
+ long atTimecode;
+ }
+
+ class Block {
+
+ InputStream data;
+ int trackNumber;
+ byte flags;
+ int dataSize;
+ long absoluteTimecode;
+
+ boolean isKeyframe() {
+ return (flags & 0x80) == 0x80;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java
new file mode 100644
index 000000000..48bea06f6
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java
@@ -0,0 +1,47 @@
+package org.schabi.newpipe.streams.io;
+
+import java.io.IOException;
+
+/**
+ * based c#
+ */
+public abstract class SharpStream {
+
+ public abstract int read() throws IOException;
+
+ public abstract int read(byte buffer[]) throws IOException;
+
+ public abstract int read(byte buffer[], int offset, int count) throws IOException;
+
+ public abstract long skip(long amount) throws IOException;
+
+
+ public abstract int available();
+
+ public abstract void rewind() throws IOException;
+
+
+ public abstract void dispose();
+
+ public abstract boolean isDisposed();
+
+
+ public abstract boolean canRewind();
+
+ public abstract boolean canRead();
+
+ public abstract boolean canWrite();
+
+
+ public abstract void write(byte value) throws IOException;
+
+ public abstract void write(byte[] buffer) throws IOException;
+
+ public abstract void write(byte[] buffer, int offset, int count) throws IOException;
+
+ public abstract void flush() throws IOException;
+
+ public void setLength(long length) throws IOException {
+ throw new IOException("Not implemented");
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java
index 738135253..5e7a5f80d 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java
@@ -1,7 +1,7 @@
package us.shandian.giga.postprocessing;
-import org.schabi.newpipe.extractor.utils.Mp4DashWriter;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.Mp4DashWriter;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
index 811ec70d7..2c6dc776b 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
@@ -2,7 +2,7 @@ package us.shandian.giga.postprocessing;
import android.os.Message;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java
index 996f02d97..66b235d7c 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java
@@ -2,7 +2,7 @@ package us.shandian.giga.postprocessing;
import android.util.Log;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.util.Random;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java
index d05440d70..4c9d44548 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java
@@ -1,16 +1,25 @@
package us.shandian.giga.postprocessing;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
-import org.schabi.newpipe.extractor.utils.SubtitleConverter;
+import android.util.Log;
+
+import org.schabi.newpipe.streams.io.SharpStream;
+import org.schabi.newpipe.streams.SubtitleConverter;
+import org.xml.sax.SAXException;
import java.io.IOException;
+import java.text.ParseException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPathExpressionException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.SharpInputStream;
+
/**
* @author kapodamy
*/
class TttmlConverter extends Postprocessing {
+ private static final String TAG = "TttmlConverter";
TttmlConverter(DownloadMission mission) {
super(mission);
@@ -26,14 +35,32 @@ class TttmlConverter extends Postprocessing {
if (format == null || format.equals("ttml")) {
SubtitleConverter ttmlDumper = new SubtitleConverter();
- int res = ttmlDumper.dumpTTML(
- sources[0],
- out,
- getArgumentAt(1, "true").equals("true"),
- getArgumentAt(2, "true").equals("true")
- );
+ try {
+ ttmlDumper.dumpTTML(
+ sources[0],
+ out,
+ getArgumentAt(1, "true").equals("true"),
+ getArgumentAt(2, "true").equals("true")
+ );
+ } catch (Exception err) {
+ Log.e(TAG, "subtitle parse failed", err);
- return res == 0 ? OK_RESULT : res;
+ if (err instanceof IOException) {
+ return 1;
+ } else if (err instanceof ParseException) {
+ return 2;
+ } else if (err instanceof SAXException) {
+ return 3;
+ } else if (err instanceof ParserConfigurationException) {
+ return 4;
+ } else if (err instanceof XPathExpressionException) {
+ return 7;
+ }
+
+ return 8;
+ }
+
+ return OK_RESULT;
} else if (format.equals("srt")) {
byte[] buffer = new byte[8 * 1024];
int read;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java
index d73fdc3b7..c69809e00 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java
@@ -1,9 +1,9 @@
package us.shandian.giga.postprocessing;
-import org.schabi.newpipe.extractor.utils.WebMReader.TrackKind;
-import org.schabi.newpipe.extractor.utils.WebMReader.WebMTrack;
-import org.schabi.newpipe.extractor.utils.WebMWriter;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.WebMReader.TrackKind;
+import org.schabi.newpipe.streams.WebMReader.WebMTrack;
+import org.schabi.newpipe.streams.WebMWriter;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java
index f3e3ccdda..cd62c5d22 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java
@@ -1,6 +1,6 @@
package us.shandian.giga.postprocessing.io;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java
index 3d4f2931f..531e0587e 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java
@@ -1,6 +1,6 @@
package us.shandian.giga.postprocessing.io;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java
index dd3f8c697..c1b675eef 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java
@@ -1,6 +1,6 @@
package us.shandian.giga.postprocessing.io;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.RandomAccessFile;
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java
index 831afbfc2..52e0775da 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java
@@ -7,7 +7,7 @@ package us.shandian.giga.postprocessing.io;
import android.support.annotation.NonNull;
-import org.schabi.newpipe.extractor.utils.io.SharpStream;
+import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;