Merged the latest changes

This commit is contained in:
Avently 2020-07-13 04:17:21 +03:00
commit d2aaa6f691
1254 changed files with 39193 additions and 18652 deletions

View file

@ -1,254 +1,266 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author kapodamy
*/
public class DataReader {
public final static int SHORT_SIZE = 2;
public final static int LONG_SIZE = 8;
public final static int INTEGER_SIZE = 4;
public final static int FLOAT_SIZE = 4;
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
private long position = 0;
private final SharpStream stream;
private InputStream view;
private int viewSize;
public DataReader(SharpStream stream) {
this.stream = stream;
this.readOffset = this.readBuffer.length;
}
public long position() {
return position;
}
public int read() throws IOException {
if (fillBuffer()) {
return -1;
}
position++;
readCount--;
return readBuffer[readOffset++] & 0xFF;
}
public long skipBytes(long amount) throws IOException {
if (readCount < 0) {
return 0;
} else if (readCount == 0) {
amount = stream.skip(amount);
} else {
if (readCount > amount) {
readCount -= (int) amount;
readOffset += (int) amount;
} else {
amount = readCount + stream.skip(amount - readCount);
readCount = 0;
readOffset = readBuffer.length;
}
}
position += amount;
return amount;
}
public int readInt() throws IOException {
primitiveRead(INTEGER_SIZE);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
}
public short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public long readLong() throws IOException {
primitiveRead(LONG_SIZE);
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
return high << 32 | low;
}
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
public int read(byte[] buffer, int offset, int count) throws IOException {
if (readCount < 0) {
return -1;
}
int total = 0;
if (count >= readBuffer.length) {
if (readCount > 0) {
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
readOffset += readCount;
offset += readCount;
count -= readCount;
total = readCount;
readCount = 0;
}
total += Math.max(stream.read(buffer, offset, count), 0);
} else {
while (count > 0 && !fillBuffer()) {
int read = Math.min(readCount, count);
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
readOffset += read;
readCount -= read;
offset += read;
count -= read;
total += read;
}
}
position += total;
return total;
}
public boolean available() {
return readCount > 0 || stream.available() > 0;
}
public void rewind() throws IOException {
stream.rewind();
if ((position - viewSize) > 0) {
viewSize = 0;// drop view
} else {
viewSize += position;
}
position = 0;
readOffset = readBuffer.length;
}
public boolean canRewind() {
return stream.canRewind();
}
/**
* Wraps this instance of {@code DataReader} into {@code InputStream}
* object. Note: Any read in the {@code DataReader} will not modify
* (decrease) the view size
*
* @param size the size of the view
* @return the view
*/
public InputStream getView(int size) {
if (view == null) {
view = new InputStream() {
@Override
public int read() throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read();
if (res > 0) {
viewSize--;
}
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 {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
viewSize -= res;
return res;
}
@Override
public long skip(long amount) throws IOException {
if (viewSize < 1) {
return 0;
}
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
viewSize -= res;
return res;
}
@Override
public int available() {
return viewSize;
}
@Override
public void close() {
viewSize = 0;
}
@Override
public boolean markSupported() {
return false;
}
};
}
viewSize = size;
return view;
}
private final short[] primitive = new short[LONG_SIZE];
private void primitiveRead(int amount) throws IOException {
byte[] buffer = new byte[amount];
int read = read(buffer, 0, amount);
if (read != amount) {
throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes");
}
for (int i = 0; i < amount; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
}
}
private final byte[] readBuffer = new byte[BUFFER_SIZE];
private int readOffset;
private int readCount;
private boolean fillBuffer() throws IOException {
if (readCount < 0) {
return true;
}
if (readOffset >= readBuffer.length) {
readCount = stream.read(readBuffer);
if (readCount < 1) {
readCount = -1;
return true;
}
readOffset = 0;
}
return readCount < 1;
}
}
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author kapodamy
*/
public class DataReader {
public static final int SHORT_SIZE = 2;
public static final int LONG_SIZE = 8;
public static final int INTEGER_SIZE = 4;
public static final int FLOAT_SIZE = 4;
private static final int BUFFER_SIZE = 128 * 1024; // 128 KiB
private long position = 0;
private final SharpStream stream;
private InputStream view;
private int viewSize;
public DataReader(final SharpStream stream) {
this.stream = stream;
this.readOffset = this.readBuffer.length;
}
public long position() {
return position;
}
public int read() throws IOException {
if (fillBuffer()) {
return -1;
}
position++;
readCount--;
return readBuffer[readOffset++] & 0xFF;
}
public long skipBytes(final long byteAmount) throws IOException {
long amount = byteAmount;
if (readCount < 0) {
return 0;
} else if (readCount == 0) {
amount = stream.skip(amount);
} else {
if (readCount > amount) {
readCount -= (int) amount;
readOffset += (int) amount;
} else {
amount = readCount + stream.skip(amount - readCount);
readCount = 0;
readOffset = readBuffer.length;
}
}
position += amount;
return amount;
}
public int readInt() throws IOException {
primitiveRead(INTEGER_SIZE);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
}
public long readUnsignedInt() throws IOException {
long value = readInt();
return value & 0xffffffffL;
}
public short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public long readLong() throws IOException {
primitiveRead(LONG_SIZE);
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
return high << 32 | low;
}
public int read(final byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
public int read(final byte[] buffer, final int off, final int c) throws IOException {
int offset = off;
int count = c;
if (readCount < 0) {
return -1;
}
int total = 0;
if (count >= readBuffer.length) {
if (readCount > 0) {
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
readOffset += readCount;
offset += readCount;
count -= readCount;
total = readCount;
readCount = 0;
}
total += Math.max(stream.read(buffer, offset, count), 0);
} else {
while (count > 0 && !fillBuffer()) {
int read = Math.min(readCount, count);
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
readOffset += read;
readCount -= read;
offset += read;
count -= read;
total += read;
}
}
position += total;
return total;
}
public boolean available() {
return readCount > 0 || stream.available() > 0;
}
public void rewind() throws IOException {
stream.rewind();
if ((position - viewSize) > 0) {
viewSize = 0; // drop view
} else {
viewSize += position;
}
position = 0;
readOffset = readBuffer.length;
readCount = 0;
}
public boolean canRewind() {
return stream.canRewind();
}
/**
* Wraps this instance of {@code DataReader} into {@code InputStream}
* object. Note: Any read in the {@code DataReader} will not modify
* (decrease) the view size
*
* @param size the size of the view
* @return the view
*/
public InputStream getView(final int size) {
if (view == null) {
view = new InputStream() {
@Override
public int read() throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read();
if (res > 0) {
viewSize--;
}
return res;
}
@Override
public int read(final byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(final byte[] buffer, final int offset, final int count)
throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
viewSize -= res;
return res;
}
@Override
public long skip(final long amount) throws IOException {
if (viewSize < 1) {
return 0;
}
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
viewSize -= res;
return res;
}
@Override
public int available() {
return viewSize;
}
@Override
public void close() {
viewSize = 0;
}
@Override
public boolean markSupported() {
return false;
}
};
}
viewSize = size;
return view;
}
private final short[] primitive = new short[LONG_SIZE];
private void primitiveRead(final int amount) throws IOException {
byte[] buffer = new byte[amount];
int read = read(buffer, 0, amount);
if (read != amount) {
throw new EOFException("Truncated stream, missing "
+ String.valueOf(amount - read) + " bytes");
}
for (int i = 0; i < amount; i++) {
// the "byte" data type in java is signed and is very annoying
primitive[i] = (short) (buffer[i] & 0xFF);
}
}
private final byte[] readBuffer = new byte[BUFFER_SIZE];
private int readOffset;
private int readCount;
private boolean fillBuffer() throws IOException {
if (readCount < 0) {
return true;
}
if (readOffset >= readBuffer.length) {
readCount = stream.read(readBuffer);
if (readCount < 1) {
readCount = -1;
return true;
}
readOffset = 0;
}
return readCount < 1;
}
}

File diff suppressed because it is too large Load diff

View file

@ -5,25 +5,27 @@ import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
import org.schabi.newpipe.streams.Mp4DashReader.TrackKind;
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* @author kapodamy
*/
public class Mp4FromDashWriter {
private final static int EPOCH_OFFSET = 2082844800;
private final static short DEFAULT_TIMESCALE = 1000;
private final static byte SAMPLES_PER_CHUNK_INIT = 2;
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
private final static short SINGLE_CHUNK_SAMPLE_BUFFER = 256;
private static final int EPOCH_OFFSET = 2082844800;
private static final short DEFAULT_TIMESCALE = 1000;
private static final byte SAMPLES_PER_CHUNK_INIT = 2;
// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
private static final byte SAMPLES_PER_CHUNK = 6;
// near 3.999 GiB
private static final long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;
// 2.2 MiB enough for: 1080p 60fps 00h35m00s
private static final int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024);
private final long time;
@ -46,7 +48,9 @@ public class Mp4FromDashWriter {
private int overrideMainBrand = 0x00;
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
private final ArrayList<Integer> compatibleBrands = new ArrayList<>(5);
public Mp4FromDashWriter(final SharpStream... sources) throws IOException {
for (SharpStream src : sources) {
if (!src.canRewind() && !src.canRead()) {
throw new IOException("All sources must be readable and allow rewind");
@ -57,9 +61,13 @@ public class Mp4FromDashWriter {
readers = new Mp4DashReader[sourceTracks.length];
readersChunks = new Mp4DashChunk[readers.length];
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
compatibleBrands.add(0x6D703431); // mp41
compatibleBrands.add(0x69736F6D); // isom
compatibleBrands.add(0x69736F32); // iso2
}
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
public Mp4Track[] getTracksFromSource(final int sourceIndex) throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
@ -86,7 +94,7 @@ public class Mp4FromDashWriter {
}
}
public void selectTracks(int... trackIndex) throws IOException {
public void selectTracks(final int... trackIndex) throws IOException {
if (done) {
throw new IOException("already done");
}
@ -104,8 +112,8 @@ public class Mp4FromDashWriter {
}
}
public void setMainBrand(int brandId) {
overrideMainBrand = brandId;
public void setMainBrand(final int brand) {
overrideMainBrand = brand;
}
public boolean isDone() {
@ -134,7 +142,7 @@ public class Mp4FromDashWriter {
outStream = null;
}
public void build(SharpStream output) throws IOException {
public void build(final SharpStream output) throws IOException {
if (done) {
throw new RuntimeException("already done");
}
@ -147,7 +155,7 @@ public class Mp4FromDashWriter {
// not allowed for very short tracks (less than 0.5 seconds)
//
outStream = output;
long read = 8;// mdat box header size
long read = 8; // mdat box header size
long totalSampleSize = 0;
int[] sampleExtra = new int[readers.length];
int[] defaultMediaTime = new int[readers.length];
@ -159,7 +167,13 @@ public class Mp4FromDashWriter {
tablesInfo[i] = new TablesInfo();
}
boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio;
int singleSampleBuffer;
if (tracks.length == 1 && tracks[0].kind == TrackKind.Audio) {
// near 1 second of audio data per chunk, avoid split the audio stream in large chunks
singleSampleBuffer = tracks[0].trak.mdia.mdhdTimeScale / 1000;
} else {
singleSampleBuffer = -1;
}
for (int i = 0; i < readers.length; i++) {
@ -175,7 +189,7 @@ public class Mp4FromDashWriter {
}
read += chunk.moof.traf.trun.chunkSize;
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration; // calculate track duration
TrunEntry info;
while ((info = chunk.getNextSampleInfo()) != null) {
@ -210,76 +224,50 @@ public class Mp4FromDashWriter {
readers[i].rewind();
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
tmp = tmp % SAMPLES_PER_CHUNK;
if (singleChunk) {
// avoid split audio streams in chunks
tablesInfo[i].stsc = 1;
tablesInfo[i].stsc_bEntries = new int[]{
1, tablesInfo[i].stsz, 1
};
tablesInfo[i].stco = 1;
} else if (tmp == 0) {
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1
};
if (singleSampleBuffer > 0) {
initChunkTables(tablesInfo[i], singleSampleBuffer, singleSampleBuffer);
} else {
tablesInfo[i].stsc = 3;// first chunk (init) and successive chunks and remain chunk
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1,
tablesInfo[i].stco + 1, tmp, 1
};
tablesInfo[i].stco++;
initChunkTables(tablesInfo[i], SAMPLES_PER_CHUNK_INIT, SAMPLES_PER_CHUNK);
}
sampleCount[i] = tablesInfo[i].stsz;
if (sampleSizeChanges == 1) {
tablesInfo[i].stsz = 0;
tablesInfo[i].stsz_default = samplesSize;
tablesInfo[i].stszDefault = samplesSize;
} else {
tablesInfo[i].stsz_default = 0;
tablesInfo[i].stszDefault = 0;
}
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes)
tablesInfo[i].stss = -1; // for audio tracks (all samples are keyframes)
}
// ensure track duration
if (tracks[i].trak.tkhd.duration < 1) {
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
tracks[i].trak.tkhd.duration = sampleExtra[i]; // this never should happen
}
}
boolean is64 = read > THRESHOLD_FOR_CO64;
// calculate the moov size;
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
// calculate the moov size
int auxSize = makeMoov(defaultMediaTime, tablesInfo, is64);
if (auxSize < THRESHOLD_MOOV_LENGTH) {
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
auxBuffer = ByteBuffer.allocate(auxSize); // cache moov in the memory
}
moovSimulation = false;
writeOffset = 0;
final int ftyp_size = make_ftyp();
final int ftypSize = makeFtyp();
// reserve moov space in the output stream
/*if (outStream.canSetLength()) {
long length = writeOffset + auxSize;
outStream.setLength(length);
outSeek(length);
} else {*/
if (auxSize > 0) {
int length = auxSize;
byte[] buffer = new byte[64 * 1024];// 64 KiB
byte[] buffer = new byte[64 * 1024]; // 64 KiB
while (length > 0) {
int count = Math.min(length, buffer.length);
outWrite(buffer, count);
@ -288,34 +276,38 @@ public class Mp4FromDashWriter {
}
if (auxBuffer == null) {
outSeek(ftyp_size);
outSeek(ftypSize);
}
// tablesInfo contains row counts
// and after returning from make_moov() will contain table offsets
make_moov(defaultMediaTime, tablesInfo, is64);
// and after returning from makeMoov() will contain those table offsets
makeMoov(defaultMediaTime, tablesInfo, is64);
// write tables: stts stsc
// write tables: stts stsc sbgp
// reset for ctts table: sampleCount sampleExtra
for (int i = 0; i < readers.length; i++) {
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
tablesInfo[i].stsc_bEntries = null;
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stscBEntries.length,
tablesInfo[i].stscBEntries);
tablesInfo[i].stscBEntries = null;
if (tablesInfo[i].ctts > 0) {
sampleCount[i] = 1;// the index is not base zero
sampleCount[i] = 1; // the index is not base zero
sampleExtra[i] = -1;
}
if (tablesInfo[i].sbgp > 0) {
writeEntryArray(tablesInfo[i].sbgp, 1, sampleCount[i]);
}
}
if (auxBuffer == null) {
outRestore();
}
outWrite(make_mdat(totalSampleSize, is64));
outWrite(makeMdat(totalSampleSize, is64));
int[] sampleIndex = new int[readers.length];
int[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
int[] sizes = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK];
int[] sync = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK];
int written = readers.length;
while (written > 0) {
@ -323,14 +315,14 @@ public class Mp4FromDashWriter {
for (int i = 0; i < readers.length; i++) {
if (sampleIndex[i] < 0) {
continue;// track is done
continue; // track is done
}
long chunkOffset = writeOffset;
int syncCount = 0;
int limit;
if (singleChunk) {
limit = SINGLE_CHUNK_SAMPLE_BUFFER;
if (singleSampleBuffer > 0) {
limit = singleSampleBuffer;
} else {
limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
}
@ -341,7 +333,9 @@ public class Mp4FromDashWriter {
if (sample == null) {
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i],
sampleExtra[i]); // flush last entries
outRestore();
}
sampleIndex[i] = -1;
break;
@ -354,7 +348,8 @@ public class Mp4FromDashWriter {
sampleCount[i]++;
} else {
if (sampleExtra[i] >= 0) {
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2,
sampleCount[i], sampleExtra[i]);
outRestore();
}
sampleCount[i] = 1;
@ -388,11 +383,8 @@ public class Mp4FromDashWriter {
if (is64) {
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
} else {
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
}
if (singleChunk) {
tablesInfo[i].stco = -1;
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1,
(int) chunkOffset);
}
}
@ -403,17 +395,17 @@ public class Mp4FromDashWriter {
if (auxBuffer != null) {
// dump moov
outSeek(ftyp_size);
outSeek(ftypSize);
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
auxBuffer = null;
}
}
private Mp4DashSample getNextSample(int track) throws IOException {
private Mp4DashSample getNextSample(final int track) throws IOException {
if (readersChunks[track] == null) {
readersChunks[track] = readers[track].getNextChunk(false);
if (readersChunks[track] == null) {
return null;// EOF reached
return null; // EOF reached
}
}
@ -427,7 +419,7 @@ public class Mp4FromDashWriter {
}
private int writeEntry64(int offset, long value) throws IOException {
private int writeEntry64(final int offset, final long value) throws IOException {
outBackup();
auxSeek(offset);
@ -436,7 +428,8 @@ public class Mp4FromDashWriter {
return offset + 8;
}
private int writeEntryArray(int offset, int count, int... values) throws IOException {
private int writeEntryArray(final int offset, final int count, final int... values)
throws IOException {
outBackup();
auxSeek(offset);
@ -470,18 +463,54 @@ public class Mp4FromDashWriter {
}
}
private void initChunkTables(final TablesInfo tables, final int firstCount,
final int successiveCount) {
// tables.stsz holds amount of samples of the track (total)
int totalSamples = (tables.stsz - firstCount);
float chunkAmount = totalSamples / (float) successiveCount;
int remainChunkOffset = (int) Math.ceil(chunkAmount);
boolean remain = remainChunkOffset != (int) chunkAmount;
int index = 0;
tables.stsc = 1;
if (firstCount != successiveCount) {
tables.stsc++;
}
if (remain) {
tables.stsc++;
}
private void outWrite(byte[] buffer) throws IOException {
// stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index]
tables.stscBEntries = new int[tables.stsc * 3];
tables.stco = remainChunkOffset + 1; // total entrys in chunk offset box
tables.stscBEntries[index++] = 1;
tables.stscBEntries[index++] = firstCount;
tables.stscBEntries[index++] = 1;
if (firstCount != successiveCount) {
tables.stscBEntries[index++] = 2;
tables.stscBEntries[index++] = successiveCount;
tables.stscBEntries[index++] = 1;
}
if (remain) {
tables.stscBEntries[index++] = remainChunkOffset + 1;
tables.stscBEntries[index++] = totalSamples % successiveCount;
tables.stscBEntries[index] = 1;
}
}
private void outWrite(final byte[] buffer) throws IOException {
outWrite(buffer, buffer.length);
}
private void outWrite(byte[] buffer, int count) throws IOException {
private void outWrite(final byte[] buffer, final int count) throws IOException {
writeOffset += count;
outStream.write(buffer, 0, count);
}
private void outSeek(long offset) throws IOException {
private void outSeek(final long offset) throws IOException {
if (outStream.canSeek()) {
outStream.seek(offset);
writeOffset = offset;
@ -494,12 +523,12 @@ public class Mp4FromDashWriter {
}
}
private void outSkip(long amount) throws IOException {
private void outSkip(final long amount) throws IOException {
outStream.skip(amount);
writeOffset += amount;
}
private int lengthFor(int offset) throws IOException {
private int lengthFor(final int offset) throws IOException {
int size = auxOffset() - offset;
if (moovSimulation) {
@ -513,7 +542,8 @@ public class Mp4FromDashWriter {
return size;
}
private int make(int type, int extra, int columns, int rows) throws IOException {
private int make(final int type, final int extra, final int columns, final int rows)
throws IOException {
final byte base = 16;
int size = columns * rows * 4;
int total = size + base;
@ -541,14 +571,14 @@ public class Mp4FromDashWriter {
return offset + base;
}
private void auxWrite(int value) throws IOException {
private void auxWrite(final int value) throws IOException {
auxWrite(ByteBuffer.allocate(4)
.putInt(value)
.array()
);
}
private void auxWrite(byte[] buffer) throws IOException {
private void auxWrite(final byte[] buffer) throws IOException {
if (moovSimulation) {
writeOffset += buffer.length;
} else if (auxBuffer == null) {
@ -558,7 +588,7 @@ public class Mp4FromDashWriter {
}
}
private void auxSeek(int offset) throws IOException {
private void auxSeek(final int offset) throws IOException {
if (moovSimulation) {
writeOffset = offset;
} else if (auxBuffer == null) {
@ -568,7 +598,7 @@ public class Mp4FromDashWriter {
}
}
private void auxSkip(int amount) throws IOException {
private void auxSkip(final int amount) throws IOException {
if (moovSimulation) {
writeOffset += amount;
} else if (auxBuffer == null) {
@ -582,43 +612,54 @@ public class Mp4FromDashWriter {
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
}
private int makeFtyp() throws IOException {
int size = 16 + (compatibleBrands.size() * 4);
if (overrideMainBrand != 0) {
size += 4;
}
ByteBuffer buffer = ByteBuffer.allocate(size);
buffer.putInt(size);
buffer.putInt(0x66747970); // "ftyp"
private int make_ftyp() throws IOException {
byte[] buffer = new byte[]{
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
0x00, 0x00, 0x02, 0x00,// default minor version (512)
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
};
if (overrideMainBrand == 0) {
buffer.putInt(0x6D703432); // mayor brand "mp42"
buffer.putInt(512); // default minor version
} else {
buffer.putInt(overrideMainBrand);
buffer.putInt(0);
buffer.putInt(0x6D703432); // "mp42" compatible brand
}
if (overrideMainBrand != 0)
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
for (Integer brand : compatibleBrands) {
buffer.putInt(brand); // compatible brand
}
outWrite(buffer);
outWrite(buffer.array());
return buffer.length;
return size;
}
private byte[] make_mdat(long refSize, boolean is64) {
private byte[] makeMdat(final long refSize, final boolean is64) {
long size = refSize;
if (is64) {
refSize += 16;
size += 16;
} else {
refSize += 8;
size += 8;
}
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
.putInt(is64 ? 0x01 : (int) refSize)
.putInt(0x6D646174);// mdat
.putInt(is64 ? 0x01 : (int) size)
.putInt(0x6D646174); // mdat
if (is64) {
buffer.putLong(refSize);
buffer.putLong(size);
}
return buffer.array();
}
private void make_mvhd(long longestTrack) throws IOException {
private void makeMvhd(final long longestTrack) throws IOException {
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
});
@ -631,21 +672,23 @@ public class Mp4FromDashWriter {
);
auxWrite(new byte[]{
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
0x00, 0x01, 0x00, 0x00, 0x01, 0x00, // default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved values
// default matrix
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00
});
auxWrite(new byte[24]);// predefined
auxWrite(new byte[24]); // predefined
auxWrite(ByteBuffer.allocate(4)
.putInt(tracks.length + 1)
.array()
);
}
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo,
final boolean is64) throws RuntimeException, IOException {
int start = auxOffset();
auxWrite(new byte[]{
@ -657,43 +700,36 @@ public class Mp4FromDashWriter {
for (int i = 0; i < durations.length; i++) {
durations[i] = (long) Math.ceil(
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE
);
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhdTimeScale)
* DEFAULT_TIMESCALE);
if (durations[i] > longestTrack) {
longestTrack = durations[i];
}
}
make_mvhd(longestTrack);
makeMvhd(longestTrack);
for (int i = 0; i < tracks.length; i++) {
if (tracks[i].trak.tkhd.matrix.length != 36) {
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
throw
new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
}
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
}
// udta/meta/ilst/©too
auxWrite(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, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
});
return lengthFor(start);
}
private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException {
private void makeTrak(final int index, final long duration, final int defaultMediaTime,
final TablesInfo tables, final boolean is64) throws IOException {
int start = auxOffset();
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
// trak header
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,
// tkhd header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03
});
ByteBuffer buffer = ByteBuffer.allocate(48);
@ -716,20 +752,21 @@ public class Mp4FromDashWriter {
);
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73, // edts header
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // elst header
});
int bMediaRate;
int mediaTime;
if (tracks[index].trak.edst_elst == null) {
if (tracks[index].trak.edstElst == null) {
// is a audio track ¿is edst/elst optional for audio tracks?
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
mediaTime = 0x00; // ffmpeg set this value as zero, instead of defaultMediaTime
bMediaRate = 0x00010000;
} else {
mediaTime = (int) tracks[index].trak.edst_elst.MediaTime;
bMediaRate = tracks[index].trak.edst_elst.bMediaRate;
mediaTime = (int) tracks[index].trak.edstElst.mediaTime;
bMediaRate = tracks[index].trak.edstElst.bMediaRate;
}
auxWrite(ByteBuffer
@ -740,33 +777,33 @@ public class Mp4FromDashWriter {
.array()
);
make_mdia(tracks[index].trak.mdia, tables, is64);
makeMdia(tracks[index].trak.mdia, tables, is64, tracks[index].kind == TrackKind.Audio);
lengthFor(start);
}
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
int start_mdia = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
private void makeMdia(final Mdia mdia, final TablesInfo tablesInfo, final boolean is64,
final boolean isAudio) throws IOException {
int startMdia = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61}); // mdia
auxWrite(mdia.mdhd);
auxWrite(make_hdlr(mdia.hdlr));
auxWrite(makeHdlr(mdia.hdlr));
int start_minf = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf
auxWrite(mdia.minf.$mhd);
int startMinf = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66}); // minf
auxWrite(mdia.minf.mhd);
auxWrite(mdia.minf.dinf);
int start_stbl = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl
auxWrite(mdia.minf.stbl_stsd);
int startStbl = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C}); // stbl
auxWrite(mdia.minf.stblStsd);
//
// In audio tracks the following tables is not required: ssts ctts
// And stsz can be empty if has a default sample size
//
if (moovSimulation) {
make(0x73747473, -1, 2, 1);
make(0x73747473, -1, 2, 1); // stts
if (tablesInfo.stss > 0) {
make(0x73747373, -1, 1, tablesInfo.stss);
}
@ -774,7 +811,7 @@ public class Mp4FromDashWriter {
make(0x63747473, -1, 2, tablesInfo.ctts);
}
make(0x73747363, -1, 3, tablesInfo.stsc);
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
make(0x7374737A, tablesInfo.stszDefault, 1, tablesInfo.stsz);
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
} else {
tablesInfo.stts = make(0x73747473, -1, 2, 1);
@ -785,47 +822,89 @@ public class Mp4FromDashWriter {
tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts);
}
tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc);
tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
tablesInfo.stsz = make(0x7374737A, tablesInfo.stszDefault, 1, tablesInfo.stsz);
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1,
tablesInfo.stco);
}
lengthFor(start_stbl);
lengthFor(start_minf);
lengthFor(start_mdia);
if (isAudio) {
auxWrite(makeSgpd());
tablesInfo.sbgp = makeSbgp(); // during simulation the returned offset is ignored
}
lengthFor(startStbl);
lengthFor(startMinf);
lengthFor(startMdia);
}
private byte[] make_hdlr(Hdlr hdlr) {
private byte[] makeHdlr(final Hdlr hdlr) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{
0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)."
0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70,
0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74,
0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67,
0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E,
0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E
0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, // hdlr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00// null string character
});
buffer.position(12);
buffer.putInt(hdlr.type);
buffer.putInt(hdlr.subType);
buffer.put(hdlr.bReserved);// always is a zero array
buffer.put(hdlr.bReserved); // always is a zero array
return buffer.array();
}
private int makeSbgp() throws IOException {
int offset = auxOffset();
auxWrite(new byte[] {
0x00, 0x00, 0x00, 0x1C, // box size
0x73, 0x62, 0x67, 0x70, // "sbpg"
0x00, 0x00, 0x00, 0x00, // default box flags
0x72, 0x6F, 0x6C, 0x6C, // group type "roll"
0x00, 0x00, 0x00, 0x01, // group table size
0x00, 0x00, 0x00, 0x00, // group[0] total samples (to be set later)
0x00, 0x00, 0x00, 0x01 // group[0] description index
});
return offset + 0x14;
}
private byte[] makeSgpd() {
/*
* Sample Group Description Box
*
* ¿whats does?
* the table inside of this box gives information about the
* characteristics of sample groups. The descriptive information is any other
* information needed to define or characterize the sample group.
*
* ¿is replicable this box?
* NO due lacks of documentation about this box but...
* most of m4a encoders and ffmpeg uses this box with dummy values (same values)
*/
ByteBuffer buffer = ByteBuffer.wrap(new byte[] {
0x00, 0x00, 0x00, 0x1A, // box size
0x73, 0x67, 0x70, 0x64, // "sgpd"
0x01, 0x00, 0x00, 0x00, // box flags (unknown flag sets)
0x72, 0x6F, 0x6C, 0x6C, // ¿¿group type??
0x00, 0x00, 0x00, 0x02, // ¿¿??
0x00, 0x00, 0x00, 0x01, // ¿¿??
(byte) 0xFF, (byte) 0xFF // ¿¿??
});
return buffer.array();
}
class TablesInfo {
int stts;
int stsc;
int[] stsc_bEntries;
int[] stscBEntries;
int ctts;
int stsz;
int stsz_default;
int stszDefault;
int stss;
int stco;
int sbgp;
}
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.streams;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.streams.WebMReader.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment;
@ -13,22 +14,19 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import androidx.annotation.Nullable;
/**
* @author kapodamy
*/
public class OggFromWebMWriter implements Closeable {
private static final byte FLAG_UNSET = 0x00;
//private static final byte FLAG_CONTINUED = 0x01;
private static final byte FLAG_FIRST = 0x02;
private static final byte FLAG_LAST = 0x04;
private final static byte HEADER_CHECKSUM_OFFSET = 22;
private final static byte HEADER_SIZE = 27;
private static final byte HEADER_CHECKSUM_OFFSET = 22;
private static final byte HEADER_SIZE = 27;
private final static int TIME_SCALE_NS = 1000000000;
private static final int TIME_SCALE_NS = 1000000000;
private boolean done = false;
private boolean parsed = false;
@ -36,26 +34,26 @@ public class OggFromWebMWriter implements Closeable {
private SharpStream source;
private SharpStream output;
private int sequence_count = 0;
private final int STREAM_ID;
private byte packet_flag = FLAG_FIRST;
private int sequenceCount = 0;
private final int streamId;
private byte packetFlag = FLAG_FIRST;
private WebMReader webm = null;
private WebMTrack webm_track = null;
private Segment webm_segment = null;
private Cluster webm_cluster = null;
private SimpleBlock webm_block = null;
private WebMTrack webmTrack = null;
private Segment webmSegment = null;
private Cluster webmCluster = null;
private SimpleBlock webmBlock = null;
private long webm_block_last_timecode = 0;
private long webm_block_near_duration = 0;
private long webmBlockLastTimecode = 0;
private long webmBlockNearDuration = 0;
private short segment_table_size = 0;
private final byte[] segment_table = new byte[255];
private long segment_table_next_timestamp = TIME_SCALE_NS;
private short segmentTableSize = 0;
private final byte[] segmentTable = new byte[255];
private long segmentTableNextTimestamp = TIME_SCALE_NS;
private final int[] crc32_table = new int[256];
private final int[] crc32Table = new int[256];
public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) {
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) {
if (!source.canRead() || !source.canRewind()) {
throw new IllegalArgumentException("source stream must be readable and allows seeking");
}
@ -66,9 +64,9 @@ public class OggFromWebMWriter implements Closeable {
this.source = source;
this.output = target;
this.STREAM_ID = (int) System.currentTimeMillis();
this.streamId = (int) System.currentTimeMillis();
populate_crc32_table();
populateCrc32Table();
}
public boolean isDone() {
@ -98,20 +96,20 @@ public class OggFromWebMWriter implements Closeable {
try {
webm = new WebMReader(source);
webm.parse();
webm_segment = webm.getNextSegment();
webmSegment = webm.getNextSegment();
} finally {
parsed = true;
}
}
public void selectTrack(int trackIndex) throws IOException {
public void selectTrack(final int trackIndex) throws IOException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
if (done) {
throw new IOException("already done");
}
if (webm_track != null) {
if (webmTrack != null) {
throw new IOException("tracks already selected");
}
@ -124,7 +122,7 @@ public class OggFromWebMWriter implements Closeable {
}
try {
webm_track = webm.selectTrack(trackIndex);
webmTrack = webm.selectTrack(trackIndex);
} finally {
parsed = true;
}
@ -135,7 +133,7 @@ public class OggFromWebMWriter implements Closeable {
done = true;
parsed = true;
webm_track = null;
webmTrack = null;
webm = null;
if (!output.isClosed()) {
@ -155,43 +153,44 @@ public class OggFromWebMWriter implements Closeable {
header.order(ByteOrder.LITTLE_ENDIAN);
/* step 1: get the amount of frames per seconds */
switch (webm_track.kind) {
switch (webmTrack.kind) {
case Audio:
resolution = getSampleFrequencyFromTrack(webm_track.bMetadata);
if (resolution == 0.0f) {
resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata);
if (resolution == 0f) {
throw new RuntimeException("cannot get the audio sample rate");
}
break;
case Video:
// WARNING: untested
if (webm_track.defaultDuration == 0) {
if (webmTrack.defaultDuration == 0) {
throw new RuntimeException("missing default frame time");
}
resolution = 1000.0f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale);
resolution = 1000f / ((float) webmTrack.defaultDuration
/ webmSegment.info.timecodeScale);
break;
default:
throw new RuntimeException("not implemented");
}
/* step 2: create packet with code init data */
if (webm_track.codecPrivate != null) {
addPacketSegment(webm_track.codecPrivate.length);
make_packetHeader(0x00, header, webm_track.codecPrivate);
if (webmTrack.codecPrivate != null) {
addPacketSegment(webmTrack.codecPrivate.length);
makePacketheader(0x00, header, webmTrack.codecPrivate);
write(header);
output.write(webm_track.codecPrivate);
output.write(webmTrack.codecPrivate);
}
/* step 3: create packet with metadata */
byte[] buffer = make_metadata();
byte[] buffer = makeMetadata();
if (buffer != null) {
addPacketSegment(buffer.length);
make_packetHeader(0x00, header, buffer);
makePacketheader(0x00, header, buffer);
write(header);
output.write(buffer);
}
/* step 4: calculate amount of packets */
while (webm_segment != null) {
while (webmSegment != null) {
bloq = getNextBlock();
if (bloq != null && addPacketSegment(bloq)) {
@ -203,29 +202,29 @@ public class OggFromWebMWriter implements Closeable {
}
// calculate the current packet duration using the next block
double elapsed_ns = webm_track.codecDelay;
double elapsedNs = webmTrack.codecDelay;
if (bloq == null) {
packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed
elapsed_ns += webm_block_last_timecode;
packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed
elapsedNs += webmBlockLastTimecode;
if (webm_track.defaultDuration > 0) {
elapsed_ns += webm_track.defaultDuration;
if (webmTrack.defaultDuration > 0) {
elapsedNs += webmTrack.defaultDuration;
} else {
// hardcoded way, guess the sample duration
elapsed_ns += webm_block_near_duration;
elapsedNs += webmBlockNearDuration;
}
} else {
elapsed_ns += bloq.absoluteTimeCodeNs;
elapsedNs += bloq.absoluteTimeCodeNs;
}
// get the sample count in the page
elapsed_ns = elapsed_ns / TIME_SCALE_NS;
elapsed_ns = Math.ceil(elapsed_ns * resolution);
elapsedNs = elapsedNs / TIME_SCALE_NS;
elapsedNs = Math.ceil(elapsedNs * resolution);
// create header and calculate page checksum
int checksum = make_packetHeader((long) elapsed_ns, header, null);
checksum = calc_crc32(checksum, page.array(), page.position());
int checksum = makePacketheader((long) elapsedNs, header, null);
checksum = calcCrc32(checksum, page.array(), page.position());
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
@ -233,69 +232,57 @@ public class OggFromWebMWriter implements Closeable {
write(header);
write(page);
webm_block = bloq;
webmBlock = bloq;
}
}
private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) {
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
final byte[] immediatePage) {
short length = HEADER_SIZE;
buffer.putInt(0x5367674f);// "OggS" binary string in little-endian
buffer.put((byte) 0x00);// version
buffer.put(packet_flag);// type
buffer.putInt(0x5367674f); // "OggS" binary string in little-endian
buffer.put((byte) 0x00); // version
buffer.put(packetFlag); // type
buffer.putLong(gran_pos);// granulate position
buffer.putLong(granPos); // granulate position
buffer.putInt(STREAM_ID);// bitstream serial number
buffer.putInt(sequence_count++);// page sequence number
buffer.putInt(streamId); // bitstream serial number
buffer.putInt(sequenceCount++); // page sequence number
buffer.putInt(0x00);// page checksum
buffer.putInt(0x00); // page checksum
buffer.put((byte) segment_table_size);// segment table
buffer.put(segment_table, 0, segment_table_size);// segment size
buffer.put((byte) segmentTableSize); // segment table
buffer.put(segmentTable, 0, segmentTableSize); // segment size
length += segment_table_size;
length += segmentTableSize;
clearSegmentTable();// clear segment table for next header
clearSegmentTable(); // clear segment table for next header
int checksum_crc32 = calc_crc32(0x00, buffer.array(), length);
int checksumCrc32 = calcCrc32(0x00, buffer.array(), length);
if (immediate_page != null) {
checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length);
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32);
segment_table_next_timestamp -= TIME_SCALE_NS;
if (immediatePage != null) {
checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length);
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32);
segmentTableNextTimestamp -= TIME_SCALE_NS;
}
return checksum_crc32;
return checksumCrc32;
}
@Nullable
private byte[] make_metadata() {
if ("A_OPUS".equals(webm_track.codecId)) {
private byte[] makeMetadata() {
if ("A_OPUS".equals(webmTrack.codecId)) {
return new byte[]{
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string
0x07, 0x00, 0x00, 0x00,// writting application string size
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags)
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
} else if ("A_VORBIS".equals(webm_track.codecId)) {
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
return new byte[]{
0x03,// ????????
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string
0x07, 0x00, 0x00, 0x00,// writting application string size
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags)
/*
// whole file duration (not implemented)
0x44,// tag string size
0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30,
0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30
*/
0x0F,// tag string size
0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ????????
0x03, // ¿¿¿???
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
}
@ -303,51 +290,49 @@ public class OggFromWebMWriter implements Closeable {
return null;
}
private void write(ByteBuffer buffer) throws IOException {
private void write(final ByteBuffer buffer) throws IOException {
output.write(buffer.array(), 0, buffer.position());
buffer.position(0);
}
@Nullable
private SimpleBlock getNextBlock() throws IOException {
SimpleBlock res;
if (webm_block != null) {
res = webm_block;
webm_block = null;
if (webmBlock != null) {
res = webmBlock;
webmBlock = null;
return res;
}
if (webm_segment == null) {
webm_segment = webm.getNextSegment();
if (webm_segment == null) {
return null;// no more blocks in the selected track
if (webmSegment == null) {
webmSegment = webm.getNextSegment();
if (webmSegment == null) {
return null; // no more blocks in the selected track
}
}
if (webm_cluster == null) {
webm_cluster = webm_segment.getNextCluster();
if (webm_cluster == null) {
webm_segment = null;
if (webmCluster == null) {
webmCluster = webmSegment.getNextCluster();
if (webmCluster == null) {
webmSegment = null;
return getNextBlock();
}
}
res = webm_cluster.getNextSimpleBlock();
res = webmCluster.getNextSimpleBlock();
if (res == null) {
webm_cluster = null;
webmCluster = null;
return getNextBlock();
}
webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode;
webm_block_last_timecode = res.absoluteTimeCodeNs;
webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode;
webmBlockLastTimecode = res.absoluteTimeCodeNs;
return res;
}
private float getSampleFrequencyFromTrack(byte[] bMetadata) {
private float getSampleFrequencyFromTrack(final byte[] bMetadata) {
// hardcoded way
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
@ -362,27 +347,27 @@ public class OggFromWebMWriter implements Closeable {
}
private void clearSegmentTable() {
segment_table_next_timestamp += TIME_SCALE_NS;
packet_flag = FLAG_UNSET;
segment_table_size = 0;
segmentTableNextTimestamp += TIME_SCALE_NS;
packetFlag = FLAG_UNSET;
segmentTableSize = 0;
}
private boolean addPacketSegment(SimpleBlock block) {
long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay;
private boolean addPacketSegment(final SimpleBlock block) {
long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay;
if (timestamp >= segment_table_next_timestamp) {
if (timestamp >= segmentTableNextTimestamp) {
return false;
}
return addPacketSegment(block.dataSize);
}
private boolean addPacketSegment(int size) {
private boolean addPacketSegment(final int size) {
if (size > 65025) {
throw new UnsupportedOperationException("page size cannot be larger than 65025");
}
int available = (segment_table.length - segment_table_size) * 255;
int available = (segmentTable.length - segmentTableSize) * 255;
boolean extra = (size % 255) == 0;
if (extra) {
@ -393,21 +378,21 @@ public class OggFromWebMWriter implements Closeable {
// check if possible add the segment, without overflow the table
if (available < size) {
return false;// not enough space on the page
return false; // not enough space on the page
}
for (; size > 0; size -= 255) {
segment_table[segment_table_size++] = (byte) Math.min(size, 255);
for (int seg = size; seg > 0; seg -= 255) {
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
}
if (extra) {
segment_table[segment_table_size++] = 0x00;
segmentTable[segmentTableSize++] = 0x00;
}
return true;
}
private void populate_crc32_table() {
private void populateCrc32Table() {
for (int i = 0; i < 0x100; i++) {
int crc = i << 24;
for (int j = 0; j < 8; j++) {
@ -415,17 +400,17 @@ public class OggFromWebMWriter implements Closeable {
crc <<= 1;
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
}
crc32_table[i] = crc;
crc32Table[i] = crc;
}
}
private int calc_crc32(int initial_crc, byte[] buffer, int size) {
private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) {
int crc = initialCrc;
for (int i = 0; i < size; i++) {
int reg = (initial_crc >>> 24) & 0xff;
initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)];
int reg = (crc >>> 24) & 0xff;
crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)];
}
return initial_crc;
return crc;
}
}

View file

@ -0,0 +1,103 @@
package org.schabi.newpipe.streams;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.parser.Parser;
import org.jsoup.select.Elements;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author kapodamy
*/
public class SrtFromTtmlWriter {
private static final String NEW_LINE = "\r\n";
private SharpStream out;
private boolean ignoreEmptyFrames;
private final Charset charset = StandardCharsets.UTF_8;
private int frameIndex = 0;
public SrtFromTtmlWriter(final SharpStream out, final boolean ignoreEmptyFrames) {
this.out = out;
this.ignoreEmptyFrames = ignoreEmptyFrames;
}
private static String getTimestamp(final Element frame, final String attr) {
return frame
.attr(attr)
.replace('.', ','); // SRT subtitles uses comma as decimal separator
}
private void writeFrame(final String begin, final String end, final StringBuilder text)
throws IOException {
writeString(String.valueOf(frameIndex++));
writeString(NEW_LINE);
writeString(begin);
writeString(" --> ");
writeString(end);
writeString(NEW_LINE);
writeString(text.toString());
writeString(NEW_LINE);
writeString(NEW_LINE);
}
private void writeString(final String text) throws IOException {
out.write(text.getBytes(charset));
}
public void build(final SharpStream ttml) throws IOException {
/*
* TTML 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
*/
// parse XML
byte[] buffer = new byte[(int) ttml.available()];
ttml.read(buffer);
Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "",
Parser.xmlParser());
StringBuilder text = new StringBuilder(128);
Elements paragraphList = doc.select("body > div > p");
// check if has frames
if (paragraphList.size() < 1) {
return;
}
for (Element paragraph : paragraphList) {
text.setLength(0);
for (Node children : paragraph.childNodes()) {
if (children instanceof TextNode) {
text.append(((TextNode) children).text());
} else if (children instanceof Element
&& ((Element) children).tagName().equalsIgnoreCase("br")) {
text.append(NEW_LINE);
}
}
if (ignoreEmptyFrames && text.length() < 1) {
continue;
}
String begin = getTimestamp(paragraph, "begin");
String end = getTimestamp(paragraph, "end");
writeFrame(begin, end, text);
}
}
}

View file

@ -1,369 +0,0 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Locale;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
/**
* @author kapodamy
*/
public class SubtitleConverter {
private static final String NEW_LINE = "\r\n";
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
final FrameWriter callback = new FrameWriter() {
int frameIndex = 0;
final Charset charset = Charset.forName("utf-8");
@Override
public void yield(SubtitleFrame frame) throws IOException {
if (ignoreEmptyFrames && frame.isEmptyText()) {
return;
}
out.write(String.valueOf(frameIndex++).getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(getTime(frame.start, true).getBytes(charset));
out.write(" --> ".getBytes(charset));
out.write(getTime(frame.end, true).getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(frame.text.getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
}
};
read_xml_based(in, callback, detectYoutubeDuplicateLines,
"tt", "xmlns", "http://www.w3.org/ns/ttml",
new String[]{"timedtext", "head", "wp"},
new String[]{"body", "div", "p"},
"begin", "end", true
);
}
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
String timeAttr, String durationAttr, boolean hasTimestamp
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
/*
* XML based subtitles parser with BASIC support
* multiple CUE is not supported
* styling is not supported
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
* also TimestampTagOption enum is not applicable
* Language parsing is not supported
*/
byte[] buffer = new byte[(int) source.available()];
source.read(buffer);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document xml = builder.parse(new ByteArrayInputStream(buffer));
String attr;
// get the format version or namespace
Element node = xml.getDocumentElement();
if (node == null) {
throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
} else if (!node.getNodeName().equals(root)) {
throw new ParseException("Invalid root", -1);
}
if (formatAttr.equals("xmlns")) {
if (!node.getNamespaceURI().equals(formatVersion)) {
throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
}
} else {
attr = node.getAttributeNS(formatVersion, formatAttr);
if (attr == null) {
throw new ParseException("Can't get the format attribute", -1);
}
if (!attr.equals(formatVersion)) {
throw new ParseException("Invalid format version : " + attr, -1);
}
}
NodeList node_list;
int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
if (!hasTimestamp) {
node_list = selectNodes(xml, cuePath, formatVersion);
if (node_list != null) {
// if the subtitle has multiple CUEs, use the highest value
for (int i = 0; i < node_list.getLength(); i++) {
try {
int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
if (tmp > line_break) {
line_break = tmp;
}
} catch (Exception err) {
}
}
}
}
// parse every frame
node_list = selectNodes(xml, framePath, formatVersion);
if (node_list == null) {
return;// no frames detected
}
int fs_ff = -1;// first timestamp of first frame
boolean limit_lines = false;
for (int i = 0; i < node_list.getLength(); i++) {
Element elem = (Element) node_list.item(i);
SubtitleFrame obj = new SubtitleFrame();
obj.text = elem.getTextContent();
attr = elem.getAttribute(timeAttr);// ¡this cant be null!
obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
attr = elem.getAttribute(durationAttr);
if (obj.text == null || attr == null) {
continue;// normally is a blank line (on auto-generated subtitles) ignore
}
if (hasTimestamp) {
obj.end = parseTimestamp(attr);
if (detectYoutubeDuplicateLines) {
if (limit_lines) {
int swap = obj.end;
obj.end = fs_ff;
fs_ff = swap;
} else {
if (fs_ff < 0) {
fs_ff = obj.end;
} else {
if (fs_ff < obj.start) {
limit_lines = true;// the subtitles has duplicated lines
} else {
detectYoutubeDuplicateLines = false;
}
}
}
}
} else {
obj.end = obj.start + Integer.parseInt(attr);
}
if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
// implement auto line breaking (once)
StringBuilder text = new StringBuilder(obj.text);
obj.text = null;
switch (text.charAt(line_break)) {
case ' ':
case '\t':
putBreakAt(line_break, text);
break;
default:// find the word start position
for (int j = line_break - 1; j > 0; j--) {
switch (text.charAt(j)) {
case ' ':
case '\t':
putBreakAt(j, text);
j = -1;
break;
case '\r':
case '\n':
j = -1;// long word, just ignore
break;
}
}
break;
}
obj.text = text.toString();// set the processed text
}
callback.yield(obj);
}
}
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
Element ref = xml.getDocumentElement();
for (int i = 0; i < path.length - 1; i++) {
NodeList nodes = ref.getChildNodes();
if (nodes.getLength() < 1) {
return null;
}
Element elem;
for (int j = 0; j < nodes.getLength(); j++) {
if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
elem = (Element) nodes.item(j);
if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
ref = elem;
break;
}
}
}
}
return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
}
private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
if (multiImpl.length() < 1) {
return 0;
} else if (multiImpl.length() == 1) {
return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
}
// detect wallclock-time
if (multiImpl.startsWith("wallclock(")) {
throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
}
// detect offset-time
if (multiImpl.indexOf(':') < 0) {
int multiplier = 1000;
char metric = multiImpl.charAt(multiImpl.length() - 1);
switch (metric) {
case 'h':
multiplier *= 3600000;
break;
case 'm':
multiplier *= 60000;
break;
case 's':
if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
multiplier = 1;// ms
}
break;
default:
if (!Character.isDigit(metric)) {
throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
}
metric = '\0';
break;
}
try {
String offset_time = multiImpl;
if (multiplier == 1) {
offset_time = offset_time.substring(0, offset_time.length() - 2);
} else if (metric != '\0') {
offset_time = offset_time.substring(0, offset_time.length() - 1);
}
double time_metric_based = Double.parseDouble(offset_time);
if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
return (int) (time_metric_based * multiplier);
}
} catch (Exception err) {
throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
}
}
// detect clock-time
int time = 0;
String[] units = multiImpl.split(":");
if (units.length < 3) {
throw new ParseException("Invalid clock-time timestamp", -1);
}
time += Integer.parseInt(units[0]) * 3600000;// hours
time += Integer.parseInt(units[1]) * 60000;//minutes
time += Float.parseFloat(units[2]) * 1000.0f;// seconds and milliseconds (if present)
// frames and sub-frames are ignored (not implemented)
// time += units[3] * fps;
return time;
}
private static void putBreakAt(int idx, StringBuilder str) {
// this should be optimized at compile time
if (NEW_LINE.length() > 1) {
str.delete(idx, idx + 1);// remove after replace
str.insert(idx, NEW_LINE);
} else {
str.setCharAt(idx, NEW_LINE.charAt(0));
}
}
private static String getTime(int time, boolean comma) {
// cast every value to integer to avoid auto-round in ToString("00").
StringBuilder str = new StringBuilder(12);
str.append(numberToString(time / 1000 / 3600, 2));// hours
str.append(':');
str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
str.append(':');
str.append(numberToString(time / 1000 % 60, 2));// seconds
str.append(comma ? ',' : '.');
str.append(numberToString(time % 1000, 3));// miliseconds
return str.toString();
}
private static String numberToString(int nro, int pad) {
return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
}
/******************
* helper classes *
******************/
private interface FrameWriter {
void yield(SubtitleFrame frame) throws IOException;
}
private static class SubtitleFrame {
//Java no support unsigned int
public int end;
public int start;
public String text = "";
private boolean isEmptyText() {
if (text == null) {
return true;
}
for (int i = 0; i < text.length(); i++) {
switch (text.charAt(i)) {
case ' ':
case '\t':
case '\r':
case '\n':
break;
default:
return false;
}
}
return true;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,63 +1,62 @@
package org.schabi.newpipe.streams.io;
import java.io.Closeable;
import java.io.IOException;
/**
* based on c#
*/
public abstract class SharpStream implements Closeable {
public abstract int read() throws IOException;
public abstract int read(byte buffer[]) throws IOException;
public abstract int read(byte buffer[], int offset, int count) throws IOException;
public abstract long skip(long amount) throws IOException;
public abstract long available();
public abstract void rewind() throws IOException;
public abstract boolean isClosed();
@Override
public abstract void close();
public abstract boolean canRewind();
public abstract boolean canRead();
public abstract boolean canWrite();
public boolean canSetLength() {
return false;
}
public boolean canSeek() {
return false;
}
public abstract void write(byte value) throws IOException;
public abstract void write(byte[] buffer) throws IOException;
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
public void flush() throws IOException {
// STUB
}
public void setLength(long length) throws IOException {
throw new IOException("Not implemented");
}
public void seek(long offset) throws IOException {
throw new IOException("Not implemented");
}
public long length() throws IOException {
throw new UnsupportedOperationException("Unsupported operation");
}
}
package org.schabi.newpipe.streams.io;
import java.io.Closeable;
import java.io.IOException;
/**
* Based on C#'s Stream class.
*/
public abstract class SharpStream implements Closeable {
public abstract int read() throws IOException;
public abstract int read(byte[] buffer) throws IOException;
public abstract int read(byte[] buffer, int offset, int count) throws IOException;
public abstract long skip(long amount) throws IOException;
public abstract long available();
public abstract void rewind() throws IOException;
public abstract boolean isClosed();
@Override
public abstract void close();
public abstract boolean canRewind();
public abstract boolean canRead();
public abstract boolean canWrite();
public boolean canSetLength() {
return false;
}
public boolean canSeek() {
return false;
}
public abstract void write(byte value) throws IOException;
public abstract void write(byte[] buffer) throws IOException;
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
public void flush() throws IOException {
// STUB
}
public void setLength(final long length) throws IOException {
throw new IOException("Not implemented");
}
public void seek(final long offset) throws IOException {
throw new IOException("Not implemented");
}
public long length() throws IOException {
throw new UnsupportedOperationException("Unsupported operation");
}
}