2020-04-02 13:42:28 +00:00
|
|
|
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<ClusterInfo> 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() {
|
|
|
|
done = true;
|
|
|
|
parsed = true;
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
for (final SharpStream src : sourceTracks) {
|
2020-04-02 13:42:28 +00:00
|
|
|
src.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
sourceTracks = null;
|
|
|
|
readers = null;
|
|
|
|
infoTracks = null;
|
|
|
|
readersSegment = null;
|
|
|
|
readersCluster = null;
|
|
|
|
outBuffer = null;
|
|
|
|
outByteBuffer = null;
|
|
|
|
clustersOffsetsSizes = null;
|
|
|
|
}
|
|
|
|
|
2022-03-18 22:57:11 +00:00
|
|
|
@SuppressWarnings("MethodLength")
|
2020-04-02 13:42:28 +00:00
|
|
|
public void build(final SharpStream out) throws IOException, RuntimeException {
|
|
|
|
if (!out.canRewind()) {
|
|
|
|
throw new IOException("The output stream must be allow seek");
|
|
|
|
}
|
|
|
|
|
|
|
|
makeEBML(out);
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final long offsetSegmentSizeSet = written + 5;
|
|
|
|
final long offsetInfoDurationSet = written + 94;
|
|
|
|
final long offsetClusterSet = written + 58;
|
|
|
|
final long offsetCuesSet = written + 75;
|
2020-04-02 13:42:28 +00:00
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<byte[]> listBuffer = new ArrayList<>(4);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
/* segment */
|
|
|
|
listBuffer.add(new byte[]{
|
|
|
|
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
|
|
|
});
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final long segmentOffset = written + listBuffer.get(0).length;
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
/* 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,
|
2020-07-06 02:55:40 +00:00
|
|
|
(byte) 0xac, (byte) 0x81,
|
|
|
|
/*info offset*/ 0x43,
|
2020-04-02 13:42:28 +00:00
|
|
|
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
|
|
|
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
2020-07-03 05:07:42 +00:00
|
|
|
/*tracks offset*/ 0x56,
|
2020-04-02 13:42:28 +00:00
|
|
|
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
2020-07-06 02:55:40 +00:00
|
|
|
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84,
|
|
|
|
/*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
2020-04-02 13:42:28 +00:00
|
|
|
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
2020-07-06 02:55:40 +00:00
|
|
|
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84,
|
|
|
|
/*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
2020-04-02 13:42:28 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
/* info */
|
|
|
|
listBuffer.add(new byte[]{
|
2020-07-03 05:07:42 +00:00
|
|
|
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0x8e, 0x2a, (byte) 0xd7, (byte) 0xb1
|
2020-04-02 13:42:28 +00:00
|
|
|
});
|
2020-07-06 02:55:40 +00:00
|
|
|
// the segment duration MUST NOT exceed 4 bytes
|
|
|
|
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));
|
2020-04-02 13:42:28 +00:00
|
|
|
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
|
|
|
|
0x00, 0x00, 0x00, 0x00, // info.duration
|
|
|
|
});
|
|
|
|
|
|
|
|
/* tracks */
|
|
|
|
listBuffer.addAll(makeTracks());
|
|
|
|
|
|
|
|
dump(listBuffer, out);
|
|
|
|
|
|
|
|
// reserve space for Cues element
|
2020-08-16 08:24:58 +00:00
|
|
|
final long cueOffset = written;
|
2020-04-02 13:42:28 +00:00
|
|
|
makeEbmlVoid(out, CUE_RESERVE_SIZE, true);
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final int[] defaultSampleDuration = new int[infoTracks.length];
|
|
|
|
final long[] duration = new long[infoTracks.length];
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
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
|
2020-08-16 08:24:58 +00:00
|
|
|
final int cuesForTrackId = selectTrackForCue();
|
2020-04-02 13:42:28 +00:00
|
|
|
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
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) {
|
2020-08-16 08:24:58 +00:00
|
|
|
final Block bloq = getNextBlockFrom(i);
|
2020-04-02 13:42:28 +00:00
|
|
|
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);
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final long segmentSize = written - offsetSegmentSizeSet - 7;
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
/* 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
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
for (final KeyFrame keyFrame : keyFrames) {
|
|
|
|
final int size = makeCuePoint(cuesForTrackId, keyFrame, outBuffer);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
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));
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
for (final ClusterInfo cluster : clustersOffsetsSizes) {
|
2020-04-02 13:42:28 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock();
|
2020-04-02 13:42:28 +00:00
|
|
|
if (res == null) {
|
|
|
|
readersCluster[internalTrackId] = null;
|
|
|
|
return new Block(); // fake block to indicate the end of the cluster
|
|
|
|
}
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final Block bloq = new Block();
|
2020-04-02 13:42:28 +00:00
|
|
|
bloq.data = res.data;
|
2020-04-08 15:08:01 +00:00
|
|
|
bloq.dataSize = res.dataSize;
|
2020-04-02 13:42:28 +00:00
|
|
|
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 {
|
2020-08-16 08:24:58 +00:00
|
|
|
final long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode;
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) {
|
|
|
|
throw new IndexOutOfBoundsException("SimpleBlock timecode overflow.");
|
|
|
|
}
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<byte[]> listBuffer = new ArrayList<>(5);
|
2020-04-02 13:42:28 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-03 23:51:45 +00:00
|
|
|
private long makeCluster(final SharpStream stream, final long timecode, final long offsetStart,
|
2020-04-02 13:42:28 +00:00
|
|
|
final boolean create) throws IOException {
|
|
|
|
ClusterInfo cluster;
|
2020-07-03 23:51:45 +00:00
|
|
|
long offset = offsetStart;
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-07-03 23:51:45 +00:00
|
|
|
offset = written;
|
|
|
|
|
2020-04-02 13:42:28 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-07-03 23:51:45 +00:00
|
|
|
return offset;
|
2020-04-02 13:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void makeEBML(final SharpStream stream) throws IOException {
|
2020-07-03 23:51:45 +00:00
|
|
|
// default values
|
2020-04-02 13:42:28 +00:00
|
|
|
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() {
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<byte[]> buffer = new ArrayList<>(1);
|
2020-04-02 13:42:28 +00:00
|
|
|
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(final int internalTrackId, final WebMTrack track) {
|
2020-08-16 08:24:58 +00:00
|
|
|
final byte[] id = encode(internalTrackId + 1, true);
|
|
|
|
final ArrayList<byte[]> buffer = new ArrayList<>(12);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
/* 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) {
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<byte[]> cue = new ArrayList<>(5);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
/* 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);
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
for (final byte[] buff : cue) {
|
2020-04-02 13:42:28 +00:00
|
|
|
System.arraycopy(buff, 0, buffer, size, buff.length);
|
|
|
|
size += buff.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
private ArrayList<byte[]> makeCueTrackPosition(final int internalTrackId,
|
|
|
|
final KeyFrame keyFrame) {
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<byte[]> buffer = new ArrayList<>(8);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
/* 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);
|
|
|
|
}
|
|
|
|
|
2020-07-03 05:07:42 +00:00
|
|
|
private void makeEbmlVoid(final SharpStream out, final int amount, final boolean wipe)
|
2020-04-02 13:42:28 +00:00
|
|
|
throws IOException {
|
2020-07-03 05:07:42 +00:00
|
|
|
int size = amount;
|
|
|
|
|
2020-04-02 13:42:28 +00:00
|
|
|
/* ebml void */
|
|
|
|
outByteBuffer.putShort(0, (short) 0xec20);
|
|
|
|
outByteBuffer.putShort(2, (short) (size - 4));
|
|
|
|
|
|
|
|
dump(outBuffer, 4, out);
|
|
|
|
|
|
|
|
if (wipe) {
|
|
|
|
size -= 4;
|
|
|
|
while (size > 0) {
|
2020-08-16 08:24:58 +00:00
|
|
|
final int write = Math.min(size, outBuffer.length);
|
2020-04-02 13:42:28 +00:00
|
|
|
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<byte[]> buffers, final SharpStream stream)
|
|
|
|
throws IOException {
|
2020-08-16 08:24:58 +00:00
|
|
|
for (final byte[] buffer : buffers) {
|
2020-04-02 13:42:28 +00:00
|
|
|
stream.write(buffer);
|
|
|
|
written += buffer.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private ArrayList<byte[]> lengthFor(final 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(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++;
|
|
|
|
}
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final int offset = withLength ? 1 : 0;
|
|
|
|
final byte[] buffer = new byte[offset + length];
|
2022-08-15 15:31:12 +00:00
|
|
|
final long marker = Math.floorDiv(length - 1, 8);
|
2020-04-02 13:42:28 +00:00
|
|
|
|
|
|
|
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<byte[]> encode(final String value) {
|
2020-08-16 08:24:58 +00:00
|
|
|
final byte[] str = value.getBytes(StandardCharsets.UTF_8); // or use "utf-8"
|
2020-04-02 13:42:28 +00:00
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final ArrayList<byte[]> buffer = new ArrayList<>(2);
|
2020-04-02 13:42:28 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-16 08:24:58 +00:00
|
|
|
final int kind;
|
2020-04-02 13:42:28 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-05-15 15:48:16 +00:00
|
|
|
// TODO: in the above code, find and select the shortest track for the desired kind
|
2020-04-02 13:42:28 +00:00
|
|
|
for (i = 0; i < infoTracks.length; i++) {
|
|
|
|
if (kind == infoTracks[i].trackType) {
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-04-08 15:08:01 +00:00
|
|
|
static class KeyFrame {
|
2020-04-02 13:42:28 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-04-08 15:08:01 +00:00
|
|
|
static class Block {
|
2020-04-02 13:42:28 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-08 15:08:01 +00:00
|
|
|
static class ClusterInfo {
|
2020-04-02 13:42:28 +00:00
|
|
|
long offset;
|
|
|
|
int size;
|
|
|
|
}
|
|
|
|
}
|