diff --git a/app/build.gradle b/app/build.gradle index 893885c6d..2507ec1b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,7 +99,6 @@ task runCheckstyle(type: Checkstyle) { exclude '**/R.java' exclude '**/BuildConfig.java' exclude 'main/java/us/shandian/giga/**' - exclude 'main/java/org/schabi/newpipe/streams/**' // empty classpath classpath = files() diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 8c57d8978..0f142ad32 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -10,13 +10,12 @@ import java.io.InputStream; * @author kapodamy */ public class DataReader { + public static final int SHORT_SIZE = 2; + public static final int LONG_SIZE = 8; + public static final int INTEGER_SIZE = 4; + public static final int FLOAT_SIZE = 4; - 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 final static int BUFFER_SIZE = 128 * 1024;// 128 KiB + private static final int BUFFER_SIZE = 128 * 1024; // 128 KiB private long position = 0; private final SharpStream stream; @@ -24,7 +23,7 @@ public class DataReader { private InputStream view; private int viewSize; - public DataReader(SharpStream stream) { + public DataReader(final SharpStream stream) { this.stream = stream; this.readOffset = this.readBuffer.length; } @@ -74,6 +73,7 @@ public class DataReader { return value & 0xffffffffL; } + public short readShort() throws IOException { primitiveRead(SHORT_SIZE); return (short) (primitive[0] << 8 | primitive[1]); @@ -86,11 +86,11 @@ public class DataReader { return high << 32 | low; } - public int read(byte[] buffer) throws IOException { + public int read(final byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } - public int read(byte[] buffer, int offset, int count) throws IOException { + public int read(final byte[] buffer, int offset, int count) throws IOException { if (readCount < 0) { return -1; } @@ -135,7 +135,7 @@ public class DataReader { stream.rewind(); if ((position - viewSize) > 0) { - viewSize = 0;// drop view + viewSize = 0; // drop view } else { viewSize += position; } @@ -157,7 +157,7 @@ public class DataReader { * @param size the size of the view * @return the view */ - public InputStream getView(int size) { + public InputStream getView(final int size) { if (view == null) { view = new InputStream() { @Override @@ -173,12 +173,13 @@ public class DataReader { } @Override - public int read(byte[] buffer) throws IOException { + public int read(final byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override - public int read(byte[] buffer, int offset, int count) throws IOException { + public int read(final byte[] buffer, final int offset, final int count) + throws IOException { if (viewSize < 1) { return -1; } @@ -190,7 +191,7 @@ public class DataReader { } @Override - public long skip(long amount) throws IOException { + public long skip(final long amount) throws IOException { if (viewSize < 1) { return 0; } @@ -224,16 +225,18 @@ public class DataReader { private final short[] primitive = new short[LONG_SIZE]; - private void primitiveRead(int amount) throws IOException { + private void primitiveRead(final int amount) throws IOException { byte[] buffer = new byte[amount]; int read = read(buffer, 0, amount); if (read != amount) { - throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes"); + throw new EOFException("Truncated stream, missing " + + String.valueOf(amount - read) + " bytes"); } for (int i = 0; i < amount; i++) { - primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying + // the "byte" data type in java is signed and is very annoying + primitive[i] = (short) (buffer[i] & 0xFF); } } @@ -256,5 +259,4 @@ public class DataReader { return readCount < 1; } - } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java index b7efa038e..8fad7fa7c 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -14,7 +14,6 @@ import java.util.NoSuchElementException; * @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; @@ -50,7 +49,6 @@ public class Mp4DashReader { private static final int HANDLER_SOUN = 0x736F756E; private static final int HANDLER_SUBT = 0x73756274; - private final DataReader stream; private Mp4Track[] tracks = null; @@ -68,7 +66,7 @@ public class Mp4DashReader { Audio, Video, Subtitles, Other } - public Mp4DashReader(SharpStream source) { + public Mp4DashReader(final SharpStream source) { this.stream = new DataReader(source); } @@ -78,14 +76,15 @@ public class Mp4DashReader { } box = readBox(ATOM_FTYP); - brands = parse_ftyp(box); + brands = parseFtyp(box); switch (brands[0]) { case BRAND_DASH: case BRAND_ISO5:// ¿why not? break; default: throw new NoSuchElementException( - "Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0]) + "Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + + boxName(brands[0]) ); } @@ -98,7 +97,7 @@ public class Mp4DashReader { switch (box.type) { case ATOM_MOOV: - moov = parse_moov(box); + moov = parseMoov(box); break; case ATOM_SIDX: break; @@ -117,10 +116,10 @@ public class Mp4DashReader { 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.mvexTrex != null) { + for (Trex mvexTrex : moov.mvexTrex) { + if (tracks[i].trak.tkhd.trackId == mvexTrex.trackId) { + tracks[i].trex = mvexTrex; } } } @@ -144,7 +143,7 @@ public class Mp4DashReader { backupBox = box; } - Mp4Track selectTrack(int index) { + Mp4Track selectTrack(final int index) { selectedTrack = index; return tracks[index]; } @@ -179,7 +178,7 @@ public class Mp4DashReader { Box traf; while ((traf = untilBox(tmp, ATOM_TRAF)) != null) { Box tfhd = readBox(ATOM_TFHD); - if (parse_tfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) { + if (parseTfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) { count++; break; } @@ -196,7 +195,9 @@ public class Mp4DashReader { } public int[] getBrands() { - if (brands == null) throw new IllegalStateException("Not parsed"); + if (brands == null) { + throw new IllegalStateException("Not parsed"); + } return brands; } @@ -219,7 +220,7 @@ public class Mp4DashReader { return tracks; } - public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException { + public Mp4DashChunk getNextChunk(final boolean infoOnly) throws IOException { Mp4Track track = tracks[selectedTrack]; while (stream.available()) { @@ -240,27 +241,31 @@ public class Mp4DashReader { throw new IOException("moof found without mdat"); } - moof = parse_moof(box, track.trak.tkhd.trackId); + moof = parseMoof(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"); + 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; + moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize + * moof.traf.trun.entryCount; } else { moof.traf.trun.chunkSize = (int) (box.size - 8); } } - if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) { + 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; + moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration + * moof.traf.trun.entryCount; } } } @@ -272,7 +277,7 @@ public class Mp4DashReader { if (moof.traf == null) { moof = null; - continue;// find another chunk + continue; // find another chunk } Mp4DashChunk chunk = new Mp4DashChunk(); @@ -292,17 +297,15 @@ public class Mp4DashReader { return null; } - - - public static boolean hasFlag(int flags, int mask) { + public static boolean hasFlag(final int flags, final int mask) { return (flags & mask) == mask; } - private String boxName(Box ref) { + private String boxName(final Box ref) { return boxName(ref.type); } - private String boxName(int type) { + private String boxName(final int type) { try { return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8"); } catch (UnsupportedEncodingException e) { @@ -323,15 +326,16 @@ public class Mp4DashReader { return b; } - private Box readBox(int expected) throws IOException { + private Box readBox(final int expected) throws IOException { Box b = readBox(); if (b.type != expected) { - throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b)); + throw new NoSuchElementException("expected " + boxName(expected) + + " found " + boxName(b)); } return b; } - private byte[] readFullBox(Box ref) throws IOException { + private byte[] readFullBox(final Box ref) throws IOException { // full box reading is limited to 2 GiB, and should be enough int size = (int) ref.size; @@ -342,15 +346,14 @@ public class Mp4DashReader { int read = size - 8; if (stream.read(buffer.array(), 8, read) != read) { - throw new EOFException( - String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size) - ); + throw new EOFException(String.format("EOF reached in box: type=%s offset=%s size=%s", + boxName(ref.type), ref.offset, ref.size)); } return buffer.array(); } - private void ensure(Box ref) throws IOException { + private void ensure(final Box ref) throws IOException { long skip = ref.offset + ref.size - stream.position(); if (skip == 0) { @@ -365,7 +368,7 @@ public class Mp4DashReader { stream.skipBytes((int) skip); } - private Box untilBox(Box ref, int... expected) throws IOException { + private Box untilBox(final Box ref, final int... expected) throws IOException { Box b; while (stream.position() < (ref.offset + ref.size)) { b = readBox(); @@ -380,7 +383,7 @@ public class Mp4DashReader { return null; } - private Box untilAnyBox(Box ref) throws IOException { + private Box untilAnyBox(final Box ref) throws IOException { if (stream.position() >= (ref.offset + ref.size)) { return null; } @@ -388,17 +391,15 @@ public class Mp4DashReader { return readBox(); } - - - private Moof parse_moof(Box ref, int trackId) throws IOException { + private Moof parseMoof(final Box ref, final int trackId) throws IOException { Moof obj = new Moof(); Box b = readBox(ATOM_MFHD); - obj.mfhd_SequenceNumber = parse_mfhd(); + obj.mfhdSequenceNumber = parseMfhd(); ensure(b); while ((b = untilBox(ref, ATOM_TRAF)) != null) { - obj.traf = parse_traf(b, trackId); + obj.traf = parseTraf(b, trackId); ensure(b); if (obj.traf != null) { @@ -409,7 +410,7 @@ public class Mp4DashReader { return obj; } - private int parse_mfhd() throws IOException { + private int parseMfhd() throws IOException { // version // flags stream.skipBytes(4); @@ -417,11 +418,11 @@ public class Mp4DashReader { return stream.readInt(); } - private Traf parse_traf(Box ref, int trackId) throws IOException { + private Traf parseTraf(final Box ref, final int trackId) throws IOException { Traf traf = new Traf(); Box b = readBox(ATOM_TFHD); - traf.tfhd = parse_tfhd(trackId); + traf.tfhd = parseTfhd(trackId); ensure(b); if (traf.tfhd == null) { @@ -431,18 +432,18 @@ public class Mp4DashReader { b = untilBox(ref, ATOM_TRUN, ATOM_TFDT); if (b.type == ATOM_TFDT) { - traf.tfdt = parse_tfdt(); + traf.tfdt = parseTfdt(); ensure(b); b = readBox(ATOM_TRUN); } - traf.trun = parse_trun(); + traf.trun = parseTrun(); ensure(b); return traf; } - private Tfhd parse_tfhd(int trackId) throws IOException { + private Tfhd parseTfhd(final int trackId) throws IOException { Tfhd obj = new Tfhd(); obj.bFlags = stream.readInt(); @@ -471,31 +472,31 @@ public class Mp4DashReader { return obj; } - private long parse_tfdt() throws IOException { + private long parseTfdt() throws IOException { int version = stream.read(); - stream.skipBytes(3);// flags + stream.skipBytes(3); // flags return version == 0 ? stream.readUnsignedInt() : stream.readLong(); } - private Trun parse_trun() throws IOException { + private Trun parseTrun() throws IOException { Trun obj = new Trun(); obj.bFlags = stream.readInt(); - obj.entryCount = stream.readInt();// unsigned int + obj.entryCount = stream.readInt(); // unsigned int - obj.entries_rowSize = 0; + obj.entriesRowSize = 0; if (hasFlag(obj.bFlags, 0x0100)) { - obj.entries_rowSize += 4; + obj.entriesRowSize += 4; } if (hasFlag(obj.bFlags, 0x0200)) { - obj.entries_rowSize += 4; + obj.entriesRowSize += 4; } if (hasFlag(obj.bFlags, 0x0400)) { - obj.entries_rowSize += 4; + obj.entriesRowSize += 4; } if (hasFlag(obj.bFlags, 0x0800)) { - obj.entries_rowSize += 4; + obj.entriesRowSize += 4; } - obj.bEntries = new byte[obj.entries_rowSize * obj.entryCount]; + obj.bEntries = new byte[obj.entriesRowSize * obj.entryCount]; if (hasFlag(obj.bFlags, 0x0001)) { obj.dataOffset = stream.readInt(); @@ -524,23 +525,24 @@ public class Mp4DashReader { return obj; } - private int[] parse_ftyp(Box ref) throws IOException { + private int[] parseFtyp(final Box ref) throws IOException { int i = 0; int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)]; - list[i++] = stream.readInt();// major brand + list[i++] = stream.readInt(); // major brand - stream.skipBytes(4);// minor version + stream.skipBytes(4); // minor version - for (; i < list.length; i++) - list[i] = stream.readInt();// compatible brands + for (; i < list.length; i++) { + list[i] = stream.readInt(); // compatible brands + } return list; } - private Mvhd parse_mvhd() throws IOException { + private Mvhd parseMvhd() throws IOException { int version = stream.read(); - stream.skipBytes(3);// flags + stream.skipBytes(3); // flags // creation entries_time // modification entries_time @@ -564,7 +566,7 @@ public class Mp4DashReader { return obj; } - private Tkhd parse_tkhd() throws IOException { + private Tkhd parseTkhd() throws IOException { int version = stream.read(); Tkhd obj = new Tkhd(); @@ -576,17 +578,17 @@ public class Mp4DashReader { obj.trackId = stream.readInt(); - stream.skipBytes(4);// reserved + stream.skipBytes(4); // reserved obj.duration = version == 0 ? stream.readUnsignedInt() : stream.readLong(); - stream.skipBytes(2 * 4);// reserved + stream.skipBytes(2 * 4); // reserved obj.bLayer = stream.readShort(); obj.bAlternateGroup = stream.readShort(); obj.bVolume = stream.readShort(); - stream.skipBytes(2);// reserved + stream.skipBytes(2); // reserved obj.matrix = new byte[9 * 4]; stream.read(obj.matrix); @@ -597,20 +599,20 @@ public class Mp4DashReader { return obj; } - private Trak parse_trak(Box ref) throws IOException { + private Trak parseTrak(final Box ref) throws IOException { Trak trak = new Trak(); Box b = readBox(ATOM_TKHD); - trak.tkhd = parse_tkhd(); + trak.tkhd = parseTkhd(); ensure(b); while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) { switch (b.type) { case ATOM_MDIA: - trak.mdia = parse_mdia(b); + trak.mdia = parseMdia(b); break; case ATOM_EDTS: - trak.edst_elst = parse_edts(b); + trak.edstElst = parseEdts(b); break; } @@ -620,7 +622,7 @@ public class Mp4DashReader { return trak; } - private Mdia parse_mdia(Box ref) throws IOException { + private Mdia parseMdia(final Box ref) throws IOException { Mdia obj = new Mdia(); Box b; @@ -633,13 +635,13 @@ public class Mp4DashReader { ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd); byte version = buffer.get(8); buffer.position(12 + ((version == 0 ? 4 : 8) * 2)); - obj.mdhd_timeScale = buffer.getInt(); + obj.mdhdTimeScale = buffer.getInt(); break; case ATOM_HDLR: - obj.hdlr = parse_hdlr(b); + obj.hdlr = parseHdlr(b); break; case ATOM_MINF: - obj.minf = parse_minf(b); + obj.minf = parseMinf(b); break; } ensure(b); @@ -648,7 +650,7 @@ public class Mp4DashReader { return obj; } - private Hdlr parse_hdlr(Box ref) throws IOException { + private Hdlr parseHdlr(final Box ref) throws IOException { // version // flags stream.skipBytes(4); @@ -666,10 +668,10 @@ public class Mp4DashReader { return obj; } - private Moov parse_moov(Box ref) throws IOException { + private Moov parseMoov(final Box ref) throws IOException { Box b = readBox(ATOM_MVHD); Moov moov = new Moov(); - moov.mvhd = parse_mvhd(); + moov.mvhd = parseMvhd(); ensure(b); ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId); @@ -677,10 +679,10 @@ public class Mp4DashReader { switch (b.type) { case ATOM_TRAK: - tmp.add(parse_trak(b)); + tmp.add(parseTrak(b)); break; case ATOM_MVEX: - moov.mvex_trex = parse_mvex(b, (int) moov.mvhd.nextTrackId); + moov.mvexTrex = parseMvex(b, (int) moov.mvhd.nextTrackId); break; } @@ -692,19 +694,19 @@ public class Mp4DashReader { return moov; } - private Trex[] parse_mvex(Box ref, int possibleTrackCount) throws IOException { + private Trex[] parseMvex(final Box ref, final int possibleTrackCount) throws IOException { ArrayList tmp = new ArrayList<>(possibleTrackCount); Box b; while ((b = untilBox(ref, ATOM_TREX)) != null) { - tmp.add(parse_trex()); + tmp.add(parseTrex()); ensure(b); } return tmp.toArray(new Trex[0]); } - private Trex parse_trex() throws IOException { + private Trex parseTrex() throws IOException { // version // flags stream.skipBytes(4); @@ -719,7 +721,7 @@ public class Mp4DashReader { return obj; } - private Elst parse_edts(Box ref) throws IOException { + private Elst parseEdts(final Box ref) throws IOException { Box b = untilBox(ref, ATOM_ELST); if (b == null) { return null; @@ -728,22 +730,22 @@ public class Mp4DashReader { Elst obj = new Elst(); boolean v1 = stream.read() == 1; - stream.skipBytes(3);// flags + stream.skipBytes(3); // flags int entryCount = stream.readInt(); if (entryCount < 1) { - obj.bMediaRate = 0x00010000;// default media rate (1.0) + obj.bMediaRate = 0x00010000; // default media rate (1.0) return obj; } if (v1) { - stream.skipBytes(DataReader.LONG_SIZE);// segment duration - obj.MediaTime = stream.readLong(); + stream.skipBytes(DataReader.LONG_SIZE); // segment duration + obj.mediaTime = stream.readLong(); // ignore all remain entries stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2)); } else { - stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration - obj.MediaTime = stream.readInt(); + stream.skipBytes(DataReader.INTEGER_SIZE); // segment duration + obj.mediaTime = stream.readInt(); } obj.bMediaRate = stream.readInt(); @@ -751,7 +753,7 @@ public class Mp4DashReader { return obj; } - private Minf parse_minf(Box ref) throws IOException { + private Minf parseMinf(final Box ref) throws IOException { Minf obj = new Minf(); Box b; @@ -762,11 +764,11 @@ public class Mp4DashReader { obj.dinf = readFullBox(b); break; case ATOM_STBL: - obj.stbl_stsd = parse_stbl(b); + obj.stblStsd = parseStbl(b); break; case ATOM_VMHD: case ATOM_SMHD: - obj.$mhd = readFullBox(b); + obj.mhd = readFullBox(b); break; } @@ -777,42 +779,39 @@ public class Mp4DashReader { } /** - * this only read the "stsd" box inside + * This only reads the "stsd" box inside. + * + * @param ref stbl box + * @return stsd box inside */ - private byte[] parse_stbl(Box ref) throws IOException { + private byte[] parseStbl(final Box ref) throws IOException { Box b = untilBox(ref, ATOM_STSD); if (b == null) { - return new byte[0];// this never should happens (missing codec startup data) + return new byte[0]; // this never should happens (missing codec startup data) } return readFullBox(b); } - - class Box { - int type; long offset; long size; } public class Moof { - - int mfhd_SequenceNumber; + int mfhdSequenceNumber; public Traf traf; } public class Traf { - public Tfhd tfhd; long tfdt; public Trun trun; } public class Tfhd { - int bFlags; public int trackId; int defaultSampleDuration; @@ -821,7 +820,6 @@ public class Mp4DashReader { } class TrunEntry { - int sampleDuration; int sampleSize; int sampleFlags; @@ -833,7 +831,6 @@ public class Mp4DashReader { } public class Trun { - public int chunkDuration; public int chunkSize; @@ -843,10 +840,10 @@ public class Mp4DashReader { public int entryCount; byte[] bEntries; - int entries_rowSize; + int entriesRowSize; - public TrunEntry getEntry(int i) { - ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entries_rowSize, entries_rowSize); + public TrunEntry getEntry(final int i) { + ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entriesRowSize, entriesRowSize); TrunEntry entry = new TrunEntry(); if (hasFlag(bFlags, 0x0100)) { @@ -868,7 +865,7 @@ public class Mp4DashReader { return entry; } - public TrunEntry getAbsoluteEntry(int i, Tfhd header) { + public TrunEntry getAbsoluteEntry(final int i, final Tfhd header) { TrunEntry entry = getEntry(i); if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) { @@ -892,7 +889,6 @@ public class Mp4DashReader { } public class Tkhd { - int trackId; long duration; short bVolume; @@ -904,28 +900,24 @@ public class Mp4DashReader { } public class Trak { - public Tkhd tkhd; - public Elst edst_elst; + public Elst edstElst; public Mdia mdia; } class Mvhd { - long timeScale; long nextTrackId; } class Moov { - Mvhd mvhd; Trak[] trak; - Trex[] mvex_trex; + Trex[] mvexTrex; } public class Trex { - private int trackId; int defaultSampleDescriptionIndex; int defaultSampleDuration; @@ -934,42 +926,36 @@ public class Mp4DashReader { } public class Elst { - - public long MediaTime; + public long mediaTime; public int bMediaRate; } public class Mdia { - - public int mdhd_timeScale; + public int mdhdTimeScale; public byte[] mdhd; public Hdlr hdlr; public Minf minf; } public class Hdlr { - public int type; public int subType; public byte[] bReserved; } public class Minf { - public byte[] dinf; - public byte[] stbl_stsd; - public byte[] $mhd; + public byte[] stblStsd; + public byte[] mhd; } public class Mp4Track { - public TrackKind kind; public Trak trak; public Trex trex; } public class Mp4DashChunk { - public InputStream data; public Moof moof; private int i = 0; @@ -1002,9 +988,7 @@ public class Mp4DashReader { } public class Mp4DashSample { - public TrunEntry info; public byte[] data; } - } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 67f68d3a7..2fc887896 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -17,13 +17,15 @@ import java.util.ArrayList; * @author kapodamy */ public class Mp4FromDashWriter { - - private final static int EPOCH_OFFSET = 2082844800; - private final static short DEFAULT_TIMESCALE = 1000; - private final static byte SAMPLES_PER_CHUNK_INIT = 2; - private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 - private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB - private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s + private static final int EPOCH_OFFSET = 2082844800; + private static final short DEFAULT_TIMESCALE = 1000; + private static final byte SAMPLES_PER_CHUNK_INIT = 2; + // ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 + private static final byte SAMPLES_PER_CHUNK = 6; + // near 3.999 GiB + private static final long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL; + // 2.2 MiB enough for: 1080p 60fps 00h35m00s + private static final int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); private final long time; @@ -48,7 +50,7 @@ public class Mp4FromDashWriter { private final ArrayList compatibleBrands = new ArrayList<>(5); - public Mp4FromDashWriter(SharpStream... sources) throws IOException { + public Mp4FromDashWriter(final SharpStream... sources) throws IOException { for (SharpStream src : sources) { if (!src.canRewind() && !src.canRead()) { throw new IOException("All sources must be readable and allow rewind"); @@ -60,12 +62,12 @@ public class Mp4FromDashWriter { readersChunks = new Mp4DashChunk[readers.length]; time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET; - compatibleBrands.add(0x6D703431);// mp41 - compatibleBrands.add(0x69736F6D);// isom - compatibleBrands.add(0x69736F32);// iso2 + compatibleBrands.add(0x6D703431); // mp41 + compatibleBrands.add(0x69736F6D); // isom + compatibleBrands.add(0x69736F32); // iso2 } - public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException { + public Mp4Track[] getTracksFromSource(final int sourceIndex) throws IllegalStateException { if (!parsed) { throw new IllegalStateException("All sources must be parsed first"); } @@ -92,7 +94,7 @@ public class Mp4FromDashWriter { } } - public void selectTracks(int... trackIndex) throws IOException { + public void selectTracks(final int... trackIndex) throws IOException { if (done) { throw new IOException("already done"); } @@ -110,7 +112,7 @@ public class Mp4FromDashWriter { } } - public void setMainBrand(int brand) { + public void setMainBrand(final int brand) { overrideMainBrand = brand; } @@ -140,7 +142,7 @@ public class Mp4FromDashWriter { outStream = null; } - public void build(SharpStream output) throws IOException { + public void build(final SharpStream output) throws IOException { if (done) { throw new RuntimeException("already done"); } @@ -153,7 +155,7 @@ public class Mp4FromDashWriter { // not allowed for very short tracks (less than 0.5 seconds) // outStream = output; - long read = 8;// mdat box header size + long read = 8; // mdat box header size long totalSampleSize = 0; int[] sampleExtra = new int[readers.length]; int[] defaultMediaTime = new int[readers.length]; @@ -165,12 +167,12 @@ public class Mp4FromDashWriter { tablesInfo[i] = new TablesInfo(); } - int single_sample_buffer; + int singleSampleBuffer; if (tracks.length == 1 && tracks[0].kind == TrackKind.Audio) { // near 1 second of audio data per chunk, avoid split the audio stream in large chunks - single_sample_buffer = tracks[0].trak.mdia.mdhd_timeScale / 1000; + singleSampleBuffer = tracks[0].trak.mdia.mdhdTimeScale / 1000; } else { - single_sample_buffer = -1; + singleSampleBuffer = -1; } @@ -187,7 +189,7 @@ public class Mp4FromDashWriter { } read += chunk.moof.traf.trun.chunkSize; - sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration + sampleExtra[i] += chunk.moof.traf.trun.chunkDuration; // calculate track duration TrunEntry info; while ((info = chunk.getNextSampleInfo()) != null) { @@ -222,8 +224,8 @@ public class Mp4FromDashWriter { readers[i].rewind(); - if (single_sample_buffer > 0) { - initChunkTables(tablesInfo[i], single_sample_buffer, single_sample_buffer); + if (singleSampleBuffer > 0) { + initChunkTables(tablesInfo[i], singleSampleBuffer, singleSampleBuffer); } else { initChunkTables(tablesInfo[i], SAMPLES_PER_CHUNK_INIT, SAMPLES_PER_CHUNK); } @@ -232,18 +234,18 @@ public class Mp4FromDashWriter { if (sampleSizeChanges == 1) { tablesInfo[i].stsz = 0; - tablesInfo[i].stsz_default = samplesSize; + tablesInfo[i].stszDefault = samplesSize; } else { - tablesInfo[i].stsz_default = 0; + tablesInfo[i].stszDefault = 0; } if (tablesInfo[i].stss == tablesInfo[i].stsz) { - tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes) + tablesInfo[i].stss = -1; // for audio tracks (all samples are keyframes) } // ensure track duration if (tracks[i].trak.tkhd.duration < 1) { - tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen + tracks[i].trak.tkhd.duration = sampleExtra[i]; // this never should happen } } @@ -251,21 +253,21 @@ public class Mp4FromDashWriter { boolean is64 = read > THRESHOLD_FOR_CO64; // calculate the moov size - int auxSize = make_moov(defaultMediaTime, tablesInfo, is64); + int auxSize = makeMoov(defaultMediaTime, tablesInfo, is64); if (auxSize < THRESHOLD_MOOV_LENGTH) { - auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory + auxBuffer = ByteBuffer.allocate(auxSize); // cache moov in the memory } moovSimulation = false; writeOffset = 0; - final int ftyp_size = make_ftyp(); + final int ftypSize = makeFtyp(); // reserve moov space in the output stream if (auxSize > 0) { int length = auxSize; - byte[] buffer = new byte[64 * 1024];// 64 KiB + byte[] buffer = new byte[64 * 1024]; // 64 KiB while (length > 0) { int count = Math.min(length, buffer.length); outWrite(buffer, count); @@ -274,21 +276,22 @@ public class Mp4FromDashWriter { } if (auxBuffer == null) { - outSeek(ftyp_size); + outSeek(ftypSize); } // tablesInfo contains row counts - // and after returning from make_moov() will contain those table offsets - make_moov(defaultMediaTime, tablesInfo, is64); + // and after returning from makeMoov() will contain those table offsets + makeMoov(defaultMediaTime, tablesInfo, is64); // write tables: stts stsc sbgp // reset for ctts table: sampleCount sampleExtra for (int i = 0; i < readers.length; i++) { writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]); - writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries); - tablesInfo[i].stsc_bEntries = null; + writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stscBEntries.length, + tablesInfo[i].stscBEntries); + tablesInfo[i].stscBEntries = null; if (tablesInfo[i].ctts > 0) { - sampleCount[i] = 1;// the index is not base zero + sampleCount[i] = 1; // the index is not base zero sampleExtra[i] = -1; } if (tablesInfo[i].sbgp > 0) { @@ -300,11 +303,11 @@ public class Mp4FromDashWriter { outRestore(); } - outWrite(make_mdat(totalSampleSize, is64)); + outWrite(makeMdat(totalSampleSize, is64)); int[] sampleIndex = new int[readers.length]; - int[] sizes = new int[single_sample_buffer > 0 ? single_sample_buffer : SAMPLES_PER_CHUNK]; - int[] sync = new int[single_sample_buffer > 0 ? single_sample_buffer : SAMPLES_PER_CHUNK]; + int[] sizes = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; + int[] sync = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; int written = readers.length; while (written > 0) { @@ -312,14 +315,14 @@ public class Mp4FromDashWriter { for (int i = 0; i < readers.length; i++) { if (sampleIndex[i] < 0) { - continue;// track is done + continue; // track is done } long chunkOffset = writeOffset; int syncCount = 0; int limit; - if (single_sample_buffer > 0) { - limit = single_sample_buffer; + if (singleSampleBuffer > 0) { + limit = singleSampleBuffer; } else { limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK; } @@ -330,7 +333,8 @@ public class Mp4FromDashWriter { if (sample == null) { if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) { - writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries + writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], + sampleExtra[i]); // flush last entries outRestore(); } sampleIndex[i] = -1; @@ -344,7 +348,8 @@ public class Mp4FromDashWriter { sampleCount[i]++; } else { if (sampleExtra[i] >= 0) { - tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]); + tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, + sampleCount[i], sampleExtra[i]); outRestore(); } sampleCount[i] = 1; @@ -378,7 +383,8 @@ public class Mp4FromDashWriter { if (is64) { tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset); } else { - tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset); + tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, + (int) chunkOffset); } } @@ -389,17 +395,17 @@ public class Mp4FromDashWriter { if (auxBuffer != null) { // dump moov - outSeek(ftyp_size); + outSeek(ftypSize); outStream.write(auxBuffer.array(), 0, auxBuffer.capacity()); auxBuffer = null; } } - private Mp4DashSample getNextSample(int track) throws IOException { + private Mp4DashSample getNextSample(final int track) throws IOException { if (readersChunks[track] == null) { readersChunks[track] = readers[track].getNextChunk(false); if (readersChunks[track] == null) { - return null;// EOF reached + return null; // EOF reached } } @@ -413,7 +419,7 @@ public class Mp4FromDashWriter { } - private int writeEntry64(int offset, long value) throws IOException { + private int writeEntry64(final int offset, final long value) throws IOException { outBackup(); auxSeek(offset); @@ -422,7 +428,8 @@ public class Mp4FromDashWriter { return offset + 8; } - private int writeEntryArray(int offset, int count, int... values) throws IOException { + private int writeEntryArray(final int offset, final int count, final int... values) + throws IOException { outBackup(); auxSeek(offset); @@ -456,7 +463,8 @@ public class Mp4FromDashWriter { } } - private void initChunkTables(TablesInfo tables, int firstCount, int succesiveCount) { + private void initChunkTables(final TablesInfo tables, final int firstCount, + final int succesiveCount) { // tables.stsz holds amount of samples of the track (total) int totalSamples = (tables.stsz - firstCount); float chunkAmount = totalSamples / (float) succesiveCount; @@ -473,36 +481,36 @@ public class Mp4FromDashWriter { } // stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index] - tables.stsc_bEntries = new int[tables.stsc * 3]; - tables.stco = remainChunkOffset + 1;// total entrys in chunk offset box + tables.stscBEntries = new int[tables.stsc * 3]; + tables.stco = remainChunkOffset + 1; // total entrys in chunk offset box - tables.stsc_bEntries[index++] = 1; - tables.stsc_bEntries[index++] = firstCount; - tables.stsc_bEntries[index++] = 1; + tables.stscBEntries[index++] = 1; + tables.stscBEntries[index++] = firstCount; + tables.stscBEntries[index++] = 1; if (firstCount != succesiveCount) { - tables.stsc_bEntries[index++] = 2; - tables.stsc_bEntries[index++] = succesiveCount; - tables.stsc_bEntries[index++] = 1; + tables.stscBEntries[index++] = 2; + tables.stscBEntries[index++] = succesiveCount; + tables.stscBEntries[index++] = 1; } if (remain) { - tables.stsc_bEntries[index++] = remainChunkOffset + 1; - tables.stsc_bEntries[index++] = totalSamples % succesiveCount; - tables.stsc_bEntries[index] = 1; + tables.stscBEntries[index++] = remainChunkOffset + 1; + tables.stscBEntries[index++] = totalSamples % succesiveCount; + tables.stscBEntries[index] = 1; } } - private void outWrite(byte[] buffer) throws IOException { + private void outWrite(final byte[] buffer) throws IOException { outWrite(buffer, buffer.length); } - private void outWrite(byte[] buffer, int count) throws IOException { + private void outWrite(final byte[] buffer, final int count) throws IOException { writeOffset += count; outStream.write(buffer, 0, count); } - private void outSeek(long offset) throws IOException { + private void outSeek(final long offset) throws IOException { if (outStream.canSeek()) { outStream.seek(offset); writeOffset = offset; @@ -515,12 +523,12 @@ public class Mp4FromDashWriter { } } - private void outSkip(long amount) throws IOException { + private void outSkip(final long amount) throws IOException { outStream.skip(amount); writeOffset += amount; } - private int lengthFor(int offset) throws IOException { + private int lengthFor(final int offset) throws IOException { int size = auxOffset() - offset; if (moovSimulation) { @@ -534,7 +542,8 @@ public class Mp4FromDashWriter { return size; } - private int make(int type, int extra, int columns, int rows) throws IOException { + private int make(final int type, final int extra, final int columns, final int rows) + throws IOException { final byte base = 16; int size = columns * rows * 4; int total = size + base; @@ -562,14 +571,14 @@ public class Mp4FromDashWriter { return offset + base; } - private void auxWrite(int value) throws IOException { + private void auxWrite(final int value) throws IOException { auxWrite(ByteBuffer.allocate(4) .putInt(value) .array() ); } - private void auxWrite(byte[] buffer) throws IOException { + private void auxWrite(final byte[] buffer) throws IOException { if (moovSimulation) { writeOffset += buffer.length; } else if (auxBuffer == null) { @@ -579,7 +588,7 @@ public class Mp4FromDashWriter { } } - private void auxSeek(int offset) throws IOException { + private void auxSeek(final int offset) throws IOException { if (moovSimulation) { writeOffset = offset; } else if (auxBuffer == null) { @@ -589,7 +598,7 @@ public class Mp4FromDashWriter { } } - private void auxSkip(int amount) throws IOException { + private void auxSkip(final int amount) throws IOException { if (moovSimulation) { writeOffset += amount; } else if (auxBuffer == null) { @@ -603,27 +612,27 @@ public class Mp4FromDashWriter { return auxBuffer == null ? (int) writeOffset : auxBuffer.position(); } - - - private int make_ftyp() throws IOException { + private int makeFtyp() throws IOException { int size = 16 + (compatibleBrands.size() * 4); - if (overrideMainBrand != 0) size += 4; + if (overrideMainBrand != 0) { + size += 4; + } ByteBuffer buffer = ByteBuffer.allocate(size); buffer.putInt(size); - buffer.putInt(0x66747970);// "ftyp" + buffer.putInt(0x66747970); // "ftyp" if (overrideMainBrand == 0) { - buffer.putInt(0x6D703432);// mayor brand "mp42" - buffer.putInt(512);// default minor version + buffer.putInt(0x6D703432); // mayor brand "mp42" + buffer.putInt(512); // default minor version } else { buffer.putInt(overrideMainBrand); buffer.putInt(0); - buffer.putInt(0x6D703432);// "mp42" compatible brand + buffer.putInt(0x6D703432); // "mp42" compatible brand } for (Integer brand : compatibleBrands) { - buffer.putInt(brand);// compatible brand + buffer.putInt(brand); // compatible brand } outWrite(buffer.array()); @@ -631,7 +640,7 @@ public class Mp4FromDashWriter { return size; } - private byte[] make_mdat(long refSize, boolean is64) { + private byte[] makeMdat(long refSize, final boolean is64) { if (is64) { refSize += 16; } else { @@ -640,7 +649,7 @@ public class Mp4FromDashWriter { ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) .putInt(is64 ? 0x01 : (int) refSize) - .putInt(0x6D646174);// mdat + .putInt(0x6D646174); // mdat if (is64) { buffer.putLong(refSize); @@ -649,7 +658,7 @@ public class Mp4FromDashWriter { return buffer.array(); } - private void make_mvhd(long longestTrack) throws IOException { + private void makeMvhd(final long longestTrack) throws IOException { auxWrite(new byte[]{ 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00 }); @@ -662,21 +671,22 @@ public class Mp4FromDashWriter { ); auxWrite(new byte[]{ - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values + 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 }); - auxWrite(new byte[24]);// predefined + auxWrite(new byte[24]); // predefined auxWrite(ByteBuffer.allocate(4) .putInt(tracks.length + 1) .array() ); } - private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException { + private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo, + final boolean is64) throws RuntimeException, IOException { int start = auxOffset(); auxWrite(new byte[]{ @@ -688,21 +698,21 @@ public class Mp4FromDashWriter { for (int i = 0; i < durations.length; i++) { durations[i] = (long) Math.ceil( - ((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE - ); + ((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhdTimeScale) + * DEFAULT_TIMESCALE); if (durations[i] > longestTrack) { longestTrack = durations[i]; } } - make_mvhd(longestTrack); + makeMvhd(longestTrack); for (int i = 0; i < tracks.length; i++) { if (tracks[i].trak.tkhd.matrix.length != 36) { throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i); } - make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); + makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); } // udta/meta/ilst/©too @@ -713,17 +723,18 @@ public class Mp4FromDashWriter { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 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 + 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65 // "NewPipe" binary string }); return lengthFor(start); } - private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException { + private void makeTrak(final int index, final long duration, final int defaultMediaTime, + final TablesInfo tables, final boolean is64) throws IOException { int start = auxOffset(); auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header + 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B, // trak header 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header }); @@ -747,20 +758,20 @@ public class Mp4FromDashWriter { ); auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header - 0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header + 0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73, // edts header + 0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // elst header }); int bMediaRate; int mediaTime; - if (tracks[index].trak.edst_elst == null) { + if (tracks[index].trak.edstElst == null) { // is a audio track ¿is edst/elst optional for audio tracks? - mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime + mediaTime = 0x00; // ffmpeg set this value as zero, instead of defaultMediaTime bMediaRate = 0x00010000; } else { - mediaTime = (int) tracks[index].trak.edst_elst.MediaTime; - bMediaRate = tracks[index].trak.edst_elst.bMediaRate; + mediaTime = (int) tracks[index].trak.edstElst.mediaTime; + bMediaRate = tracks[index].trak.edstElst.bMediaRate; } auxWrite(ByteBuffer @@ -771,32 +782,33 @@ public class Mp4FromDashWriter { .array() ); - make_mdia(tracks[index].trak.mdia, tables, is64, tracks[index].kind == TrackKind.Audio); + makeMdia(tracks[index].trak.mdia, tables, is64, tracks[index].kind == TrackKind.Audio); lengthFor(start); } - private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64, boolean isAudio) throws IOException { - int start_mdia = auxOffset(); - auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia + private void makeMdia(final Mdia mdia, final TablesInfo tablesInfo, final boolean is64, + final boolean isAudio) throws IOException { + int startMdia = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61}); // mdia auxWrite(mdia.mdhd); - auxWrite(make_hdlr(mdia.hdlr)); + auxWrite(makeHdlr(mdia.hdlr)); - int start_minf = auxOffset(); - auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf - auxWrite(mdia.minf.$mhd); + int startMinf = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66}); // minf + auxWrite(mdia.minf.mhd); auxWrite(mdia.minf.dinf); - int start_stbl = auxOffset(); - auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl - auxWrite(mdia.minf.stbl_stsd); + int startStbl = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C}); // stbl + auxWrite(mdia.minf.stblStsd); // // In audio tracks the following tables is not required: ssts ctts // And stsz can be empty if has a default sample size // if (moovSimulation) { - make(0x73747473, -1, 2, 1);// stts + make(0x73747473, -1, 2, 1); // stts if (tablesInfo.stss > 0) { make(0x73747373, -1, 1, tablesInfo.stss); } @@ -804,7 +816,7 @@ public class Mp4FromDashWriter { make(0x63747473, -1, 2, tablesInfo.ctts); } make(0x73747363, -1, 3, tablesInfo.stsc); - make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz); + make(0x7374737A, tablesInfo.stszDefault, 1, tablesInfo.stsz); make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); } else { tablesInfo.stts = make(0x73747473, -1, 2, 1); @@ -815,23 +827,24 @@ public class Mp4FromDashWriter { tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts); } tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc); - tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz); - tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); + tablesInfo.stsz = make(0x7374737A, tablesInfo.stszDefault, 1, tablesInfo.stsz); + tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, + tablesInfo.stco); } if (isAudio) { - auxWrite(make_sgpd()); - tablesInfo.sbgp = make_sbgp();// during simulation the returned offset is ignored + auxWrite(makeSgpd()); + tablesInfo.sbgp = makeSbgp(); // during simulation the returned offset is ignored } - lengthFor(start_stbl); - lengthFor(start_minf); - lengthFor(start_mdia); + lengthFor(startStbl); + lengthFor(startMinf); + lengthFor(startMdia); } - private byte[] make_hdlr(Hdlr hdlr) { + private byte[] makeHdlr(final Hdlr hdlr) { ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ - 0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr + 0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72, // hdlr 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)." @@ -846,28 +859,28 @@ public class Mp4FromDashWriter { buffer.position(12); buffer.putInt(hdlr.type); buffer.putInt(hdlr.subType); - buffer.put(hdlr.bReserved);// always is a zero array + buffer.put(hdlr.bReserved); // always is a zero array return buffer.array(); } - private int make_sbgp() throws IOException { + private int makeSbgp() throws IOException { int offset = auxOffset(); auxWrite(new byte[] { - 0x00, 0x00, 0x00, 0x1C,// box size - 0x73, 0x62, 0x67, 0x70,// "sbpg" - 0x00, 0x00, 0x00, 0x00,// default box flags - 0x72, 0x6F, 0x6C, 0x6C,// group type "roll" - 0x00, 0x00, 0x00, 0x01,// group table size - 0x00, 0x00, 0x00, 0x00,// group[0] total samples (to be set later) - 0x00, 0x00, 0x00, 0x01// group[0] description index + 0x00, 0x00, 0x00, 0x1C, // box size + 0x73, 0x62, 0x67, 0x70, // "sbpg" + 0x00, 0x00, 0x00, 0x00, // default box flags + 0x72, 0x6F, 0x6C, 0x6C, // group type "roll" + 0x00, 0x00, 0x00, 0x01, // group table size + 0x00, 0x00, 0x00, 0x00, // group[0] total samples (to be set later) + 0x00, 0x00, 0x00, 0x01 // group[0] description index }); return offset + 0x14; } - private byte[] make_sgpd() { + private byte[] makeSgpd() { /* * Sample Group Description Box * @@ -882,26 +895,25 @@ public class Mp4FromDashWriter { */ ByteBuffer buffer = ByteBuffer.wrap(new byte[] { - 0x00, 0x00, 0x00, 0x1A,// box size - 0x73, 0x67, 0x70, 0x64,// "sgpd" - 0x01, 0x00, 0x00, 0x00,// box flags (unknown flag sets) + 0x00, 0x00, 0x00, 0x1A, // box size + 0x73, 0x67, 0x70, 0x64, // "sgpd" + 0x01, 0x00, 0x00, 0x00, // box flags (unknown flag sets) 0x72, 0x6F, 0x6C, 0x6C, // ¿¿group type?? - 0x00, 0x00, 0x00, 0x02,// ¿¿?? - 0x00, 0x00, 0x00, 0x01,// ¿¿?? - (byte)0xFF, (byte)0xFF// ¿¿?? + 0x00, 0x00, 0x00, 0x02, // ¿¿?? + 0x00, 0x00, 0x00, 0x01, // ¿¿?? + (byte) 0xFF, (byte) 0xFF // ¿¿?? }); return buffer.array(); } class TablesInfo { - int stts; int stsc; - int[] stsc_bEntries; + int[] stscBEntries; int ctts; int stsz; - int stsz_default; + int stszDefault; int stss; int stco; int sbgp; diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index 16bffea9a..ee0a61492 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,431 +1,428 @@ -package org.schabi.newpipe.streams; - -import androidx.annotation.NonNull; - -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 org.schabi.newpipe.streams.io.SharpStream; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import androidx.annotation.Nullable; - -/** - * @author kapodamy - */ -public class OggFromWebMWriter implements Closeable { - - private static final byte FLAG_UNSET = 0x00; - //private static final byte FLAG_CONTINUED = 0x01; - private static final byte FLAG_FIRST = 0x02; - private static final byte FLAG_LAST = 0x04; - - private final static byte HEADER_CHECKSUM_OFFSET = 22; - private final static byte HEADER_SIZE = 27; - - private final static int TIME_SCALE_NS = 1000000000; - - private boolean done = false; - private boolean parsed = false; - - private SharpStream source; - private SharpStream output; - - private int sequence_count = 0; - private final int STREAM_ID; - private byte packet_flag = FLAG_FIRST; - - private WebMReader webm = null; - private WebMTrack webm_track = null; - private Segment webm_segment = null; - private Cluster webm_cluster = null; - private SimpleBlock webm_block = null; - - private long webm_block_last_timecode = 0; - private long webm_block_near_duration = 0; - - private short segment_table_size = 0; - private final byte[] segment_table = new byte[255]; - private long segment_table_next_timestamp = TIME_SCALE_NS; - - private final int[] crc32_table = new int[256]; - - public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) { - if (!source.canRead() || !source.canRewind()) { - throw new IllegalArgumentException("source stream must be readable and allows seeking"); - } - if (!target.canWrite() || !target.canRewind()) { - throw new IllegalArgumentException("output stream must be writable and allows seeking"); - } - - this.source = source; - this.output = target; - - this.STREAM_ID = (int) System.currentTimeMillis(); - - populate_crc32_table(); - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - public WebMTrack[] getTracksFromSource() throws IllegalStateException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - - return webm.getAvailableTracks(); - } - - public void parseSource() throws IOException, IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (parsed) { - throw new IllegalStateException("already parsed"); - } - - try { - webm = new WebMReader(source); - webm.parse(); - webm_segment = webm.getNextSegment(); - } finally { - parsed = true; - } - } - - public void selectTrack(int trackIndex) throws IOException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - if (done) { - throw new IOException("already done"); - } - if (webm_track != null) { - throw new IOException("tracks already selected"); - } - - switch (webm.getAvailableTracks()[trackIndex].kind) { - case Audio: - case Video: - break; - default: - throw new UnsupportedOperationException("the track must an audio or video stream"); - } - - try { - webm_track = webm.selectTrack(trackIndex); - } finally { - parsed = true; - } - } - - @Override - public void close() throws IOException { - done = true; - parsed = true; - - webm_track = null; - webm = null; - - if (!output.isClosed()) { - output.flush(); - } - - source.close(); - output.close(); - } - - public void build() throws IOException { - float resolution; - SimpleBlock bloq; - ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); - ByteBuffer page = ByteBuffer.allocate(64 * 1024); - - header.order(ByteOrder.LITTLE_ENDIAN); - - /* step 1: get the amount of frames per seconds */ - switch (webm_track.kind) { - case Audio: - resolution = getSampleFrequencyFromTrack(webm_track.bMetadata); - if (resolution == 0f) { - throw new RuntimeException("cannot get the audio sample rate"); - } - break; - case Video: - // WARNING: untested - if (webm_track.defaultDuration == 0) { - throw new RuntimeException("missing default frame time"); - } - resolution = 1000f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale); - break; - default: - throw new RuntimeException("not implemented"); - } - - /* step 2: create packet with code init data */ - if (webm_track.codecPrivate != null) { - addPacketSegment(webm_track.codecPrivate.length); - make_packetHeader(0x00, header, webm_track.codecPrivate); - write(header); - output.write(webm_track.codecPrivate); - } - - /* step 3: create packet with metadata */ - byte[] buffer = make_metadata(); - if (buffer != null) { - addPacketSegment(buffer.length); - make_packetHeader(0x00, header, buffer); - write(header); - output.write(buffer); - } - - /* step 4: calculate amount of packets */ - while (webm_segment != null) { - bloq = getNextBlock(); - - if (bloq != null && addPacketSegment(bloq)) { - int pos = page.position(); - //noinspection ResultOfMethodCallIgnored - bloq.data.read(page.array(), pos, bloq.dataSize); - page.position(pos + bloq.dataSize); - continue; - } - - // calculate the current packet duration using the next block - double elapsed_ns = webm_track.codecDelay; - - if (bloq == null) { - packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed - elapsed_ns += webm_block_last_timecode; - - if (webm_track.defaultDuration > 0) { - elapsed_ns += webm_track.defaultDuration; - } else { - // hardcoded way, guess the sample duration - elapsed_ns += webm_block_near_duration; - } - } else { - elapsed_ns += bloq.absoluteTimeCodeNs; - } - - // get the sample count in the page - elapsed_ns = elapsed_ns / TIME_SCALE_NS; - elapsed_ns = Math.ceil(elapsed_ns * resolution); - - // create header and calculate page checksum - int checksum = make_packetHeader((long) elapsed_ns, header, null); - checksum = calc_crc32(checksum, page.array(), page.position()); - - header.putInt(HEADER_CHECKSUM_OFFSET, checksum); - - // dump data - write(header); - write(page); - - webm_block = bloq; - } - } - - private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) { - short length = HEADER_SIZE; - - buffer.putInt(0x5367674f);// "OggS" binary string in little-endian - buffer.put((byte) 0x00);// version - buffer.put(packet_flag);// type - - buffer.putLong(gran_pos);// granulate position - - buffer.putInt(STREAM_ID);// bitstream serial number - buffer.putInt(sequence_count++);// page sequence number - - buffer.putInt(0x00);// page checksum - - buffer.put((byte) segment_table_size);// segment table - buffer.put(segment_table, 0, segment_table_size);// segment size - - length += segment_table_size; - - clearSegmentTable();// clear segment table for next header - - int checksum_crc32 = calc_crc32(0x00, buffer.array(), length); - - if (immediate_page != null) { - checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length); - buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32); - segment_table_next_timestamp -= TIME_SCALE_NS; - } - - return checksum_crc32; - } - - @Nullable - private byte[] make_metadata() { - if ("A_OPUS".equals(webm_track.codecId)) { - return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string - 0x07, 0x00, 0x00, 0x00,// writting application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string - 0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags) - }; - } else if ("A_VORBIS".equals(webm_track.codecId)) { - return new byte[]{ - 0x03,// ???????? - 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string - 0x07, 0x00, 0x00, 0x00,// writting application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags) - - /* - // whole file duration (not implemented) - 0x44,// tag string size - 0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, - 0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 - */ - 0x0F,// tag string size - 0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ???????? - }; - } - - // not implemented for the desired codec - return null; - } - - private void write(ByteBuffer buffer) throws IOException { - output.write(buffer.array(), 0, buffer.position()); - buffer.position(0); - } - - - - @Nullable - private SimpleBlock getNextBlock() throws IOException { - SimpleBlock res; - - if (webm_block != null) { - res = webm_block; - webm_block = null; - return res; - } - - if (webm_segment == null) { - webm_segment = webm.getNextSegment(); - if (webm_segment == null) { - return null;// no more blocks in the selected track - } - } - - if (webm_cluster == null) { - webm_cluster = webm_segment.getNextCluster(); - if (webm_cluster == null) { - webm_segment = null; - return getNextBlock(); - } - } - - res = webm_cluster.getNextSimpleBlock(); - if (res == null) { - webm_cluster = null; - return getNextBlock(); - } - - webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode; - webm_block_last_timecode = res.absoluteTimeCodeNs; - - return res; - } - - private float getSampleFrequencyFromTrack(byte[] bMetadata) { - // hardcoded way - ByteBuffer buffer = ByteBuffer.wrap(bMetadata); - - while (buffer.remaining() >= 6) { - int id = buffer.getShort() & 0xFFFF; - if (id == 0x0000B584) { - return buffer.getFloat(); - } - } - - return 0f; - } - - private void clearSegmentTable() { - segment_table_next_timestamp += TIME_SCALE_NS; - packet_flag = FLAG_UNSET; - segment_table_size = 0; - } - - private boolean addPacketSegment(SimpleBlock block) { - long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay; - - if (timestamp >= segment_table_next_timestamp) { - return false; - } - - return addPacketSegment(block.dataSize); - } - - private boolean addPacketSegment(int size) { - if (size > 65025) { - throw new UnsupportedOperationException("page size cannot be larger than 65025"); - } - - int available = (segment_table.length - segment_table_size) * 255; - boolean extra = (size % 255) == 0; - - if (extra) { - // add a zero byte entry in the table - // required to indicate the sample size is multiple of 255 - available -= 255; - } - - // check if possible add the segment, without overflow the table - if (available < size) { - return false;// not enough space on the page - } - - for (; size > 0; size -= 255) { - segment_table[segment_table_size++] = (byte) Math.min(size, 255); - } - - if (extra) { - segment_table[segment_table_size++] = 0x00; - } - - return true; - } - - private void populate_crc32_table() { - for (int i = 0; i < 0x100; i++) { - int crc = i << 24; - for (int j = 0; j < 8; j++) { - long b = crc >>> 31; - crc <<= 1; - crc ^= (int) (0x100000000L - b) & 0x04c11db7; - } - crc32_table[i] = crc; - } - } - - private int calc_crc32(int initial_crc, byte[] buffer, int size) { - for (int i = 0; i < size; i++) { - int reg = (initial_crc >>> 24) & 0xff; - initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)]; - } - - return initial_crc; - } - -} +package org.schabi.newpipe.streams; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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 org.schabi.newpipe.streams.io.SharpStream; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @author kapodamy + */ +public class OggFromWebMWriter implements Closeable { + private static final byte FLAG_UNSET = 0x00; + //private static final byte FLAG_CONTINUED = 0x01; + private static final byte FLAG_FIRST = 0x02; + private static final byte FLAG_LAST = 0x04; + + private static final byte HEADER_CHECKSUM_OFFSET = 22; + private static final byte HEADER_SIZE = 27; + + private static final int TIME_SCALE_NS = 1000000000; + + private boolean done = false; + private boolean parsed = false; + + private SharpStream source; + private SharpStream output; + + private int sequenceCount = 0; + private final int streamId; + private byte packetFlag = FLAG_FIRST; + + private WebMReader webm = null; + private WebMTrack webmTrack = null; + private Segment webmSegment = null; + private Cluster webmCluster = null; + private SimpleBlock webmBlock = null; + + private long webmBlockLastTimecode = 0; + private long webmBlockNearDuration = 0; + + private short segmentTableSize = 0; + private final byte[] segmentTable = new byte[255]; + private long segmentTableNextTimestamp = TIME_SCALE_NS; + + private final int[] crc32Table = new int[256]; + + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { + if (!source.canRead() || !source.canRewind()) { + throw new IllegalArgumentException("source stream must be readable and allows seeking"); + } + if (!target.canWrite() || !target.canRewind()) { + throw new IllegalArgumentException("output stream must be writable and allows seeking"); + } + + this.source = source; + this.output = target; + + this.streamId = (int) System.currentTimeMillis(); + + populateCrc32Table(); + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public WebMTrack[] getTracksFromSource() throws IllegalStateException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + + return webm.getAvailableTracks(); + } + + public void parseSource() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + webm = new WebMReader(source); + webm.parse(); + webmSegment = webm.getNextSegment(); + } finally { + parsed = true; + } + } + + public void selectTrack(final int trackIndex) throws IOException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + if (done) { + throw new IOException("already done"); + } + if (webmTrack != null) { + throw new IOException("tracks already selected"); + } + + switch (webm.getAvailableTracks()[trackIndex].kind) { + case Audio: + case Video: + break; + default: + throw new UnsupportedOperationException("the track must an audio or video stream"); + } + + try { + webmTrack = webm.selectTrack(trackIndex); + } finally { + parsed = true; + } + } + + @Override + public void close() throws IOException { + done = true; + parsed = true; + + webmTrack = null; + webm = null; + + if (!output.isClosed()) { + output.flush(); + } + + source.close(); + output.close(); + } + + public void build() throws IOException { + float resolution; + SimpleBlock bloq; + ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); + ByteBuffer page = ByteBuffer.allocate(64 * 1024); + + header.order(ByteOrder.LITTLE_ENDIAN); + + /* step 1: get the amount of frames per seconds */ + switch (webmTrack.kind) { + case Audio: + resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); + if (resolution == 0f) { + throw new RuntimeException("cannot get the audio sample rate"); + } + break; + case Video: + // WARNING: untested + if (webmTrack.defaultDuration == 0) { + throw new RuntimeException("missing default frame time"); + } + resolution = 1000f / ((float) webmTrack.defaultDuration + / webmSegment.info.timecodeScale); + break; + default: + throw new RuntimeException("not implemented"); + } + + /* step 2: create packet with code init data */ + if (webmTrack.codecPrivate != null) { + addPacketSegment(webmTrack.codecPrivate.length); + makePacketheader(0x00, header, webmTrack.codecPrivate); + write(header); + output.write(webmTrack.codecPrivate); + } + + /* step 3: create packet with metadata */ + byte[] buffer = makeMetadata(); + if (buffer != null) { + addPacketSegment(buffer.length); + makePacketheader(0x00, header, buffer); + write(header); + output.write(buffer); + } + + /* step 4: calculate amount of packets */ + while (webmSegment != null) { + bloq = getNextBlock(); + + if (bloq != null && addPacketSegment(bloq)) { + int pos = page.position(); + //noinspection ResultOfMethodCallIgnored + bloq.data.read(page.array(), pos, bloq.dataSize); + page.position(pos + bloq.dataSize); + continue; + } + + // calculate the current packet duration using the next block + double elapsedNs = webmTrack.codecDelay; + + if (bloq == null) { + packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed + elapsedNs += webmBlockLastTimecode; + + if (webmTrack.defaultDuration > 0) { + elapsedNs += webmTrack.defaultDuration; + } else { + // hardcoded way, guess the sample duration + elapsedNs += webmBlockNearDuration; + } + } else { + elapsedNs += bloq.absoluteTimeCodeNs; + } + + // get the sample count in the page + elapsedNs = elapsedNs / TIME_SCALE_NS; + elapsedNs = Math.ceil(elapsedNs * resolution); + + // create header and calculate page checksum + int checksum = makePacketheader((long) elapsedNs, header, null); + checksum = calcCrc32(checksum, page.array(), page.position()); + + header.putInt(HEADER_CHECKSUM_OFFSET, checksum); + + // dump data + write(header); + write(page); + + webmBlock = bloq; + } + } + + private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, + final byte[] immediatePage) { + short length = HEADER_SIZE; + + buffer.putInt(0x5367674f); // "OggS" binary string in little-endian + buffer.put((byte) 0x00); // version + buffer.put(packetFlag); // type + + buffer.putLong(granPos); // granulate position + + buffer.putInt(streamId); // bitstream serial number + buffer.putInt(sequenceCount++); // page sequence number + + buffer.putInt(0x00); // page checksum + + buffer.put((byte) segmentTableSize); // segment table + buffer.put(segmentTable, 0, segmentTableSize); // segment size + + length += segmentTableSize; + + clearSegmentTable(); // clear segment table for next header + + int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); + + if (immediatePage != null) { + checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); + buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); + segmentTableNextTimestamp -= TIME_SCALE_NS; + } + + return checksumCrc32; + } + + @Nullable + private byte[] makeMetadata() { + if ("A_OPUS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x07, 0x00, 0x00, 0x00, // writing application string size + 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } else if ("A_VORBIS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x03, // ???????? + 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string + 0x07, 0x00, 0x00, 0x00, // writting application string size + 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + 0x01, 0x00, 0x00, 0x00, // additional tags count (zero means no tags) + + /* + // whole file duration (not implemented) + 0x44,// tag string size + 0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, + 0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 + */ + 0x0F, // tag string size + 0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D, // "ENCODER=" binary string + 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ???????? + }; + } + + // not implemented for the desired codec + return null; + } + + private void write(final ByteBuffer buffer) throws IOException { + output.write(buffer.array(), 0, buffer.position()); + buffer.position(0); + } + + @Nullable + private SimpleBlock getNextBlock() throws IOException { + SimpleBlock res; + + if (webmBlock != null) { + res = webmBlock; + webmBlock = null; + return res; + } + + if (webmSegment == null) { + webmSegment = webm.getNextSegment(); + if (webmSegment == null) { + return null; // no more blocks in the selected track + } + } + + if (webmCluster == null) { + webmCluster = webmSegment.getNextCluster(); + if (webmCluster == null) { + webmSegment = null; + return getNextBlock(); + } + } + + res = webmCluster.getNextSimpleBlock(); + if (res == null) { + webmCluster = null; + return getNextBlock(); + } + + webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; + webmBlockLastTimecode = res.absoluteTimeCodeNs; + + return res; + } + + private float getSampleFrequencyFromTrack(final byte[] bMetadata) { + // hardcoded way + ByteBuffer buffer = ByteBuffer.wrap(bMetadata); + + while (buffer.remaining() >= 6) { + int id = buffer.getShort() & 0xFFFF; + if (id == 0x0000B584) { + return buffer.getFloat(); + } + } + + return 0f; + } + + private void clearSegmentTable() { + segmentTableNextTimestamp += TIME_SCALE_NS; + packetFlag = FLAG_UNSET; + segmentTableSize = 0; + } + + private boolean addPacketSegment(final SimpleBlock block) { + long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; + + if (timestamp >= segmentTableNextTimestamp) { + return false; + } + + return addPacketSegment(block.dataSize); + } + + private boolean addPacketSegment(int size) { + if (size > 65025) { + throw new UnsupportedOperationException("page size cannot be larger than 65025"); + } + + int available = (segmentTable.length - segmentTableSize) * 255; + boolean extra = (size % 255) == 0; + + if (extra) { + // add a zero byte entry in the table + // required to indicate the sample size is multiple of 255 + available -= 255; + } + + // check if possible add the segment, without overflow the table + if (available < size) { + return false; // not enough space on the page + } + + for (; size > 0; size -= 255) { + segmentTable[segmentTableSize++] = (byte) Math.min(size, 255); + } + + if (extra) { + segmentTable[segmentTableSize++] = 0x00; + } + + return true; + } + + private void populateCrc32Table() { + for (int i = 0; i < 0x100; i++) { + int crc = i << 24; + for (int j = 0; j < 8; j++) { + long b = crc >>> 31; + crc <<= 1; + crc ^= (int) (0x100000000L - b) & 0x04c11db7; + } + crc32Table[i] = crc; + } + } + + private int calcCrc32(int initialCrc, final byte[] buffer, final int size) { + for (int i = 0; i < size; i++) { + int reg = (initialCrc >>> 24) & 0xff; + initialCrc = (initialCrc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; + } + + return initialCrc; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java index 6f1cceeed..eddb951e5 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java @@ -26,18 +26,19 @@ public class SrtFromTtmlWriter { private int frameIndex = 0; - public SrtFromTtmlWriter(SharpStream out, boolean ignoreEmptyFrames) { + public SrtFromTtmlWriter(final SharpStream out, final boolean ignoreEmptyFrames) { this.out = out; this.ignoreEmptyFrames = ignoreEmptyFrames; } - private static String getTimestamp(Element frame, String attr) { + private static String getTimestamp(final Element frame, final String attr) { return frame .attr(attr) - .replace('.', ',');// SRT subtitles uses comma as decimal separator + .replace('.', ','); // SRT subtitles uses comma as decimal separator } - private void writeFrame(String begin, String end, StringBuilder text) throws IOException { + private void writeFrame(final String begin, final String end, final StringBuilder text) + throws IOException { writeString(String.valueOf(frameIndex++)); writeString(NEW_LINE); writeString(begin); @@ -49,11 +50,11 @@ public class SrtFromTtmlWriter { writeString(NEW_LINE); } - private void writeString(String text) throws IOException { + private void writeString(final String text) throws IOException { out.write(text.getBytes(charset)); } - public void build(SharpStream ttml) throws IOException { + public void build(final SharpStream ttml) throws IOException { /* * TTML parser with BASIC support * multiple CUE is not supported @@ -66,25 +67,32 @@ public class SrtFromTtmlWriter { // parse XML byte[] buffer = new byte[(int) ttml.available()]; ttml.read(buffer); - Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", Parser.xmlParser()); + Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", + Parser.xmlParser()); StringBuilder text = new StringBuilder(128); - Elements paragraph_list = doc.select("body > div > p"); + Elements paragraphList = doc.select("body > div > p"); // check if has frames - if (paragraph_list.size() < 1) return; + if (paragraphList.size() < 1) { + return; + } - for (Element paragraph : paragraph_list) { + for (Element paragraph : paragraphList) { text.setLength(0); for (Node children : paragraph.childNodes()) { - if (children instanceof TextNode) + if (children instanceof TextNode) { text.append(((TextNode) children).text()); - else if (children instanceof Element && ((Element) children).tagName().equalsIgnoreCase("br")) + } else if (children instanceof Element + && ((Element) children).tagName().equalsIgnoreCase("br")) { text.append(NEW_LINE); + } } - if (ignoreEmptyFrames && text.length() < 1) continue; + if (ignoreEmptyFrames && text.length() < 1) { + continue; + } String begin = getTimestamp(paragraph, "begin"); String end = getTimestamp(paragraph, "end"); diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java index b1628d954..56cea9f2d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -1,546 +1,538 @@ -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 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_CodecDelay = 0x16AA; - private final static int ID_SeekPreRoll = 0x16BB; - - private final static int ID_Cluster = 0x0F43B675; - private final static int ID_Timecode = 0x67; - private final static int ID_SimpleBlock = 0x23; - private final static int ID_Block = 0x21; - private final static int ID_GroupBlock = 0x20; - - - 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); - // WARNING: track cannot be the same or have different index in new segments - 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), StandardCharsets.UTF_8);// or use "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(); - if (expected.length < 1) { - return elem; - } - 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)) != 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; - case ID_CodecDelay: - entry.codecDelay = readNumber(elem); - break; - case ID_SeekPreRoll: - entry.seekPreRoll = readNumber(elem); - break; - default: - 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.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(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 = -1; - public long codecDelay = -1; - public long seekPreRoll = -1; - } - - 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 InputStream data; - public boolean createdFromBlock; - - SimpleBlock(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(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_SimpleBlock, ID_GroupBlock); - if (elem == null) { - return null; - } - - if (elem.type == ID_GroupBlock) { - 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((int) currentSimpleBlock.dataSize); - - // calculate the timestamp in nanoseconds - currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode; - currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale; - - return currentSimpleBlock; - } - - ensure(elem); - } - - return null; - } - - } - -} +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 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 + 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) { + 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 { + 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(final 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(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 (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 { + 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; + 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 { + 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(final Element ref, final int lacingExpected) throws IOException { + ArrayList trackEntries = new ArrayList<>(2); + Element elemTrackEntry; + + while ((elemTrackEntry = untilElement(ref, ID_TRACK_ENTRY)) != null) { + 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); + } + + 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(final Element ref) throws IOException { + 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 { + 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 = -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); + + Element elem = untilElement(segment.ref, ID_CLUSTER); + if (elem == null) { + return null; + } + + segment.currentCluster = elem; + + return readCluster(segment.currentCluster); + } + } + + public 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((int) currentSimpleBlock.dataSize); + + // calculate the timestamp in nanoseconds + currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + + this.timecode; + currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale; + + 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 index 39db33ad0..da1e119c3 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -1,757 +1,766 @@ -package org.schabi.newpipe.streams; - -import androidx.annotation.NonNull; - -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 org.schabi.newpipe.streams.io.SharpStream; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; - -/** - * @author kapodamy - */ -public class WebMWriter implements Closeable { - - 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;// 5000ms on 1000000us timecode scale - private final static byte CLUSTER_HEADER_SIZE = 8; - private final static int CUE_RESERVE_SIZE = 65535; - private final static byte MINIMUM_EBML_VOID_SIZE = 4; - - 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[] readersCluster; - - private ArrayList clustersOffsetsSizes; - - private byte[] outBuffer; - private ByteBuffer outByteBuffer; - - public WebMWriter(SharpStream... source) { - sourceTracks = source; - readers = new WebMReader[sourceTracks.length]; - infoTracks = new WebMTrack[sourceTracks.length]; - outBuffer = new byte[BUFFER_SIZE]; - outByteBuffer = ByteBuffer.wrap(outBuffer); - clustersOffsetsSizes = new ArrayList<>(256); - } - - 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]; - readersCluster = new Cluster[readers.length]; - - for (int i = 0; i < readers.length; i++) { - infoTracks[i] = readers[i].selectTrack(trackIndex[i]); - readersSegment[i] = readers[i].getNextSegment(); - } - } finally { - parsed = true; - } - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - @Override - public void close() { - done = true; - parsed = true; - - for (SharpStream src : sourceTracks) { - src.close(); - } - - sourceTracks = null; - readers = null; - infoTracks = null; - readersSegment = null; - readersCluster = null; - outBuffer = null; - outByteBuffer = null; - clustersOffsetsSizes = 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 segmentOffset = 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()); - - dump(listBuffer, out); - - // reserve space for Cues element - long cueOffset = written; - make_EBML_void(out, CUE_RESERVE_SIZE, true); - - int[] defaultSampleDuration = new int[infoTracks.length]; - long[] duration = new long[infoTracks.length]; - - for (int i = 0; i < infoTracks.length; i++) { - if (infoTracks[i].defaultDuration < 0) { - defaultSampleDuration[i] = -1;// not available - } else { - defaultSampleDuration[i] = (int) Math.ceil(infoTracks[i].defaultDuration / (float) DEFAULT_TIMECODE_SCALE); - } - duration[i] = -1; - } - - // Select a track for the cue - int cuesForTrackId = selectTrackForCue(); - long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; - ArrayList keyFrames = new ArrayList<>(32); - - int firstClusterOffset = (int) written; - long currentClusterOffset = makeCluster(out, 0, 0, true); - - 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; - currentClusterOffset = makeCluster(out, baseTimecode, currentClusterOffset, true); - } - - 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(segmentOffset, currentClusterOffset, written, bloq.absoluteTimecode)); - } - } - - writeBlock(out, bloq, baseTimecode); - blockWritten++; - - if (defaultSampleDuration[i] < 0 && duration[i] >= 0) { - // if the sample duration in unknown, calculate using current_duration - previous_duration - defaultSampleDuration[i] = (int) (bloq.absoluteTimecode - duration[i]); - } - duration[i] = bloq.absoluteTimecode; - - if (limitTimecode < 0) { - limitTimecode = bloq.absoluteTimecode + INTERV; - continue; - } - - if (bloq.absoluteTimecode >= limitTimecode) { - if (limitTimecodeByTrackId != i) { - limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode); - } - i++; - } - } - } - - makeCluster(out, -1, currentClusterOffset, false); - - long segmentSize = written - offsetSegmentSizeSet - 7; - - /* Segment size */ - seekTo(out, offsetSegmentSizeSet); - outByteBuffer.putLong(0, segmentSize); - out.write(outBuffer, 1, DataReader.LONG_SIZE - 1); - - /* Segment duration */ - long longestDuration = 0; - for (int i = 0; i < duration.length; i++) { - if (defaultSampleDuration[i] > 0) { - duration[i] += defaultSampleDuration[i]; - } - if (duration[i] > longestDuration) { - longestDuration = duration[i]; - } - } - seekTo(out, offsetInfoDurationSet); - outByteBuffer.putFloat(0, longestDuration); - dump(outBuffer, DataReader.FLOAT_SIZE, out); - - /* first Cluster offset */ - firstClusterOffset -= segmentOffset; - writeInt(out, offsetClusterSet, firstClusterOffset); - - seekTo(out, cueOffset); - - /* Cue */ - short cueSize = 0; - dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);// header size is 7 - - for (KeyFrame keyFrame : keyFrames) { - int size = makeCuePoint(cuesForTrackId, keyFrame, outBuffer); - - if ((cueSize + size + 7 + MINIMUM_EBML_VOID_SIZE) > CUE_RESERVE_SIZE) { - break;// no space left - } - - cueSize += size; - dump(outBuffer, size, out); - } - - make_EBML_void(out, CUE_RESERVE_SIZE - cueSize - 7, false); - - seekTo(out, cueOffset + 5); - outByteBuffer.putShort(0, cueSize); - dump(outBuffer, DataReader.SHORT_SIZE, out); - - /* seek head, seek for cues element */ - writeInt(out, offsetCuesSet, (int) (cueOffset - segmentOffset)); - - for (ClusterInfo cluster : clustersOffsetsSizes) { - writeInt(out, cluster.offset, cluster.size | 0x10000000); - } - } - - 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 (readersCluster[internalTrackId] == null) { - readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); - if (readersCluster[internalTrackId] == null) { - readersSegment[internalTrackId] = null; - return getNextBlockFrom(internalTrackId); - } - } - - SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); - if (res == null) { - readersCluster[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 = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE; - - return bloq; - } - - private void seekTo(SharpStream stream, long offset) throws IOException { - if (stream.canSeek()) { - stream.seek(offset); - } else { - if (offset > written) { - stream.skip(offset - written); - } else { - stream.rewind(); - stream.skip(offset); - } - } - - written = offset; - } - - private void writeInt(SharpStream stream, long offset, int number) throws IOException { - seekTo(stream, offset); - outByteBuffer.putInt(0, number); - dump(outBuffer, DataReader.INTEGER_SIZE, 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)); - - dump(listBuffer, stream); - - int read; - while ((read = bloq.data.read(outBuffer)) > 0) { - dump(outBuffer, read, stream); - } - } - - private long makeCluster(SharpStream stream, long timecode, long offset, boolean create) throws IOException { - ClusterInfo cluster; - - if (offset > 0) { - // save the size of the previous cluster (maximum 256 MiB) - cluster = clustersOffsetsSizes.get(clustersOffsetsSizes.size() - 1); - cluster.size = (int) (written - offset - CLUSTER_HEADER_SIZE); - } - - offset = written; - - if (create) { - /* cluster */ - dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); - - cluster = new ClusterInfo(); - cluster.offset = written; - clustersOffsetsSizes.add(cluster); - - dump(new byte[]{ - 0x10, 0x00, 0x00, 0x00, - /* timestamp */ - (byte) 0xe7 - }, stream); - - dump(encode(timecode, true), stream); - } - - return offset; - } - - 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)); - - /* codec delay*/ - if (track.codecDelay >= 0) { - buffer.add(new byte[]{0x56, (byte) 0xAA}); - buffer.add(encode(track.codecDelay, true)); - } - - /* codec seek pre-roll*/ - if (track.seekPreRoll >= 0) { - buffer.add(new byte[]{0x56, (byte) 0xBB}); - buffer.add(encode(track.seekPreRoll, true)); - } - - /* type */ - buffer.add(new byte[]{(byte) 0x83}); - buffer.add(encode(track.trackType, true)); - - /* default duration */ - if (track.defaultDuration >= 0) { - 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 int makeCuePoint(int internalTrackId, KeyFrame keyFrame, byte[] buffer) { - ArrayList cue = new ArrayList<>(5); - - /* CuePoint */ - cue.add(new byte[]{(byte) 0xbb}); - cue.add(null); - - /* CueTime */ - cue.add(new byte[]{(byte) 0xb3}); - cue.add(encode(keyFrame.duration, true)); - - /* CueTrackPosition */ - cue.addAll(makeCueTrackPosition(internalTrackId, keyFrame)); - - int size = 0; - lengthFor(cue); - - for (byte[] buff : cue) { - System.arraycopy(buff, 0, buffer, size, buff.length); - size += buff.length; - } - - return size; - } - - 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.clusterPosition, true)); - - /* CueRelativePosition */ - if (keyFrame.relativePosition > 0) { - buffer.add(new byte[]{(byte) 0xf0}); - buffer.add(encode(keyFrame.relativePosition, true)); - } - - return lengthFor(buffer); - } - - private void make_EBML_void(SharpStream out, int size, boolean wipe) throws IOException { - /* ebml void */ - outByteBuffer.putShort(0, (short) 0xec20); - outByteBuffer.putShort(2, (short) (size - 4)); - - dump(outBuffer, 4, out); - - if (wipe) { - size -= 4; - while (size > 0) { - int write = Math.min(size, outBuffer.length); - dump(outBuffer, write, out); - size -= write; - } - } - } - - private void dump(byte[] buffer, SharpStream stream) throws IOException { - dump(buffer, buffer.length, stream); - } - - private void dump(byte[] buffer, int count, SharpStream stream) throws IOException { - stream.write(buffer, 0, count); - written += count; - } - - private void dump(ArrayList buffers, SharpStream stream) throws IOException { - for (byte[] buffer : buffers) { - 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 - 1f) / 8f); - - int shift = 0; - for (int i = length - 1; i >= 0; i--, shift += 8) { - long b = number >>> shift; - 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; - str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8" - - 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, long timecode) { - clusterPosition = cluster - segment; - relativePosition = (int) (block - cluster - CLUSTER_HEADER_SIZE); - duration = timecode; - } - - final long clusterPosition; - final int relativePosition; - final long duration; - } - - class Block { - - InputStream data; - int trackNumber; - byte flags; - int dataSize; - long absoluteTimecode; - - boolean isKeyframe() { - return (flags & 0x80) == 0x80; - } - - @NonNull - @Override - public String toString() { - return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode); - } - } - - class ClusterInfo { - - long offset; - int size; - } - -} +package org.schabi.newpipe.streams; + +import androidx.annotation.NonNull; + +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 org.schabi.newpipe.streams.io.SharpStream; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +/** + * @author kapodamy + */ +public class WebMWriter implements Closeable { + private static final int BUFFER_SIZE = 8 * 1024; + private static final int DEFAULT_TIMECODE_SCALE = 1000000; + private static final int INTERV = 100; // 100ms on 1000000us timecode scale + private static final int DEFAULT_CUES_EACH_MS = 5000; // 5000ms on 1000000us timecode scale + private static final byte CLUSTER_HEADER_SIZE = 8; + private static final int CUE_RESERVE_SIZE = 65535; + private static final byte MINIMUM_EBML_VOID_SIZE = 4; + + 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[] readersCluster; + + private ArrayList clustersOffsetsSizes; + + private byte[] outBuffer; + private ByteBuffer outByteBuffer; + + public WebMWriter(final SharpStream... source) { + sourceTracks = source; + readers = new WebMReader[sourceTracks.length]; + infoTracks = new WebMTrack[sourceTracks.length]; + outBuffer = new byte[BUFFER_SIZE]; + outByteBuffer = ByteBuffer.wrap(outBuffer); + clustersOffsetsSizes = new ArrayList<>(256); + } + + public WebMTrack[] getTracksFromSource(final 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(final int... trackIndex) throws IOException { + try { + readersSegment = new Segment[readers.length]; + readersCluster = new Cluster[readers.length]; + + for (int i = 0; i < readers.length; i++) { + infoTracks[i] = readers[i].selectTrack(trackIndex[i]); + readersSegment[i] = readers[i].getNextSegment(); + } + } finally { + parsed = true; + } + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + @Override + public void close() { + done = true; + parsed = true; + + for (SharpStream src : sourceTracks) { + src.close(); + } + + sourceTracks = null; + readers = null; + infoTracks = null; + readersSegment = null; + readersCluster = null; + outBuffer = null; + outByteBuffer = null; + clustersOffsetsSizes = null; + } + + public void build(final 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 segmentOffset = 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()); + + dump(listBuffer, out); + + // reserve space for Cues element + long cueOffset = written; + makeEbmlVoid(out, CUE_RESERVE_SIZE, true); + + int[] defaultSampleDuration = new int[infoTracks.length]; + long[] duration = new long[infoTracks.length]; + + for (int i = 0; i < infoTracks.length; i++) { + if (infoTracks[i].defaultDuration < 0) { + defaultSampleDuration[i] = -1; // not available + } else { + defaultSampleDuration[i] = (int) Math.ceil(infoTracks[i].defaultDuration + / (float) DEFAULT_TIMECODE_SCALE); + } + duration[i] = -1; + } + + // Select a track for the cue + int cuesForTrackId = selectTrackForCue(); + long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; + ArrayList keyFrames = new ArrayList<>(32); + + int firstClusterOffset = (int) written; + long currentClusterOffset = makeCluster(out, 0, 0, true); + + 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; + currentClusterOffset = makeCluster(out, baseTimecode, currentClusterOffset, + true); + } + + 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(segmentOffset, currentClusterOffset, written, + bloq.absoluteTimecode)); + } + } + + writeBlock(out, bloq, baseTimecode); + blockWritten++; + + if (defaultSampleDuration[i] < 0 && duration[i] >= 0) { + // if the sample duration in unknown, + // calculate using current_duration - previous_duration + defaultSampleDuration[i] = (int) (bloq.absoluteTimecode - duration[i]); + } + duration[i] = bloq.absoluteTimecode; + + if (limitTimecode < 0) { + limitTimecode = bloq.absoluteTimecode + INTERV; + continue; + } + + if (bloq.absoluteTimecode >= limitTimecode) { + if (limitTimecodeByTrackId != i) { + limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode); + } + i++; + } + } + } + + makeCluster(out, -1, currentClusterOffset, false); + + long segmentSize = written - offsetSegmentSizeSet - 7; + + /* Segment size */ + seekTo(out, offsetSegmentSizeSet); + outByteBuffer.putLong(0, segmentSize); + out.write(outBuffer, 1, DataReader.LONG_SIZE - 1); + + /* Segment duration */ + long longestDuration = 0; + for (int i = 0; i < duration.length; i++) { + if (defaultSampleDuration[i] > 0) { + duration[i] += defaultSampleDuration[i]; + } + if (duration[i] > longestDuration) { + longestDuration = duration[i]; + } + } + seekTo(out, offsetInfoDurationSet); + outByteBuffer.putFloat(0, longestDuration); + dump(outBuffer, DataReader.FLOAT_SIZE, out); + + /* first Cluster offset */ + firstClusterOffset -= segmentOffset; + writeInt(out, offsetClusterSet, firstClusterOffset); + + seekTo(out, cueOffset); + + /* Cue */ + short cueSize = 0; + dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); // header size is 7 + + for (KeyFrame keyFrame : keyFrames) { + int size = makeCuePoint(cuesForTrackId, keyFrame, outBuffer); + + if ((cueSize + size + 7 + MINIMUM_EBML_VOID_SIZE) > CUE_RESERVE_SIZE) { + break; // no space left + } + + cueSize += size; + dump(outBuffer, size, out); + } + + makeEbmlVoid(out, CUE_RESERVE_SIZE - cueSize - 7, false); + + seekTo(out, cueOffset + 5); + outByteBuffer.putShort(0, cueSize); + dump(outBuffer, DataReader.SHORT_SIZE, out); + + /* seek head, seek for cues element */ + writeInt(out, offsetCuesSet, (int) (cueOffset - segmentOffset)); + + for (ClusterInfo cluster : clustersOffsetsSizes) { + writeInt(out, cluster.offset, cluster.size | 0x10000000); + } + } + + private Block getNextBlockFrom(final 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 (readersCluster[internalTrackId] == null) { + readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); + if (readersCluster[internalTrackId] == null) { + readersSegment[internalTrackId] = null; + return getNextBlockFrom(internalTrackId); + } + } + + SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); + if (res == null) { + readersCluster[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 = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE; + + return bloq; + } + + private void seekTo(final SharpStream stream, final long offset) throws IOException { + if (stream.canSeek()) { + stream.seek(offset); + } else { + if (offset > written) { + stream.skip(offset - written); + } else { + stream.rewind(); + stream.skip(offset); + } + } + + written = offset; + } + + private void writeInt(final SharpStream stream, final long offset, final int number) + throws IOException { + seekTo(stream, offset); + outByteBuffer.putInt(0, number); + dump(outBuffer, DataReader.INTEGER_SIZE, stream); + } + + private void writeBlock(final SharpStream stream, final Block bloq, final 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)); + + dump(listBuffer, stream); + + int read; + while ((read = bloq.data.read(outBuffer)) > 0) { + dump(outBuffer, read, stream); + } + } + + private long makeCluster(final SharpStream stream, final long timecode, long offset, + final boolean create) throws IOException { + ClusterInfo cluster; + + if (offset > 0) { + // save the size of the previous cluster (maximum 256 MiB) + cluster = clustersOffsetsSizes.get(clustersOffsetsSizes.size() - 1); + cluster.size = (int) (written - offset - CLUSTER_HEADER_SIZE); + } + + offset = written; + + if (create) { + /* cluster */ + dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); + + cluster = new ClusterInfo(); + cluster.offset = written; + clustersOffsetsSizes.add(cluster); + + dump(new byte[]{ + 0x10, 0x00, 0x00, 0x00, + /* timestamp */ + (byte) 0xe7 + }, stream); + + dump(encode(timecode, true), stream); + } + + return offset; + } + + private void makeEBML(final 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(final int internalTrackId, final 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)); + + /* codec delay*/ + if (track.codecDelay >= 0) { + buffer.add(new byte[]{0x56, (byte) 0xAA}); + buffer.add(encode(track.codecDelay, true)); + } + + /* codec seek pre-roll*/ + if (track.seekPreRoll >= 0) { + buffer.add(new byte[]{0x56, (byte) 0xBB}); + buffer.add(encode(track.seekPreRoll, true)); + } + + /* type */ + buffer.add(new byte[]{(byte) 0x83}); + buffer.add(encode(track.trackType, true)); + + /* default duration */ + if (track.defaultDuration >= 0) { + 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 int makeCuePoint(final int internalTrackId, final KeyFrame keyFrame, + final byte[] buffer) { + ArrayList cue = new ArrayList<>(5); + + /* CuePoint */ + cue.add(new byte[]{(byte) 0xbb}); + cue.add(null); + + /* CueTime */ + cue.add(new byte[]{(byte) 0xb3}); + cue.add(encode(keyFrame.duration, true)); + + /* CueTrackPosition */ + cue.addAll(makeCueTrackPosition(internalTrackId, keyFrame)); + + int size = 0; + lengthFor(cue); + + for (byte[] buff : cue) { + System.arraycopy(buff, 0, buffer, size, buff.length); + size += buff.length; + } + + return size; + } + + private ArrayList makeCueTrackPosition(final int internalTrackId, + final 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.clusterPosition, true)); + + /* CueRelativePosition */ + if (keyFrame.relativePosition > 0) { + buffer.add(new byte[]{(byte) 0xf0}); + buffer.add(encode(keyFrame.relativePosition, true)); + } + + return lengthFor(buffer); + } + + private void makeEbmlVoid(final SharpStream out, int size, final boolean wipe) + throws IOException { + /* ebml void */ + outByteBuffer.putShort(0, (short) 0xec20); + outByteBuffer.putShort(2, (short) (size - 4)); + + dump(outBuffer, 4, out); + + if (wipe) { + size -= 4; + while (size > 0) { + int write = Math.min(size, outBuffer.length); + dump(outBuffer, write, out); + size -= write; + } + } + } + + private void dump(final byte[] buffer, final SharpStream stream) throws IOException { + dump(buffer, buffer.length, stream); + } + + private void dump(final byte[] buffer, final int count, final SharpStream stream) + throws IOException { + stream.write(buffer, 0, count); + written += count; + } + + private void dump(final ArrayList buffers, final SharpStream stream) + throws IOException { + for (byte[] buffer : buffers) { + stream.write(buffer); + written += buffer.length; + } + } + + private ArrayList lengthFor(final 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(final long number, final 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 - 1f) / 8f); + + int shift = 0; + for (int i = length - 1; i >= 0; i--, shift += 8) { + long b = number >>> shift; + 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(final String value) { + byte[] str; + str = value.getBytes(StandardCharsets.UTF_8); // or use "utf-8" + + ArrayList buffer = new ArrayList<>(2); + buffer.add(encode(str.length, false)); + buffer.add(str); + + return buffer; + } + + private boolean valid(final 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(final long segment, final long cluster, final long block, final long timecode) { + clusterPosition = cluster - segment; + relativePosition = (int) (block - cluster - CLUSTER_HEADER_SIZE); + duration = timecode; + } + + final long clusterPosition; + final int relativePosition; + final long duration; + } + + class Block { + InputStream data; + int trackNumber; + byte flags; + int dataSize; + long absoluteTimecode; + + boolean isKeyframe() { + return (flags & 0x80) == 0x80; + } + + @NonNull + @Override + public String toString() { + return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, + isKeyframe(), absoluteTimecode); + } + } + + class ClusterInfo { + long offset; + int size; + } +} 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 index 5950ba3dd..46ec68d9e 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -1,63 +1,62 @@ -package org.schabi.newpipe.streams.io; - -import java.io.Closeable; -import java.io.IOException; - -/** - * based on c# - */ -public abstract class SharpStream implements Closeable { - - 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 long available(); - - public abstract void rewind() throws IOException; - - public abstract boolean isClosed(); - - @Override - public abstract void close(); - - public abstract boolean canRewind(); - - public abstract boolean canRead(); - - public abstract boolean canWrite(); - - public boolean canSetLength() { - return false; - } - - public boolean canSeek() { - return false; - } - - 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 void flush() throws IOException { - // STUB - } - - public void setLength(long length) throws IOException { - throw new IOException("Not implemented"); - } - - public void seek(long offset) throws IOException { - throw new IOException("Not implemented"); - } - - public long length() throws IOException { - throw new UnsupportedOperationException("Unsupported operation"); - } -} +package org.schabi.newpipe.streams.io; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Based on C#'s Stream class. + */ +public abstract class SharpStream implements Closeable { + 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 long available(); + + public abstract void rewind() throws IOException; + + public abstract boolean isClosed(); + + @Override + public abstract void close(); + + public abstract boolean canRewind(); + + public abstract boolean canRead(); + + public abstract boolean canWrite(); + + public boolean canSetLength() { + return false; + } + + public boolean canSeek() { + return false; + } + + 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 void flush() throws IOException { + // STUB + } + + public void setLength(final long length) throws IOException { + throw new IOException("Not implemented"); + } + + public void seek(final long offset) throws IOException { + throw new IOException("Not implemented"); + } + + public long length() throws IOException { + throw new UnsupportedOperationException("Unsupported operation"); + } +} diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 54a2cee26..141522287 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -6,7 +6,38 @@ + + + + + + + + + + + + + + +