Misc utils

this will include:
* Mp4 DASH reader/writter
* WebM reader/writter
* a subtitle converter for Timed Text Markup Language v1 and TranScript (v1, v2 and v3)
* SharpStream to wrap IntputStream and OutputStream in one interface
* custom implementation of DataInputStream
This commit is contained in:
kapodamy 2018-09-25 22:42:10 -03:00 committed by GitHub
parent 5223aece7b
commit 891e23374e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 3431 additions and 0 deletions

View File

@ -0,0 +1,105 @@
package org.schabi.newpipe.extractor.utils;
import android.os.Build;
import java.io.EOFException;
import java.io.IOException;
import org.schabi.newpipe.extractor.utils.io.SharpStream;
/**
* @author kapodamy
*/
public class DataReader {
public final SharpStream stream;
private long pos;
private final boolean rewind;
public DataReader(SharpStream stream) {
this.rewind = stream.canRewind();
this.stream = stream;
this.pos = 0L;
}
public long position() {
return pos;
}
public final int readInt() throws IOException {
primitiveRead(IntegerSize);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
}
public final int read() throws IOException {
int value = stream.read();
if (value == -1) {
throw new EOFException();
}
pos++;
return value;
}
public final long skipBytes(long amount) throws IOException {
amount = stream.skip(amount);
pos += amount;
return amount;
}
public final long readLong() throws IOException {
primitiveRead(LongSize);
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
return high << 32 | low;
}
public final short readShort() throws IOException {
primitiveRead(ShortSize);
return (short) (primitive[0] << 8 | primitive[1]);
}
public final int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
public final int read(byte[] buffer, int offset, int count) throws IOException {
int res = stream.read(buffer, offset, count);
pos += res;
return res;
}
public final boolean available() {
return stream.available() > 0;
}
public void rewind() throws IOException {
stream.rewind();
pos = 0;
}
public boolean canRewind() {
return rewind;
}
private short[] primitive = new short[LongSize];
private void primitiveRead(int amount) throws IOException {
byte[] buffer = new byte[amount];
int read = stream.read(buffer, 0, amount);
pos += read;
if (read != amount) {
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
}
for (int i = 0; i < buffer.length; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
}
}
public final static int ShortSize = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? Short.BYTES : 2;
public final static int LongSize = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? Long.BYTES : 8;
public final static int IntegerSize = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? Integer.BYTES : 4;
public final static int FloatSize = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? Float.BYTES : 4;
}

View File

@ -0,0 +1,817 @@
package org.schabi.newpipe.extractor.utils;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import org.schabi.newpipe.extractor.utils.io.SharpStream;
/**
* @author kapodamy
*/
public class Mp4DashReader {
// <editor-fold defaultState="collapsed" desc="Constants">
private static final int ATOM_MOOF = 0x6D6F6F66;
private static final int ATOM_MFHD = 0x6D666864;
private static final int ATOM_TRAF = 0x74726166;
private static final int ATOM_TFHD = 0x74666864;
private static final int ATOM_TFDT = 0x74666474;
private static final int ATOM_TRUN = 0x7472756E;
private static final int ATOM_MDIA = 0x6D646961;
private static final int ATOM_FTYP = 0x66747970;
private static final int ATOM_SIDX = 0x73696478;
private static final int ATOM_MOOV = 0x6D6F6F76;
private static final int ATOM_MDAT = 0x6D646174;
private static final int ATOM_MVHD = 0x6D766864;
private static final int ATOM_TRAK = 0x7472616B;
private static final int ATOM_MVEX = 0x6D766578;
private static final int ATOM_TREX = 0x74726578;
private static final int ATOM_TKHD = 0x746B6864;
private static final int ATOM_MFRA = 0x6D667261;
private static final int ATOM_TFRA = 0x74667261;
private static final int ATOM_MDHD = 0x6D646864;
private static final int BRAND_DASH = 0x64617368;
// </editor-fold>
private final DataReader stream;
private Mp4Track[] tracks = null;
private Box box;
private Moof moof;
private boolean chunkZero = false;
private int selectedTrack = -1;
public enum TrackKind {
Audio, Video, Other
}
public Mp4DashReader(SharpStream source) {
this.stream = new DataReader(source);
}
public void parse() throws IOException, NoSuchElementException {
if (selectedTrack > -1) {
return;
}
box = readBox(ATOM_FTYP);
if (parse_ftyp() != BRAND_DASH) {
throw new NoSuchElementException("Main Brand is not dash");
}
Moov moov = null;
int i;
while (box.type != ATOM_MOOF) {
ensure(box);
box = readBox();
switch (box.type) {
case ATOM_MOOV:
moov = parse_moov(box);
break;
case ATOM_SIDX:
break;
case ATOM_MFRA:
break;
case ATOM_MDAT:
throw new IOException("Expected moof, found mdat");
}
}
if (moov == null) {
throw new IOException("The provided Mp4 doesn't have the 'moov' box");
}
tracks = new Mp4Track[moov.trak.length];
for (i = 0; i < tracks.length; i++) {
tracks[i] = new Mp4Track();
tracks[i].trak = moov.trak[i];
if (moov.mvex_trex != null) {
for (Trex mvex_trex : moov.mvex_trex) {
if (tracks[i].trak.tkhd.trackId == mvex_trex.trackId) {
tracks[i].trex = mvex_trex;
}
}
}
if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) {
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
} else {
tracks[i].kind = TrackKind.Video;
}
}
}
public Mp4Track selectTrack(int index) {
selectedTrack = index;
return tracks[index];
}
/**
* Count all fragments present. This operation requires a seekable stream
*
* @return list with a basic info
* @throws IOException if the source stream is not seekeable
*/
public int getFragmentsCount() throws IOException {
if (selectedTrack < 0) {
throw new IllegalStateException("track no selected");
}
if (!stream.canRewind()) {
throw new IOException("The provided stream doesn't allow seek");
}
Box tmp;
int count = 0;
long orig_offset = stream.position();
if (box.type == ATOM_MOOF) {
tmp = box;
} else {
ensure(box);
tmp = readBox();
}
do {
if (tmp.type == ATOM_MOOF) {
ensure(readBox(ATOM_MFHD));
Box traf;
while ((traf = untilBox(tmp, ATOM_TRAF)) != null) {
Box tfhd = readBox(ATOM_TFHD);
if (parse_tfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) {
count++;
break;
}
ensure(tfhd);
ensure(traf);
}
}
ensure(tmp);
} while (stream.available() && (tmp = readBox()) != null);
stream.rewind();
stream.skipBytes((int) orig_offset);
return count;
}
public Mp4Track[] getAvailableTracks() {
return tracks;
}
public Mp4TrackChunk getNextChunk() throws IOException {
Mp4Track track = tracks[selectedTrack];
while (stream.available()) {
if (chunkZero) {
ensure(box);
if (!stream.available()) {
break;
}
box = readBox();
} else {
chunkZero = true;
}
switch (box.type) {
case ATOM_MOOF:
if (moof != null) {
throw new IOException("moof found without mdat");
}
moof = parse_moof(box, track.trak.tkhd.trackId);
if (moof.traf != null) {
if (hasFlag(moof.traf.trun.bFlags, 0x0001)) {
moof.traf.trun.dataOffset -= box.size + 8;
if (moof.traf.trun.dataOffset < 0) {
throw new IOException("trun box has wrong data offset, points outside of concurrent mdat box");
}
}
if (moof.traf.trun.chunkSize < 1) {
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
} else {
moof.traf.trun.chunkSize = box.size - 8;
}
}
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
if (hasFlag(moof.traf.tfhd.bFlags, 0x20)) {
moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration * moof.traf.trun.entryCount;
}
}
}
break;
case ATOM_MDAT:
if (moof == null) {
throw new IOException("mdat found without moof");
}
if (moof.traf == null) {
moof = null;
continue;// find another chunk
}
Mp4TrackChunk chunk = new Mp4TrackChunk();
chunk.moof = moof;
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
moof = null;
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
return chunk;
default:
}
}
return null;
}
// <editor-fold defaultState="collapsed" desc="Utils">
private long readUint() throws IOException {
return stream.readInt() & 0xffffffffL;
}
public static boolean hasFlag(int flags, int mask) {
return (flags & mask) == mask;
}
private String boxName(Box ref) {
return boxName(ref.type);
}
private String boxName(int type) {
try {
return new String(ByteBuffer.allocate(4).putInt(type).array(), "US-ASCII");
} catch (UnsupportedEncodingException e) {
return "0x" + Integer.toHexString(type);
}
}
private Box readBox() throws IOException {
Box b = new Box();
b.offset = stream.position();
b.size = stream.readInt();
b.type = stream.readInt();
return b;
}
private Box readBox(int expected) throws IOException {
Box b = readBox();
if (b.type != expected) {
throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b));
}
return b;
}
private void ensure(Box ref) throws IOException {
long skip = ref.offset + ref.size - stream.position();
if (skip == 0) {
return;
} else if (skip < 0) {
throw new EOFException(String.format(
"parser go beyond limits of the box. type=%s offset=%s size=%s position=%s",
boxName(ref), ref.offset, ref.size, stream.position()
));
}
stream.skipBytes((int) skip);
}
private Box untilBox(Box ref, int... expected) throws IOException {
Box b;
while (stream.position() < (ref.offset + ref.size)) {
b = readBox();
for (int type : expected) {
if (b.type == type) {
return b;
}
}
ensure(b);
}
return null;
}
// </editor-fold>
// <editor-fold defaultState="collapsed" desc="Box readers">
private Moof parse_moof(Box ref, int trackId) throws IOException {
Moof obj = new Moof();
Box b = readBox(ATOM_MFHD);
obj.mfhd_SequenceNumber = parse_mfhd();
ensure(b);
while ((b = untilBox(ref, ATOM_TRAF)) != null) {
obj.traf = parse_traf(b, trackId);
ensure(b);
if (obj.traf != null) {
return obj;
}
}
return obj;
}
private int parse_mfhd() throws IOException {
// version
// flags
stream.skipBytes(4);
return stream.readInt();
}
private Traf parse_traf(Box ref, int trackId) throws IOException {
Traf traf = new Traf();
Box b = readBox(ATOM_TFHD);
traf.tfhd = parse_tfhd(trackId);
ensure(b);
if (traf.tfhd == null) {
return null;
}
b = untilBox(ref, ATOM_TRUN, ATOM_TFDT);
if (b.type == ATOM_TFDT) {
traf.tfdt = parse_tfdt();
ensure(b);
b = readBox(ATOM_TRUN);
}
traf.trun = parse_trun();
ensure(b);
return traf;
}
private Tfhd parse_tfhd(int trackId) throws IOException {
Tfhd obj = new Tfhd();
obj.bFlags = stream.readInt();
obj.trackId = stream.readInt();
if (trackId != -1 && obj.trackId != trackId) {
return null;
}
if (hasFlag(obj.bFlags, 0x01)) {
stream.skipBytes(8);
}
if (hasFlag(obj.bFlags, 0x02)) {
stream.skipBytes(4);
}
if (hasFlag(obj.bFlags, 0x08)) {
obj.defaultSampleDuration = stream.readInt();
}
if (hasFlag(obj.bFlags, 0x10)) {
obj.defaultSampleSize = stream.readInt();
}
if (hasFlag(obj.bFlags, 0x20)) {
obj.defaultSampleFlags = stream.readInt();
}
return obj;
}
private long parse_tfdt() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
return version == 0 ? readUint() : stream.readLong();
}
private Trun parse_trun() throws IOException {
Trun obj = new Trun();
obj.bFlags = stream.readInt();
obj.entryCount = stream.readInt();// unsigned int
obj.entries_rowSize = 0;
if (hasFlag(obj.bFlags, 0x0100)) {
obj.entries_rowSize += 4;
}
if (hasFlag(obj.bFlags, 0x0200)) {
obj.entries_rowSize += 4;
}
if (hasFlag(obj.bFlags, 0x0400)) {
obj.entries_rowSize += 4;
}
if (hasFlag(obj.bFlags, 0x0800)) {
obj.entries_rowSize += 4;
}
obj.bEntries = new byte[obj.entries_rowSize * obj.entryCount];
if (hasFlag(obj.bFlags, 0x0001)) {
obj.dataOffset = stream.readInt();
}
if (hasFlag(obj.bFlags, 0x0004)) {
obj.bFirstSampleFlags = stream.readInt();
}
stream.read(obj.bEntries);
for (int i = 0; i < obj.entryCount; i++) {
TrunEntry entry = obj.getEntry(i);
if (hasFlag(obj.bFlags, 0x0100)) {
obj.chunkDuration += entry.sampleDuration;
}
if (hasFlag(obj.bFlags, 0x0200)) {
obj.chunkSize += entry.sampleSize;
}
if (hasFlag(obj.bFlags, 0x0800)) {
if (!hasFlag(obj.bFlags, 0x0100)) {
obj.chunkDuration += entry.sampleCompositionTimeOffset;
}
}
}
return obj;
}
private int parse_ftyp() throws IOException {
int brand = stream.readInt();
stream.skipBytes(4);// minor version
return brand;
}
private Mvhd parse_mvhd() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
// creation entries_time
// modification entries_time
stream.skipBytes(2 * (version == 0 ? 4 : 8));
Mvhd obj = new Mvhd();
obj.timeScale = readUint();
// chunkDuration
stream.skipBytes(version == 0 ? 4 : 8);
// rate
// volume
// reserved
// matrix array
// predefined
stream.skipBytes(76);
obj.nextTrackId = readUint();
return obj;
}
private Tkhd parse_tkhd() throws IOException {
int version = stream.read();
Tkhd obj = new Tkhd();
// flags
// creation entries_time
// modification entries_time
stream.skipBytes(3 + (2 * (version == 0 ? 4 : 8)));
obj.trackId = stream.readInt();
stream.skipBytes(4);// reserved
obj.duration = version == 0 ? readUint() : stream.readLong();
stream.skipBytes(2 * 4);// reserved
obj.bLayer = stream.readShort();
obj.bAlternateGroup = stream.readShort();
obj.bVolume = stream.readShort();
stream.skipBytes(2);// reserved
obj.matrix = new byte[9 * 4];
stream.read(obj.matrix);
obj.bWidth = stream.readInt();
obj.bHeight = stream.readInt();
return obj;
}
private Trak parse_trak(Box ref) throws IOException {
Trak trak = new Trak();
Box b = readBox(ATOM_TKHD);
trak.tkhd = parse_tkhd();
ensure(b);
b = untilBox(ref, ATOM_MDIA);
trak.mdia = new byte[b.size];
ByteBuffer buffer = ByteBuffer.wrap(trak.mdia);
buffer.putInt(b.size);
buffer.putInt(ATOM_MDIA);
stream.read(trak.mdia, 8, b.size - 8);
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
return trak;
}
private int parse_mdia(ByteBuffer data) {
while (data.hasRemaining()) {
int end = data.position() + data.getInt();
if (data.getInt() == ATOM_MDHD) {
byte version = data.get();
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
return data.getInt();
}
data.position(end);
}
return 0;// this NEVER should happen
}
private Moov parse_moov(Box ref) throws IOException {
Box b = readBox(ATOM_MVHD);
Moov moov = new Moov();
moov.mvhd = parse_mvhd();
ensure(b);
ArrayList<Trak> tmp = new ArrayList<>((int) moov.mvhd.nextTrackId);
while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) {
switch (b.type) {
case ATOM_TRAK:
tmp.add(parse_trak(b));
break;
case ATOM_MVEX:
moov.mvex_trex = parse_mvex(b, (int) moov.mvhd.nextTrackId);
break;
}
ensure(b);
}
moov.trak = tmp.toArray(new Trak[tmp.size()]);
return moov;
}
private Trex[] parse_mvex(Box ref, int possibleTrackCount) throws IOException {
ArrayList<Trex> tmp = new ArrayList<>(possibleTrackCount);
Box b;
while ((b = untilBox(ref, ATOM_TREX)) != null) {
tmp.add(parse_trex());
ensure(b);
}
return tmp.toArray(new Trex[tmp.size()]);
}
private Trex parse_trex() throws IOException {
// version
// flags
stream.skipBytes(4);
Trex obj = new Trex();
obj.trackId = stream.readInt();
obj.defaultSampleDescriptionIndex = stream.readInt();
obj.defaultSampleDuration = stream.readInt();
obj.defaultSampleSize = stream.readInt();
obj.defaultSampleFlags = stream.readInt();
return obj;
}
private Tfra parse_tfra() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
Tfra tfra = new Tfra();
tfra.trackId = stream.readInt();
stream.skipBytes(3);// reserved
int bFlags = stream.read();
int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
tfra.entries_time = new int[stream.readInt()];
for (int i = 0; i < tfra.entries_time.length; i++) {
tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
}
return tfra;
}
private Sidx parse_sidx() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
Sidx obj = new Sidx();
obj.referenceId = stream.readInt();
obj.timescale = stream.readInt();
// earliest presentation entries_time
// first offset
// reserved
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
obj.entries_subsegmentDuration = new int[stream.readShort()];
for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) {
// reference type
// referenced size
stream.skipBytes(4);
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
// starts with SAP
// SAP type
// SAP delta entries_time
stream.skipBytes(4);
}
return obj;
}
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
long limit = ref.offset + ref.size;
while (stream.position() < limit) {
box = readBox();
if (box.type == ATOM_TFRA) {
tmp.add(parse_tfra());
}
ensure(box);
}
return tmp.toArray(new Tfra[tmp.size()]);
}
// </editor-fold>
// <editor-fold defaultState="collapsed" desc="Helper classes">
class Box {
int type;
long offset;
int size;
}
class Sidx {
int timescale;
int referenceId;
int[] entries_subsegmentDuration;
}
public class Moof {
int mfhd_SequenceNumber;
public Traf traf;
}
public class Traf {
public Tfhd tfhd;
long tfdt;
public Trun trun;
}
public class Tfhd {
int bFlags;
public int trackId;
int defaultSampleDuration;
int defaultSampleSize;
int defaultSampleFlags;
}
public class TrunEntry {
public int sampleDuration;
public int sampleSize;
public int sampleFlags;
public int sampleCompositionTimeOffset;
}
public class Trun {
public int chunkDuration;
public int chunkSize;
public int bFlags;
int bFirstSampleFlags;
int dataOffset;
public int entryCount;
byte[] bEntries;
int entries_rowSize;
public TrunEntry getEntry(int i) {
ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entries_rowSize, entries_rowSize);
TrunEntry entry = new TrunEntry();
if (hasFlag(bFlags, 0x0100)) {
entry.sampleDuration = buffer.getInt();
}
if (hasFlag(bFlags, 0x0200)) {
entry.sampleSize = buffer.getInt();
}
if (hasFlag(bFlags, 0x0400)) {
entry.sampleFlags = buffer.getInt();
}
if (hasFlag(bFlags, 0x0800)) {
entry.sampleCompositionTimeOffset = buffer.getInt();
}
return entry;
}
}
public class Tkhd {
int trackId;
long duration;
short bVolume;
int bWidth;
int bHeight;
byte[] matrix;
short bLayer;
short bAlternateGroup;
}
public class Trak {
public Tkhd tkhd;
public int mdia_mdhd_timeScale;
byte[] mdia;
}
class Mvhd {
long timeScale;
long nextTrackId;
}
class Moov {
Mvhd mvhd;
Trak[] trak;
Trex[] mvex_trex;
}
class Tfra {
int trackId;
int[] entries_time;
}
public class Trex {
private int trackId;
int defaultSampleDescriptionIndex;
int defaultSampleDuration;
int defaultSampleSize;
int defaultSampleFlags;
}
public class Mp4Track {
public TrackKind kind;
public Trak trak;
public Trex trex;
}
public class Mp4TrackChunk {
public InputStream data;
public Moof moof;
}
//</editor-fold>
}

View File

@ -0,0 +1,623 @@
package org.schabi.newpipe.extractor.utils;
import org.schabi.newpipe.extractor.utils.io.SharpStream;
import org.schabi.newpipe.extractor.utils.Mp4DashReader.Mp4Track;
import org.schabi.newpipe.extractor.utils.Mp4DashReader.Mp4TrackChunk;
import org.schabi.newpipe.extractor.utils.Mp4DashReader.Trak;
import org.schabi.newpipe.extractor.utils.Mp4DashReader.Trex;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.schabi.newpipe.extractor.utils.Mp4DashReader.hasFlag;
/**
*
* @author kapodamy
*/
public class Mp4DashWriter {
private final static byte DIMENSIONAL_FIVE = 5;
private final static byte DIMENSIONAL_TWO = 2;
private final static short DEFAULT_TIMESCALE = 1000;
private final static int BUFFER_SIZE = 8 * 1024;
private final static byte DEFAULT_TREX_SIZE = 32;
private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
private final static int EPOCH_OFFSET = 2082844800;
private Mp4Track[] infoTracks;
private SharpStream[] sourceTracks;
private Mp4DashReader[] readers;
private final long time;
private boolean done = false;
private boolean parsed = false;
private long written = 0;
private ArrayList<ArrayList<Integer>> chunkTimes;
private ArrayList<Long> moofOffsets;
private ArrayList<Integer> fragSizes;
public Mp4DashWriter(SharpStream... source) {
sourceTracks = source;
readers = new Mp4DashReader[sourceTracks.length];
infoTracks = new Mp4Track[sourceTracks.length];
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
}
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
return readers[sourceIndex].getAvailableTracks();
}
public void parseSources() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
for (int i = 0; i < readers.length; i++) {
readers[i] = new Mp4DashReader(sourceTracks[i]);
readers[i].parse();
}
} finally {
parsed = true;
}
}
public void selectTracks(int... trackIndex) throws IOException {
if (done) {
throw new IOException("already done");
}
if (chunkTimes != null) {
throw new IOException("tracks already selected");
}
try {
chunkTimes = new ArrayList<>(readers.length);
moofOffsets = new ArrayList<>(32);
fragSizes = new ArrayList<>(32);
for (int i = 0; i < readers.length; i++) {
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
chunkTimes.add(new ArrayList<Integer>(32));
}
} finally {
parsed = true;
}
}
public long getBytesWritten() {
return written;
}
public void build(SharpStream out) throws IOException, RuntimeException {
if (done) {
throw new RuntimeException("already done");
}
if (!out.canWrite()) {
throw new IOException("the provided output is not writable");
}
long sidxOffsets = -1;
int maxFrags = 0;
for (SharpStream stream : sourceTracks) {
if (!stream.canRewind()) {
sidxOffsets = -2;// sidx not available
}
}
try {
dump(make_ftyp(), out);
dump(make_moov(), out);
if (sidxOffsets == -1 && out.canRewind()) {
//<editor-fold defaultstate="collapsed" desc="calculate sidx">
int reserved = 0;
for (Mp4DashReader reader : readers) {
int count = reader.getFragmentsCount();
if (count > maxFrags) {
maxFrags = count;
}
reserved += 12 + calcSidxBodySize(count);
}
if (maxFrags > 0xFFFF) {
sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
} else {
sidxOffsets = written;
dump(make_free(reserved), out);
}
//</editor-fold>
}
ArrayList<Mp4TrackChunk> chunks = new ArrayList<>(readers.length);
chunks.add(null);
int read;
byte[] buffer = new byte[BUFFER_SIZE];
int sequenceNumber = 1;
while (true) {
chunks.clear();
for (int i = 0; i < readers.length; i++) {
Mp4TrackChunk chunk = readers[i].getNextChunk();
if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
continue;
}
chunk.moof.traf.tfhd.trackId = i + 1;
chunks.add(chunk);
if (sequenceNumber == 1) {
if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
} else {
chunkTimes.get(i).add(0);
}
}
chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
}
if (chunks.size() < 1) {
break;
}
long offset = written;
moofOffsets.add(offset);
dump(make_moof(sequenceNumber++, chunks, offset), out);
dump(make_mdat(chunks), out);
for (Mp4TrackChunk chunk : chunks) {
while ((read = chunk.data.read(buffer)) > 0) {
out.write(buffer, 0, read);
written += read;
}
}
fragSizes.add((int) (written - offset));
}
dump(make_mfra(), out);
if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
long len = written;
out.rewind();
out.skip(sidxOffsets);
written = sidxOffsets;
sidxOffsets = moofOffsets.get(0);
for (int i = 0; i < readers.length; i++) {
dump(make_sidx(i, sidxOffsets - written), out);
}
written = len;
}
} finally {
done = true;
}
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public void close() {
done = true;
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
}
sourceTracks = null;
readers = null;
infoTracks = null;
moofOffsets = null;
chunkTimes = null;
}
// <editor-fold defaultstate="collapsed" desc="Utils">
private void dump(byte[][] buffer, SharpStream stream) throws IOException {
for (byte[] buff : buffer) {
stream.write(buff);
written += buff.length;
}
}
private byte[][] lengthFor(byte[][] buffer) {
int length = 0;
for (byte[] buff : buffer) {
length += buff.length;
}
ByteBuffer.wrap(buffer[0]).putInt(length);
return buffer;
}
private int calcSidxBodySize(int entryCount) {
return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="Box makers">
private byte[][] make_moof(int sequence, ArrayList<Mp4TrackChunk> chunks, long referenceOffset) {
int pos = 2;
TrunExtra[] extra = new TrunExtra[chunks.size()];
byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
};
buffer[1] = new byte[4];
ByteBuffer.wrap(buffer[1]).putInt(sequence);
for (int i = 0; i < extra.length; i++) {
extra[i] = new TrunExtra();
for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
buffer[pos++] = buff;
}
}
lengthFor(buffer);
int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
for (int i = 0; i < extra.length; i++) {
extra[i].byteBuffer.putInt(offset);
offset += chunks.get(i).moof.traf.trun.chunkSize;
}
return buffer;
}
private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
};
int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
byte tfhdBodySize = 8 + 8;
if (hasFlag(flags, 0x08)) {
tfhdBodySize += 4;
}
if (hasFlag(flags, 0x10)) {
tfhdBodySize += 4;
}
if (hasFlag(flags, 0x20)) {
tfhdBodySize += 4;
}
buffer[1] = new byte[tfhdBodySize];
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.position(4);
set.putInt(chunk.moof.traf.tfhd.trackId);
set.putLong(moofOffset);
if (hasFlag(flags, 0x08)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
}
if (hasFlag(flags, 0x10)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
}
if (hasFlag(flags, 0x20)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
}
set.putInt(0, flags);
ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
buffer[2] = new byte[]{
0x00, 0x00, 0x00, 0x14,
0x74, 0x66, 0x64, 0x74,
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
buffer[3] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
buffer[4] = chunk.moof.traf.trun.bEntries;
lengthFor(buffer);
set = ByteBuffer.wrap(buffer[3]);
set.putInt(buffer[3].length + buffer[4].length);
set.position(8);
set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
set.putInt(chunk.moof.traf.trun.entryCount);
extra.byteBuffer = set;
return buffer;
}
private byte[][] make_mdat(ArrayList<Mp4TrackChunk> chunks) {
byte[][] buffer = new byte[][]{
{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
}
};
int length = 0;
for (Mp4TrackChunk chunk : chunks) {
length += chunk.moof.traf.trun.chunkSize;
}
ByteBuffer.wrap(buffer[0]).putInt(length + 8);
return buffer;
}
private byte[][] make_ftyp() {
return new byte[][]{
{
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
}
};
}
private byte[][] make_mvhd() {
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
};
buffer[1] = new byte[28];
buffer[2] = new byte[]{
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
// default matrix
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00
};
buffer[3] = new byte[24];// predefined
buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
long longestTrack = 0;
for (Mp4Track track : infoTracks) {
long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
if (tmp > longestTrack) {
longestTrack = tmp;
}
}
ByteBuffer.wrap(buffer[1])
.putLong(time)
.putLong(time)
.putInt(DEFAULT_TIMESCALE)
.putLong(longestTrack);
return buffer;
}
private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
if (trak.tkhd.matrix.length != 36) {
throw new RuntimeException("bad track matrix length (expected 36)");
}
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
};
buffer[1] = new byte[48];
buffer[2] = trak.tkhd.matrix;
buffer[3] = new byte[8];
buffer[4] = trak.mdia;
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putLong(time);
set.putLong(time);
set.putInt(trackId);
set.position(24);
set.putLong(trak.tkhd.duration);
set.position(40);
set.putShort(trak.tkhd.bLayer);
set.putShort(trak.tkhd.bAlternateGroup);
set.putShort(trak.tkhd.bVolume);
ByteBuffer.wrap(buffer[3])
.putInt(trak.tkhd.bWidth)
.putInt(trak.tkhd.bHeight);
return lengthFor(buffer);
}
private byte[][] make_moov() throws RuntimeException {
int pos = 1;
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
};
for (byte[] buff : make_mvhd()) {
buffer[pos++] = buff;
}
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
buffer[pos++] = buff;
}
}
buffer[pos] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
};
ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
buffer[pos++] = buff;
}
}
// default udta
buffer[pos] = new byte[]{
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00,
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
};
return lengthFor(buffer);
}
private byte[][] make_trex(int trackId, Trex trex) {
byte[][] buffer = new byte[][]{
{
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
},
new byte[20]
};
ByteBuffer.wrap(buffer[1])
.putInt(trackId)
.putInt(trex.defaultSampleDescriptionIndex)
.putInt(trex.defaultSampleDuration)
.putInt(trex.defaultSampleSize)
.putInt(trex.defaultSampleFlags);
return buffer;
}
private byte[][] make_tfra(int trackId, List<Integer> times, List<Long> moofOffsets) {
int entryCount = times.size() - 1;
byte[][] buffer = new byte[DIMENSIONAL_TWO][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
};
buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putInt(trackId);
set.position(8);
set.putInt(entryCount);
long decodeTime = 0;
for (int i = 0; i < entryCount; i++) {
decodeTime += times.get(i);
set.putLong(decodeTime);
set.putLong(moofOffsets.get(i));
set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
}
return lengthFor(buffer);
}
private byte[][] make_mfra() {
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
};
int pos = 1;
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
buffer[pos++] = buff;
}
}
buffer[pos] = new byte[]{// mfro
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
lengthFor(buffer);
ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
set.position(12);
set.put(buffer[0], 0, 4);
return buffer;
}
private byte[][] make_sidx(int internalTrackId, long firstOffset) {
List<Integer> times = chunkTimes.get(internalTrackId);
int count = times.size() - 1;// the first item is ignored (composition time)
if (count > 65535) {
throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
}
byte[][] buffer = new byte[][]{
new byte[]{
0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
},
new byte[calcSidxBodySize(count)]
};
lengthFor(buffer);
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putInt(internalTrackId + 1);
set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
set.putLong(0);
set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
set.putInt(0xFFFF & count);// unsigned
int i = 0;
while (i < count) {
set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
set.putInt(times.get(i + 1));
set.putInt(0x90000000);// default SAP settings
i++;
}
return buffer;
}
private byte[][] make_free(int totalSize) {
return lengthFor(new byte[][]{
new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
new byte[totalSize - 8]// this is waste of RAM
});
}
//</editor-fold>
class TrunExtra {
ByteBuffer byteBuffer;
}
}

View File

@ -0,0 +1,539 @@
package org.schabi.newpipe.extractor.utils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.schabi.newpipe.extractor.utils.io.SharpStream;
import org.xmlpull.v1.XmlPullParserFactory;
/**
*
* @author kapodamy
*/
public class SubtitleConverter {
private static final int BUFFER_SIZE = 64 * 1024;
private static final String NEW_LINE = "\r\n";
public int dumpTTML(InputStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines) {
try {
final int[] frame_index = {0};// ugly workaround
final Charset charset = Charset.forName("utf-8");
read_xml_based(in, new FrameWriter() {
@Override
public void yield(SubtitleFrame frame) throws IOException {
if (ignoreEmptyFrames && frame.isEmptyText()) {
return;
}
out.write(String.valueOf(frame_index[0]++).getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(getTime(frame.start, true).getBytes(charset));
out.write(" --> ".getBytes(charset));
out.write(getTime(frame.end, true).getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(frame.text.getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
}
}, detectYoutubeDuplicateLines, "tt", "xmlns", "http://www.w3.org/ns/ttml", new String[]{"tt", "body", "div", "p"}, "begin", "end", true);
} catch (Exception err) {
if (err instanceof IOException) {
return 1;
} else if (err instanceof ParseException) {
return 2;
} else if (err instanceof XmlPullParserException) {
return 3;
}
return 4;
}
return 0;
}
private void read_xml_based(InputStream reader, FrameWriter callback, boolean detectYoutubeDuplicateLines,
String root, String formatAttr, String formatVersion, String[] framePath,
String timeAttr, String durationAttr, boolean hasTimestamp
) throws XmlPullParserException, IOException, ParseException {
/*
* XML based subtitles parser with BASIC support
* multiple CUE is not supported
* styling is not supported
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
* also TimestampTagOption enum is not applicable
* Language parsing is not supported
*/
XmlDocument xml = new XmlDocument(reader, BUFFER_SIZE);
String attr;
// get the format version or namespace
XmlNode node = xml.selectSingleNode(root);
if (node == null) {
throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
}
if (formatAttr.equals("xmlns")) {
if (!node.getNameSpace().equals(formatVersion)) {
throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
}
} else {
attr = node.getAttribute(formatAttr);
if (attr == null) {
throw new ParseException("Can't get the format attribute", -1);
}
if (!attr.equals(formatVersion)) {
throw new ParseException("Invalid format version : " + attr, -1);
}
}
XmlNodeList node_list;
int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
if (!hasTimestamp) {
node_list = xml.selectNodes("timedtext", "head", "wp");
if (node_list != null) {
// if the subtitle has multiple CUEs, use the highest value
while ((node = node_list.getNextNode()) != null) {
try {
int tmp = Integer.parseInt(node.getAttribute("ah"));
if (tmp > line_break) {
line_break = tmp;
}
} catch (NumberFormatException err) {
}
}
}
}
// parse every frame
node_list = xml.selectNodes(framePath);
if (node_list == null) {
return;// no frames detected
}
int fs_ff = -1;// first timestamp of first frame
boolean limit_lines = false;
while ((node = node_list.getNextNode()) != null) {
SubtitleFrame obj = new SubtitleFrame();
obj.text = node.getInnerText();
attr = node.getAttribute(timeAttr);// ¡this cant be null!
obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
attr = node.getAttribute(durationAttr);
if (obj.text == null || attr == null) {
continue;// normally is a blank line (on auto-generated subtitles) ignore
}
if (hasTimestamp) {
obj.end = parseTimestamp(attr);
if (detectYoutubeDuplicateLines) {
if (limit_lines) {
int swap = obj.end;
obj.end = fs_ff;
fs_ff = swap;
} else {
if (fs_ff < 0) {
fs_ff = obj.end;
} else {
if (fs_ff < obj.start) {
limit_lines = true;// the subtitles has duplicated lines
} else {
detectYoutubeDuplicateLines = false;
}
}
}
}
} else {
obj.end = obj.start + Integer.parseInt(attr);
}
if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
// implement auto line breaking (once)
StringBuilder text = new StringBuilder(obj.text);
obj.text = null;
switch (text.charAt(line_break)) {
case ' ':
case '\t':
putBreakAt(line_break, text);
break;
default:// find the word start position
for (int j = line_break - 1; j > 0; j--) {
switch (text.charAt(j)) {
case ' ':
case '\t':
putBreakAt(j, text);
j = -1;
break;
case '\r':
case '\n':
j = -1;// long word, just ignore
break;
}
}
break;
}
obj.text = text.toString();// set the processed text
}
callback.yield(obj);
}
}
private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
if (multiImpl.length() < 1) {
return 0;
} else if (multiImpl.length() == 1) {
return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
}
// detect wallclock-time
if (multiImpl.startsWith("wallclock(")) {
throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
}
// detect offset-time
if (multiImpl.indexOf(':') < 0) {
int multiplier = 1000;
char metric = multiImpl.charAt(multiImpl.length() - 1);
switch (metric) {
case 'h':
multiplier *= 3600000;
break;
case 'm':
multiplier *= 60000;
break;
case 's':
if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
multiplier = 1;// ms
}
break;
default:
if (!Character.isDigit(metric)) {
throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
}
metric = '\0';
break;
}
try {
String offset_time = multiImpl;
if (multiplier == 1) {
offset_time = offset_time.substring(0, offset_time.length() - 2);
} else if (metric != '\0') {
offset_time = offset_time.substring(0, offset_time.length() - 1);
}
double time_metric_based = Double.parseDouble(offset_time);
if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
return (int) (time_metric_based * multiplier);
}
} catch (Exception err) {
throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
}
}
// detect clock-time
int time = 0;
String[] units = multiImpl.split(":");
if (units.length < 3) {
throw new ParseException("Invalid clock-time timestamp", -1);
}
time += Integer.parseInt(units[0]) * 3600000;// hours
time += Integer.parseInt(units[1]) * 60000;//minutes
time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
// frames and sub-frames are ignored (not implemented)
// time += units[3] * fps;
return time;
}
private static void putBreakAt(int idx, StringBuilder str) {
// this should be optimized at compile time
if (NEW_LINE.length() > 1) {
str.delete(idx, idx + 1);// remove after replace
str.insert(idx, NEW_LINE);
} else {
str.setCharAt(idx, NEW_LINE.charAt(0));
}
}
private static String getTime(int time, boolean comma) {
// cast every value to integer to avoid auto-round in ToString("00").
StringBuilder str = new StringBuilder(12);
str.append(numberToString(time / 1000 / 3600, 2));// hours
str.append(':');
str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
str.append(':');
str.append(numberToString(time / 1000 % 60, 2));// seconds
str.append(comma ? ',' : '.');
str.append(numberToString(time % 1000, 3));// miliseconds
return str.toString();
}
private static String numberToString(int nro, int pad) {
return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
}
/**
* XmlPullParser wrapper
* @param parser XmlPullParser instance
* @param name node name
* @param depth current tree deep
* @return true if the node was reached, otherwise, false
* @throws XmlPullParserException if cant read the next XML tag
* @throws IOException I/O error
*/
private static boolean getNextNode(XmlPullParser parser, String name, int depth) throws XmlPullParserException, IOException {
int cursor = 0;
int eventType = 0;
while (eventType != XmlPullParser.END_DOCUMENT) {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
int tmp = parser.getDepth();
if (tmp < depth) {
return false;
}
if (tmp == depth && cursor == 0 && parser.getName().equals(name)) {
return true;
}
cursor++;
break;
case XmlPullParser.END_TAG:
if (cursor > 0) {
cursor--;
}
}
}
return false;
}
/******************
* helper classes *
******************/
private interface FrameWriter {
void yield(SubtitleFrame frame) throws IOException;
}
private static class SubtitleFrame {
//Java no support unsigned int
public int end;
public int start;
public String text = "";
private boolean isEmptyText() {
if (text == null) {
return true;
}
for (int i = 0; i < text.length(); i++) {
switch (text.charAt(i)) {
case ' ':
case '\t':
case '\r':
case '\n':
break;
default:
return false;
}
}
return true;
}
}
private class XmlDocument {
private BufferedInputStream src;
private XmlPullParserFactory fac;
XmlDocument(InputStream stream, int bufferSize) throws XmlPullParserException {
// due how xml parsing works is necessary a wrapper
src = new BufferedInputStream(stream, bufferSize);
src.mark(0);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
fac = factory;
}
XmlNode selectSingleNode(String... path) throws XmlPullParserException, IOException {
if (path.length < 1) {
return null;
}
src.reset();// ¡this is very much important!
XmlPullParser parser = fac.newPullParser();
parser.setInput(src, null);
for (int i = 0; i < path.length; i++) {
if (!getNextNode(parser, path[i], i + 1)) {
return null;
}
}
return new XmlNode(parser);
}
XmlNodeList selectNodes(String... path) throws XmlPullParserException, IOException {
XmlNode node = selectSingleNode(path);
if (node == null) {
return null;
}
return new XmlNodeList(node.parser);
}
}
private class XmlNode {
XmlPullParser parser;
XmlNode(XmlPullParser parser) {
this.parser = parser;
}
private void init_attrs() {
if (attrs != null) {
return;
}
// backup attributes first
attrs = new HashMap<String, String>(parser.getAttributeCount());
for (int i = 0; i < parser.getAttributeCount(); i++) {
attrs.put(parser.getAttributeName(i), parser.getAttributeValue(i));
}
}
String getText() throws IOException, XmlPullParserException {
init_attrs();
int eventType = 0;
boolean crash = false;
int deep = parser.getDepth();
while (!crash && eventType != XmlPullParser.END_DOCUMENT) {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.TEXT:
if (parser.getDepth() != deep) {
continue;
}
return parser.getText();
case XmlPullParser.END_TAG:
if (parser.getDepth() > deep) {
continue;
}
return null;
case XmlPullParser.START_TAG:
if (parser.getDepth() < deep) {
crash = true;
}
break;
}
}
throw new XmlPullParserException("cant read the text node, XmlPullParser crashed");
}
String getInnerText() throws IOException, XmlPullParserException {
init_attrs();
int eventType = 0;
boolean crash = false;
int deep = parser.getDepth();
StringBuilder buffer = new StringBuilder(128);
while (!crash && eventType != XmlPullParser.END_DOCUMENT) {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.TEXT:
String str = parser.getText();
if (str != null) {
buffer.append(str);
}
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() > deep) {
continue;
}
return buffer.toString();
case XmlPullParser.START_TAG:
if (parser.getDepth() < deep) {
crash = true;
}
break;
}
}
throw new XmlPullParserException("cant read the text node, XmlPullParser crashed");
}
String getAttribute(String name) {
return attrs == null ? parser.getAttributeValue(null, name) : attrs.get(name);
}
String getNameSpace() {
return parser.getNamespace();
}
private Map<String, String> attrs;
}
private class XmlNodeList {
private XmlPullParser parser;
boolean first = true;
String node_name;
int node_depth;
XmlNodeList(XmlPullParser parser) {
this.parser = parser;
node_name = parser.getName();
node_depth = parser.getDepth();
}
XmlNode getNextNode() throws XmlPullParserException, IOException {
if (first) {
first = false;
return new XmlNode(parser);
}
if (!SubtitleConverter.getNextNode(parser, node_name, node_depth)) {
parser = null;
}
return parser == null ? null : new XmlNode(parser);
}
}
}

View File

@ -0,0 +1,65 @@
package org.schabi.newpipe.extractor.utils;
import java.io.InputStream;
import java.io.IOException;
public class TrackDataChunk extends InputStream {
private final DataReader base;
private int size;
public TrackDataChunk(DataReader base, int size) {
this.base = base;
this.size = size;
}
@Override
public int read() throws IOException {
if (size < 1) {
return -1;
}
int res = base.read();
if (res >= 0) {
size--;
}
return res;
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
count = Math.min(size, count);
int read = base.read(buffer, offset, count);
size -= count;
return read;
}
@Override
public long skip(long amount) throws IOException {
long res = base.skipBytes(Math.min(amount, size));
size -= res;
return res;
}
@Override
public int available() {
return size;
}
@Override
public void close() {
size = 0;
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@ -0,0 +1,507 @@
package org.schabi.newpipe.extractor.utils;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.schabi.newpipe.extractor.utils.io.SharpStream;
/**
*
* @author kapodamy
*/
public class WebMReader {
//<editor-fold defaultState="collapsed" desc="constants">
private final static int ID_EMBL = 0x0A45DFA3;
private final static int ID_EMBLReadVersion = 0x02F7;
private final static int ID_EMBLDocType = 0x0282;
private final static int ID_EMBLDocTypeReadVersion = 0x0285;
private final static int ID_Segment = 0x08538067;
private final static int ID_Info = 0x0549A966;
private final static int ID_TimecodeScale = 0x0AD7B1;
private final static int ID_Duration = 0x489;
private final static int ID_Tracks = 0x0654AE6B;
private final static int ID_TrackEntry = 0x2E;
private final static int ID_TrackNumber = 0x57;
private final static int ID_TrackType = 0x03;
private final static int ID_CodecID = 0x06;
private final static int ID_CodecPrivate = 0x23A2;
private final static int ID_Video = 0x60;
private final static int ID_Audio = 0x61;
private final static int ID_DefaultDuration = 0x3E383;
private final static int ID_FlagLacing = 0x1C;
private final static int ID_Cluster = 0x0F43B675;
private final static int ID_Timecode = 0x67;
private final static int ID_SimpleBlock = 0x23;
//</editor-fold>
public enum TrackKind {
Audio/*2*/, Video/*1*/, Other
}
private DataReader stream;
private Segment segment;
private WebMTrack[] tracks;
private int selectedTrack;
private boolean done;
private boolean firstSegment;
public WebMReader(SharpStream source) {
this.stream = new DataReader(source);
}
public void parse() throws IOException {
Element elem = readElement(ID_EMBL);
if (!readEbml(elem, 1, 2)) {
throw new UnsupportedOperationException("Unsupported EBML data (WebM)");
}
ensure(elem);
elem = untilElement(null, ID_Segment);
if (elem == null) {
throw new IOException("Fragment element not found");
}
segment = readSegment(elem, 0, true);
tracks = segment.tracks;
selectedTrack = -1;
done = false;
firstSegment = true;
}
public WebMTrack[] getAvailableTracks() {
return tracks;
}
public WebMTrack selectTrack(int index) {
selectedTrack = index;
return tracks[index];
}
public Segment getNextSegment() throws IOException {
if (done) {
return null;
}
if (firstSegment && segment != null) {
firstSegment = false;
return segment;
}
ensure(segment.ref);
Element elem = untilElement(null, ID_Segment);
if (elem == null) {
done = true;
return null;
}
segment = readSegment(elem, 0, false);
return segment;
}
//<editor-fold defaultstate="collapsed" desc="utils">
private long readNumber(Element parent) throws IOException {
int length = (int) parent.contentSize;
long value = 0;
while (length-- > 0) {
int read = stream.read();
if (read == -1) {
throw new EOFException();
}
value = (value << 8) | read;
}
return value;
}
private String readString(Element parent) throws IOException {
return new String(readBlob(parent), "utf-8");
}
private byte[] readBlob(Element parent) throws IOException {
long length = parent.contentSize;
byte[] buffer = new byte[(int) length];
int read = stream.read(buffer);
if (read < length) {
throw new EOFException();
}
return buffer;
}
private long readEncodedNumber() throws IOException {
int value = stream.read();
if (value > 0) {
byte size = 1;
int mask = 0x80;
while (size < 9) {
if ((value & mask) == mask) {
mask = 0xFF;
mask >>= size;
long number = value & mask;
for (int i = 1; i < size; i++) {
value = stream.read();
number <<= 8;
number |= value;
}
return number;
}
mask >>= 1;
size++;
}
}
throw new IOException("Invalid encoded length");
}
private Element readElement() throws IOException {
Element elem = new Element();
elem.offset = stream.position();
elem.type = (int) readEncodedNumber();
elem.contentSize = readEncodedNumber();
elem.size = elem.contentSize + stream.position() - elem.offset;
return elem;
}
private Element readElement(int expected) throws IOException {
Element elem = readElement();
if (expected != 0 && elem.type != expected) {
throw new NoSuchElementException("expected " + elementID(expected) + " found " + elementID(elem.type));
}
return elem;
}
private Element untilElement(Element ref, int... expected) throws IOException {
Element elem;
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
elem = readElement();
for (int type : expected) {
if (elem.type == type) {
return elem;
}
}
ensure(elem);
}
return null;
}
private String elementID(long type) {
return "0x".concat(Long.toHexString(type));
}
private void ensure(Element ref) throws IOException {
long skip = (ref.offset + ref.size) - stream.position();
if (skip == 0) {
return;
} else if (skip < 0) {
throw new EOFException(String.format(
"parser go beyond limits of the Element. type=%s offset=%s size=%s position=%s",
elementID(ref.type), ref.offset, ref.size, stream.position()
));
}
stream.skipBytes(skip);
}
//</editor-fold>
//<editor-fold defaultState="collapsed" desc="elements readers">
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<WebMTrack> trackEntries = new ArrayList<>(2);
Element elem_trackEntry;
while ((elem_trackEntry = untilElement(ref, ID_TrackEntry)) != null) {
WebMTrack entry = new WebMTrack();
boolean drop = false;
Element elem;
while ((elem = untilElement(elem_trackEntry,
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
)) != null) {
switch (elem.type) {
case ID_TrackNumber:
entry.trackNumber = readNumber(elem);
break;
case ID_TrackType:
entry.trackType = (int)readNumber(elem);
break;
case ID_CodecID:
entry.codecId = readString(elem);
break;
case ID_CodecPrivate:
entry.codecPrivate = readBlob(elem);
break;
case ID_Audio:
case ID_Video:
entry.bMetadata = readBlob(elem);
break;
case ID_DefaultDuration:
entry.defaultDuration = readNumber(elem);
break;
case ID_FlagLacing:
drop = readNumber(elem) != lacingExpected;
break;
default:
System.out.println();
break;
}
ensure(elem);
}
if (!drop) {
trackEntries.add(entry);
}
ensure(elem_trackEntry);
}
WebMTrack[] entries = new WebMTrack[trackEntries.size()];
trackEntries.toArray(entries);
for (WebMTrack entry : entries) {
switch (entry.trackType) {
case 1:
entry.kind = TrackKind.Video;
break;
case 2:
entry.kind = TrackKind.Audio;
break;
default:
entry.kind = TrackKind.Other;
break;
}
}
return entries;
}
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
SimpleBlock obj = new SimpleBlock(ref);
obj.dataSize = stream.position();
obj.trackNumber = readEncodedNumber();
obj.relativeTimeCode = stream.readShort();
obj.flags = (byte) stream.read();
obj.dataSize = (ref.offset + ref.size) - stream.position();
if (obj.dataSize < 0) {
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
}
return obj;
}
private Cluster readCluster(Element ref) throws IOException {
Cluster obj = new Cluster(ref);
Element elem = untilElement(ref, ID_Timecode);
if (elem == null) {
throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + " without Timecode element");
}
obj.timecode = readNumber(elem);
return obj;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="class helpers">
class Element {
int type;
long offset;
long contentSize;
long size;
}
public class Info {
public long timecodeScale;
public long duration;
}
public class WebMTrack {
public long trackNumber;
protected int trackType;
public String codecId;
public byte[] codecPrivate;
public byte[] bMetadata;
public TrackKind kind;
public long defaultDuration;
}
public class Segment {
Segment(Element ref) {
this.ref = ref;
this.firstClusterInSegment = true;
}
public Info info;
WebMTrack[] tracks;
private Element currentCluster;
private final Element ref;
boolean firstClusterInSegment;
public Cluster getNextCluster() throws IOException {
if (done) {
return null;
}
if (firstClusterInSegment && segment.currentCluster != null) {
firstClusterInSegment = false;
return readCluster(segment.currentCluster);
}
ensure(segment.currentCluster);
Element elem = untilElement(segment.ref, ID_Cluster);
if (elem == null) {
return null;
}
segment.currentCluster = elem;
return readCluster(segment.currentCluster);
}
}
public class SimpleBlock {
public TrackDataChunk data;
SimpleBlock(Element ref) {
this.ref = ref;
}
public long trackNumber;
public short relativeTimeCode;
public byte flags;
public long dataSize;
private final Element ref;
public boolean isKeyframe() {
return (flags & 0x80) == 0x80;
}
}
public class Cluster {
Element ref;
SimpleBlock currentSimpleBlock = null;
public long timecode;
Cluster(Element ref) {
this.ref = ref;
}
boolean check() {
return stream.position() >= (ref.offset + ref.size);
}
public SimpleBlock getNextSimpleBlock() throws IOException {
if (check()) {
return null;
}
if (currentSimpleBlock != null) {
ensure(currentSimpleBlock.ref);
}
while (!check()) {
Element elem = untilElement(ref, ID_SimpleBlock);
if (elem == null) {
return null;
}
currentSimpleBlock = readSimpleBlock(elem);
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
return currentSimpleBlock;
}
ensure(elem);
}
return null;
}
}
//</editor-fold>
}

View File

@ -0,0 +1,728 @@
package org.schabi.newpipe.extractor.utils;
import org.schabi.newpipe.extractor.utils.WebMReader.Cluster;
import org.schabi.newpipe.extractor.utils.WebMReader.Segment;
import org.schabi.newpipe.extractor.utils.WebMReader.SimpleBlock;
import org.schabi.newpipe.extractor.utils.WebMReader.WebMTrack;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import org.schabi.newpipe.extractor.utils.io.SharpStream;
/**
*
* @author kapodamy
*/
public class WebMWriter {
private final static int BUFFER_SIZE = 8 * 1024;
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
private final static int INTERV = 100;// 100ms on 1000000us timecode scale
private final static int DEFAULT_CUES_EACH_MS = 5000;// 100ms on 1000000us timecode scale
private WebMReader.WebMTrack[] infoTracks;
private SharpStream[] sourceTracks;
private WebMReader[] readers;
private boolean done = false;
private boolean parsed = false;
private long written = 0;
private Segment[] readersSegment;
private Cluster[] readersCluter;
private int[] predefinedDurations;
private byte[] outBuffer;
public WebMWriter(SharpStream... source) {
sourceTracks = source;
readers = new WebMReader[sourceTracks.length];
infoTracks = new WebMTrack[sourceTracks.length];
outBuffer = new byte[BUFFER_SIZE];
}
public WebMTrack[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
return readers[sourceIndex].getAvailableTracks();
}
public void parseSources() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
for (int i = 0; i < readers.length; i++) {
readers[i] = new WebMReader(sourceTracks[i]);
readers[i].parse();
}
} finally {
parsed = true;
}
}
public void selectTracks(int... trackIndex) throws IOException {
try {
readersSegment = new Segment[readers.length];
readersCluter = new Cluster[readers.length];
predefinedDurations = new int[readers.length];
for (int i = 0; i < readers.length; i++) {
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
predefinedDurations[i] = -1;
readersSegment[i] = readers[i].getNextSegment();
}
} finally {
parsed = true;
}
}
public long getBytesWritten() {
return written;
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public void close() {
done = true;
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
}
sourceTracks = null;
readers = null;
infoTracks = null;
readersSegment = null;
readersCluter = null;
outBuffer = null;
}
public void build(SharpStream out) throws IOException, RuntimeException {
if (!out.canRewind()) {
throw new IOException("The output stream must be allow seek");
}
makeEBML(out);
long offsetSegmentSizeSet = written + 5;
long offsetInfoDurationSet = written + 94;
long offsetClusterSet = written + 58;
long offsetCuesSet = written + 75;
ArrayList<byte[]> listBuffer = new ArrayList<>(4);
/* segment */
listBuffer.add(new byte[]{
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
});
long baseSegmentOffset = written + listBuffer.get(0).length;
/* seek head */
listBuffer.add(new byte[]{
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
0x4d, (byte) 0xbb, (byte) 0x8b,
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
/*tracks offset*/ 0x6a,
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
});
/* info */
listBuffer.add(new byte[]{
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
});
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
0x00, 0x00, 0x00, 0x00,// info.duration
/* MuxingApp */
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
/* WritingApp */
0x57, 0x41, (byte) 0x87, 0x4E,
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
});
/* tracks */
listBuffer.addAll(makeTracks());
for (byte[] buff : listBuffer) {
dump(buff, out);
}
// reserve space for Cues element, but is a waste of space (actually is 64 KiB)
// TODO: better Cue maker
long cueReservedOffset = written;
dump(new byte[]{(byte) 0xec, 0x20, (byte) 0xff, (byte) 0xfb}, out);
int reserved = (1024 * 63) - 4;
while (reserved > 0) {
int write = Math.min(reserved, outBuffer.length);
out.write(outBuffer, 0, write);
reserved -= write;
written += write;
}
// Select a track for the cue
int cuesForTrackId = selectTrackForCue();
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
long duration = 0;
int durationFromTrackId = 0;
byte[] bTimecode = makeTimecode(0);
int firstClusterOffset = (int) written;
long currentClusterOffset = makeCluster(out, bTimecode, 0, clusterOffsets, clusterSizes);
long baseTimecode = 0;
long limitTimecode = -1;
int limitTimecodeByTrackId = cuesForTrackId;
int blockWritten = Integer.MAX_VALUE;
int newClusterByTrackId = -1;
while (blockWritten > 0) {
blockWritten = 0;
int i = 0;
while (i < readers.length) {
Block bloq = getNextBlockFrom(i);
if (bloq == null) {
i++;
continue;
}
if (bloq.data == null) {
blockWritten = 1;// fake block
newClusterByTrackId = i;
i++;
continue;
}
if (newClusterByTrackId == i) {
limitTimecodeByTrackId = i;
newClusterByTrackId = -1;
baseTimecode = bloq.absoluteTimecode;
limitTimecode = baseTimecode + INTERV;
bTimecode = makeTimecode(baseTimecode);
currentClusterOffset = makeCluster(out, bTimecode, currentClusterOffset, clusterOffsets, clusterSizes);
}
if (cuesForTrackId == i) {
if ((nextCueTime > -1 && bloq.absoluteTimecode >= nextCueTime) || (nextCueTime < 0 && bloq.isKeyframe())) {
if (nextCueTime > -1) {
nextCueTime += DEFAULT_CUES_EACH_MS;
}
keyFrames.add(
new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
);
}
}
writeBlock(out, bloq, baseTimecode);
blockWritten++;
if (bloq.absoluteTimecode > duration) {
duration = bloq.absoluteTimecode;
durationFromTrackId = bloq.trackNumber;
}
if (limitTimecode < 0) {
limitTimecode = bloq.absoluteTimecode + INTERV;
continue;
}
if (bloq.absoluteTimecode >= limitTimecode) {
if (limitTimecodeByTrackId != i) {
limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode);
}
i++;
}
}
}
makeCluster(out, null, currentClusterOffset, null, clusterSizes);
long segmentSize = written - offsetSegmentSizeSet - 7;
// final step write offsets and sizes
out.rewind();
written = 0;
skipTo(out, offsetSegmentSizeSet);
writeLong(out, segmentSize);
if (predefinedDurations[durationFromTrackId] > -1) {
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
}
skipTo(out, offsetInfoDurationSet);
writeFloat(out, duration);
firstClusterOffset -= baseSegmentOffset;
skipTo(out, offsetClusterSet);
writeInt(out, firstClusterOffset);
skipTo(out, cueReservedOffset);
/* Cue */
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
for (KeyFrame keyFrame : keyFrames) {
for (byte[] buffer : makeCuePoint(cuesForTrackId, keyFrame)) {
dump(buffer, out);
if (written >= (cueReservedOffset + 65535 - 16)) {
throw new IOException("Too many Cues");
}
}
}
short cueSize = (short) (written - cueReservedOffset - 7);
/* EBML Void */
ByteBuffer voidBuffer = ByteBuffer.allocate(4);
voidBuffer.putShort((short) 0xec20);
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
dump(voidBuffer.array(), out);
out.rewind();
written = 0;
skipTo(out, offsetCuesSet);
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
skipTo(out, cueReservedOffset + 5);
writeShort(out, cueSize);
for (int i = 0; i < clusterSizes.size(); i++) {
skipTo(out, clusterOffsets.get(i));
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
out.write(size, 1, 3);
written += 3;
}
}
private Block getNextBlockFrom(int internalTrackId) throws IOException {
if (readersSegment[internalTrackId] == null) {
readersSegment[internalTrackId] = readers[internalTrackId].getNextSegment();
if (readersSegment[internalTrackId] == null) {
return null;// no more blocks in the selected track
}
}
if (readersCluter[internalTrackId] == null) {
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
if (readersCluter[internalTrackId] == null) {
readersSegment[internalTrackId] = null;
return getNextBlockFrom(internalTrackId);
}
}
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
if (res == null) {
readersCluter[internalTrackId] = null;
return new Block();// fake block to indicate the end of the cluster
}
Block bloq = new Block();
bloq.data = res.data;
bloq.dataSize = (int) res.dataSize;
bloq.trackNumber = internalTrackId;
bloq.flags = res.flags;
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE);
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
return bloq;
}
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
return (short) (time * (newTimeScale / oldTimeScale));
}
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
absoluteOffset -= written;
written += absoluteOffset;
stream.skip(absoluteOffset);
}
private void writeLong(SharpStream stream, long number) throws IOException {
byte[] buffer = ByteBuffer.allocate(DataReader.LongSize).putLong(number).array();
stream.write(buffer, 1, buffer.length - 1);
written += buffer.length - 1;
}
private void writeFloat(SharpStream stream, float number) throws IOException {
byte[] buffer = ByteBuffer.allocate(DataReader.FloatSize).putFloat(number).array();
dump(buffer, stream);
}
private void writeShort(SharpStream stream, short number) throws IOException {
byte[] buffer = ByteBuffer.allocate(DataReader.ShortSize).putShort(number).array();
dump(buffer, stream);
}
private void writeInt(SharpStream stream, int number) throws IOException {
byte[] buffer = ByteBuffer.allocate(DataReader.IntegerSize).putInt(number).array();
dump(buffer, stream);
}
private void writeBlock(SharpStream stream, Block bloq, long clusterTimecode) throws IOException {
long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode;
if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) {
throw new IndexOutOfBoundsException("SimpleBlock timecode overflow.");
}
ArrayList<byte[]> 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.ShortSize).putShort((short) relativeTimeCode).array());
listBuffer.add(new byte[]{bloq.flags});
int blockSize = bloq.dataSize;
for (int i = 2; i < listBuffer.size(); i++) {
blockSize += listBuffer.get(i).length;
}
listBuffer.set(1, encode(blockSize, false));
for (byte[] buff : listBuffer) {
dump(buff, stream);
}
int read;
while ((read = bloq.data.read(outBuffer)) > 0) {
stream.write(outBuffer, 0, read);
written += read;
}
}
private byte[] makeTimecode(long timecode) {
ByteBuffer buffer = ByteBuffer.allocate(9);
buffer.put((byte) 0xe7);
buffer.put(encode(timecode, true));
byte[] res = new byte[buffer.position()];
System.arraycopy(buffer.array(), 0, res, 0, res.length);
return res;
}
private long makeCluster(SharpStream stream, byte[] bTimecode, long startOffset, ArrayList<Long> clusterOffsets, ArrayList<Integer> clusterSizes) throws IOException {
if (startOffset > 0) {
clusterSizes.add((int) (written - startOffset));// size for last offset
}
if (clusterOffsets != null) {
/* cluster */
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
dump(new byte[]{0x20, 0x00, 0x00}, stream);
startOffset = written;// size for the this cluster
dump(bTimecode, stream);
return startOffset;
}
return -1;
}
private void makeEBML(SharpStream stream) throws IOException {
// deafult values
dump(new byte[]{
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
0x42, (byte) 0x85, (byte) 0x81, 0x02
}, stream);
}
private ArrayList<byte[]> makeTracks() {
ArrayList<byte[]> 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<byte[]> makeTrackEntry(int internalTrackId, WebMTrack track) {
byte[] id = encode(internalTrackId + 1, true);
ArrayList<byte[]> buffer = new ArrayList<>(12);
/* track */
buffer.add(new byte[]{(byte) 0xae});
buffer.add(null);
/* track number */
buffer.add(new byte[]{(byte) 0xd7});
buffer.add(id);
/* track uid */
buffer.add(new byte[]{0x73, (byte) 0xc5});
buffer.add(id);
/* flag lacing */
buffer.add(new byte[]{(byte) 0x9c, (byte) 0x81, 0x00});
/* lang */
buffer.add(new byte[]{0x22, (byte) 0xb5, (byte) 0x9c, (byte) 0x83, 0x75, 0x6e, 0x64});
/* codec id */
buffer.add(new byte[]{(byte) 0x86});
buffer.addAll(encode(track.codecId));
/* type */
buffer.add(new byte[]{(byte) 0x83});
buffer.add(encode(track.trackType, true));
/* default duration */
if (track.defaultDuration != 0) {
predefinedDurations[internalTrackId] = (int) Math.ceil(track.defaultDuration / (float) DEFAULT_TIMECODE_SCALE);
buffer.add(new byte[]{0x23, (byte) 0xe3, (byte) 0x83});
buffer.add(encode(track.defaultDuration, true));
}
/* audio/video */
if ((track.trackType == 1 || track.trackType == 2) && valid(track.bMetadata)) {
buffer.add(new byte[]{(byte) (track.trackType == 1 ? 0xe0 : 0xe1)});
buffer.add(encode(track.bMetadata.length, false));
buffer.add(track.bMetadata);
}
/* codec private*/
if (valid(track.codecPrivate)) {
buffer.add(new byte[]{0x63, (byte) 0xa2});
buffer.add(encode(track.codecPrivate.length, false));
buffer.add(track.codecPrivate);
}
return lengthFor(buffer);
}
private ArrayList<byte[]> makeCuePoint(int internalTrackId, KeyFrame keyFrame) {
ArrayList<byte[]> buffer = new ArrayList<>(5);
/* CuePoint */
buffer.add(new byte[]{(byte) 0xbb});
buffer.add(null);
/* CueTime */
buffer.add(new byte[]{(byte) 0xb3});
buffer.add(encode(keyFrame.atTimecode, true));
/* CueTrackPosition */
buffer.addAll(makeCueTrackPosition(internalTrackId, keyFrame));
return lengthFor(buffer);
}
private ArrayList<byte[]> makeCueTrackPosition(int internalTrackId, KeyFrame keyFrame) {
ArrayList<byte[]> buffer = new ArrayList<>(8);
/* CueTrackPositions */
buffer.add(new byte[]{(byte) 0xb7});
buffer.add(null);
/* CueTrack */
buffer.add(new byte[]{(byte) 0xf7});
buffer.add(encode(internalTrackId + 1, true));
/* CueClusterPosition */
buffer.add(new byte[]{(byte) 0xf1});
buffer.add(encode(keyFrame.atCluster, true));
/* CueRelativePosition */
if (keyFrame.atBlock > 0) {
buffer.add(new byte[]{(byte) 0xf0});
buffer.add(encode(keyFrame.atBlock, true));
}
return lengthFor(buffer);
}
private void dump(byte[] buffer, SharpStream stream) throws IOException {
stream.write(buffer);
written += buffer.length;
}
private ArrayList<byte[]> lengthFor(ArrayList<byte[]> buffer) {
long size = 0;
for (int i = 2; i < buffer.size(); i++) {
size += buffer.get(i).length;
}
buffer.set(1, encode(size, false));
return buffer;
}
private byte[] encode(long number, boolean withLength) {
int length = -1;
for (int i = 1; i <= 7; i++) {
if (number < Math.pow(2, 7 * i)) {
length = i;
break;
}
}
if (length < 1) {
throw new ArithmeticException("Can't encode a number of bigger than 7 bytes");
}
if (number == (Math.pow(2, 7 * length)) - 1) {
length++;
}
int offset = withLength ? 1 : 0;
byte[] buffer = new byte[offset + length];
long marker = (long) Math.floor((length - 1) / 8);
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
long b = (long) Math.floor(number / mul);
if (!withLength && i == marker) {
b = b | (0x80 >> (length - 1));
}
buffer[offset + i] = (byte) b;
}
if (withLength) {
buffer[0] = (byte) (0x80 | length);
}
return buffer;
}
private ArrayList<byte[]> encode(String value) {
byte[] str;
try {
str = value.getBytes("utf-8");
} catch (UnsupportedEncodingException err) {
str = value.getBytes();
}
ArrayList<byte[]> buffer = new ArrayList<>(2);
buffer.add(encode(str.length, false));
buffer.add(str);
return buffer;
}
private boolean valid(byte[] buffer) {
return buffer != null && buffer.length > 0;
}
private int selectTrackForCue() {
int i = 0;
int videoTracks = 0;
int audioTracks = 0;
for (; i < infoTracks.length; i++) {
switch (infoTracks[i].trackType) {
case 1:
videoTracks++;
break;
case 2:
audioTracks++;
break;
}
}
int kind;
if (audioTracks == infoTracks.length) {
kind = 2;
} else if (videoTracks == infoTracks.length) {
kind = 1;
} else if (videoTracks > 0) {
kind = 1;
} else if (audioTracks > 0) {
kind = 2;
} else {
return 0;
}
// TODO: in the adove code, find and select the shortest track for the desired kind
for (i = 0; i < infoTracks.length; i++) {
if (kind == infoTracks[i].trackType) {
return i;
}
}
return 0;
}
class KeyFrame {
KeyFrame(long segment, long cluster, long block, int bTimecodeLength, long timecode) {
atCluster = cluster - segment;
if ((block - bTimecodeLength) > cluster) {
atBlock = (int) (block - cluster);
}
atTimecode = timecode;
}
long atCluster;
int atBlock;
long atTimecode;
}
class Block {
InputStream data;
int trackNumber;
byte flags;
int dataSize;
long absoluteTimecode;
boolean isKeyframe() {
return (flags & 0x80) == 0x80;
}
@Override
public String toString() {
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
}
}
}

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.extractor.utils.io;
import java.io.IOException;
/**
* based c#
*/
public abstract class SharpStream {
public abstract int read() throws IOException;
public abstract int read(byte buffer[]) throws IOException;
public abstract int read(byte buffer[], int offset, int count) throws IOException;
public abstract long skip(long amount) throws IOException;
public abstract int available();
public abstract void rewind() throws IOException;
public abstract void dispose();
public abstract boolean isDisposed();
public abstract boolean canRewind();
public abstract boolean canRead();
public abstract boolean canWrite();
public abstract void write(byte value) throws IOException;
public abstract void write(byte[] buffer) throws IOException;
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
public abstract void flush() throws IOException;
public void setLength(long length) throws IOException {
throw new IOException("Not implemented");
}
}