New MP4 muxer + Queue changes + Storage fixes
Main changes:
* correctly check the available space (CircularFile.java)
* misc cleanup (CircularFile.java)
* use the "Error Reporter" for non-http errors
* rewrite network state checking and add better support for API 21 (Lollipop) or higher
* implement "metered networks"
* add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered"
* add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G
* rewrite DataReader ¡Webm muxer is now 57% more faster!
* rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices
* fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers
Download queue:
* remember queue status, unless the user pause the download (un-queue)
* semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts
* allow enqueue failed downloads
* new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time
Miscellaneous:
* fix crash while selecting details/error menu (mistake on MissionFragment.java)
* misc serialize changes (DownloadMission.java)
* minor UI tweaks
* allow overwrite paused downloads
* fix wrong icons for grid/list button in downloads
* add share option
* implement #2006
* correct misspelled word in strings.xml (es) (cmn)
* fix MissionAdapter crash during device shutdown
New Mp4Muxer + required changes:
* new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!!
* re-work in SharpStream
* drop mp4 dash muxer
* misc changes: add warning in SecondaryStreamHelper.java,
* strip m4a DASH files to normal m4a format (youtube only)
Fix storage issues:
* warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :)
* "write proof" allow post-processing resuming only if the device ran out of space
* implement "insufficient storage" error for downloads
2019-03-23 02:54:07 +01:00
|
|
|
package org.schabi.newpipe.streams;
|
|
|
|
|
|
|
|
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
|
|
|
|
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.io.SharpStream;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @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 long time;
|
|
|
|
|
|
|
|
private ByteBuffer auxBuffer;
|
|
|
|
private SharpStream outStream;
|
|
|
|
|
|
|
|
private long lastWriteOffset = -1;
|
|
|
|
private long writeOffset;
|
|
|
|
|
|
|
|
private boolean moovSimulation = true;
|
|
|
|
|
|
|
|
private boolean done = false;
|
|
|
|
private boolean parsed = false;
|
|
|
|
|
|
|
|
private Mp4Track[] tracks;
|
|
|
|
private SharpStream[] sourceTracks;
|
|
|
|
|
|
|
|
private Mp4DashReader[] readers;
|
|
|
|
private Mp4DashChunk[] readersChunks;
|
|
|
|
|
|
|
|
private int overrideMainBrand = 0x00;
|
|
|
|
|
|
|
|
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
|
|
|
|
for (SharpStream src : sources) {
|
|
|
|
if (!src.canRewind() && !src.canRead()) {
|
|
|
|
throw new IOException("All sources must be readable and allow rewind");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sourceTracks = sources;
|
|
|
|
readers = new Mp4DashReader[sourceTracks.length];
|
|
|
|
readersChunks = new Mp4DashChunk[readers.length];
|
|
|
|
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
|
|
|
if (!parsed) {
|
|
|
|
throw new IllegalStateException("All sources must be parsed first");
|
|
|
|
}
|
|
|
|
|
|
|
|
return readers[sourceIndex].getAvailableTracks();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void parseSources() throws IOException, IllegalStateException {
|
|
|
|
if (done) {
|
|
|
|
throw new IllegalStateException("already done");
|
|
|
|
}
|
|
|
|
if (parsed) {
|
|
|
|
throw new IllegalStateException("already parsed");
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
for (int i = 0; i < readers.length; i++) {
|
|
|
|
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
|
|
|
readers[i].parse();
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
parsed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void selectTracks(int... trackIndex) throws IOException {
|
|
|
|
if (done) {
|
|
|
|
throw new IOException("already done");
|
|
|
|
}
|
|
|
|
if (tracks != null) {
|
|
|
|
throw new IOException("tracks already selected");
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
tracks = new Mp4Track[readers.length];
|
|
|
|
for (int i = 0; i < readers.length; i++) {
|
|
|
|
tracks[i] = readers[i].selectTrack(trackIndex[i]);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
parsed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setMainBrand(int brandId) {
|
|
|
|
overrideMainBrand = brandId;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isDone() {
|
|
|
|
return done;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isParsed() {
|
|
|
|
return parsed;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void close() throws IOException {
|
|
|
|
done = true;
|
|
|
|
parsed = true;
|
|
|
|
|
|
|
|
for (SharpStream src : sourceTracks) {
|
2019-04-05 19:45:39 +02:00
|
|
|
src.close();
|
New MP4 muxer + Queue changes + Storage fixes
Main changes:
* correctly check the available space (CircularFile.java)
* misc cleanup (CircularFile.java)
* use the "Error Reporter" for non-http errors
* rewrite network state checking and add better support for API 21 (Lollipop) or higher
* implement "metered networks"
* add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered"
* add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G
* rewrite DataReader ¡Webm muxer is now 57% more faster!
* rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices
* fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers
Download queue:
* remember queue status, unless the user pause the download (un-queue)
* semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts
* allow enqueue failed downloads
* new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time
Miscellaneous:
* fix crash while selecting details/error menu (mistake on MissionFragment.java)
* misc serialize changes (DownloadMission.java)
* minor UI tweaks
* allow overwrite paused downloads
* fix wrong icons for grid/list button in downloads
* add share option
* implement #2006
* correct misspelled word in strings.xml (es) (cmn)
* fix MissionAdapter crash during device shutdown
New Mp4Muxer + required changes:
* new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!!
* re-work in SharpStream
* drop mp4 dash muxer
* misc changes: add warning in SecondaryStreamHelper.java,
* strip m4a DASH files to normal m4a format (youtube only)
Fix storage issues:
* warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :)
* "write proof" allow post-processing resuming only if the device ran out of space
* implement "insufficient storage" error for downloads
2019-03-23 02:54:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
tracks = null;
|
|
|
|
sourceTracks = null;
|
|
|
|
|
|
|
|
readers = null;
|
|
|
|
readersChunks = null;
|
|
|
|
|
|
|
|
auxBuffer = null;
|
|
|
|
outStream = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void build(SharpStream output) throws IOException {
|
|
|
|
if (done) {
|
|
|
|
throw new RuntimeException("already done");
|
|
|
|
}
|
|
|
|
if (!output.canWrite()) {
|
|
|
|
throw new IOException("the provided output is not writable");
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// WARNING: the muxer requires at least 8 samples of every track
|
|
|
|
// not allowed for very short tracks (less than 0.5 seconds)
|
|
|
|
//
|
|
|
|
outStream = output;
|
|
|
|
int read = 8;// mdat box header size
|
|
|
|
long totalSampleSize = 0;
|
|
|
|
int[] sampleExtra = new int[readers.length];
|
|
|
|
int[] defaultMediaTime = new int[readers.length];
|
|
|
|
int[] defaultSampleDuration = new int[readers.length];
|
|
|
|
int[] sampleCount = new int[readers.length];
|
|
|
|
|
|
|
|
TablesInfo[] tablesInfo = new TablesInfo[tracks.length];
|
|
|
|
for (int i = 0; i < tablesInfo.length; i++) {
|
|
|
|
tablesInfo[i] = new TablesInfo();
|
|
|
|
}
|
|
|
|
|
|
|
|
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
|
|
|
for (int i = 0; i < readers.length; i++) {
|
|
|
|
int samplesSize = 0;
|
|
|
|
int sampleSizeChanges = 0;
|
|
|
|
int compositionOffsetLast = -1;
|
|
|
|
|
|
|
|
Mp4DashChunk chunk;
|
|
|
|
while ((chunk = readers[i].getNextChunk(true)) != null) {
|
|
|
|
|
|
|
|
if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) {
|
|
|
|
defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration;
|
|
|
|
}
|
|
|
|
|
|
|
|
read += chunk.moof.traf.trun.chunkSize;
|
|
|
|
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
|
|
|
|
|
|
|
|
TrunEntry info;
|
|
|
|
while ((info = chunk.getNextSampleInfo()) != null) {
|
|
|
|
if (info.isKeyframe) {
|
|
|
|
tablesInfo[i].stss++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (info.sampleDuration > defaultSampleDuration[i]) {
|
|
|
|
defaultSampleDuration[i] = info.sampleDuration;
|
|
|
|
}
|
|
|
|
|
|
|
|
tablesInfo[i].stsz++;
|
|
|
|
if (samplesSize != info.sampleSize) {
|
|
|
|
samplesSize = info.sampleSize;
|
|
|
|
sampleSizeChanges++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (info.hasCompositionTimeOffset) {
|
|
|
|
if (info.sampleCompositionTimeOffset != compositionOffsetLast) {
|
|
|
|
tablesInfo[i].ctts++;
|
|
|
|
compositionOffsetLast = info.sampleCompositionTimeOffset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
totalSampleSize += info.sampleSize;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (defaultMediaTime[i] < 1) {
|
|
|
|
defaultMediaTime[i] = defaultSampleDuration[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
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 (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
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
tablesInfo[i].stsc = 3;// first chunk (init) and succesive 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++;
|
|
|
|
}
|
|
|
|
|
|
|
|
sampleCount[i] = tablesInfo[i].stsz;
|
|
|
|
|
|
|
|
if (sampleSizeChanges == 1) {
|
|
|
|
tablesInfo[i].stsz = 0;
|
|
|
|
tablesInfo[i].stsz_default = samplesSize;
|
|
|
|
} else {
|
|
|
|
tablesInfo[i].stsz_default = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//</editor-fold>
|
|
|
|
|
|
|
|
boolean is64 = read > THRESHOLD_FOR_CO64;
|
|
|
|
|
|
|
|
// calculate the moov size;
|
|
|
|
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
|
|
|
|
|
|
|
|
if (auxSize < THRESHOLD_MOOV_LENGTH) {
|
|
|
|
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
|
|
|
|
}
|
|
|
|
|
|
|
|
moovSimulation = false;
|
|
|
|
writeOffset = 0;
|
|
|
|
|
|
|
|
final int ftyp_size = make_ftyp();
|
|
|
|
|
|
|
|
// reserve moov space in the output stream
|
|
|
|
if (outStream.canSetLength()) {
|
|
|
|
long length = writeOffset + auxSize;
|
|
|
|
outStream.setLength(length);
|
|
|
|
outSeek(length);
|
|
|
|
} else {
|
|
|
|
// hard way
|
|
|
|
int length = auxSize;
|
|
|
|
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
|
|
|
while (length > 0) {
|
|
|
|
int count = Math.min(length, buffer.length);
|
|
|
|
outWrite(buffer, 0, count);
|
|
|
|
length -= count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (auxBuffer == null) {
|
|
|
|
outSeek(ftyp_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
// tablesInfo contais row counts
|
|
|
|
// and after returning from make_moov() will contain table offsets
|
|
|
|
make_moov(defaultMediaTime, tablesInfo, is64);
|
|
|
|
|
|
|
|
// write tables: stts stsc
|
|
|
|
// 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;
|
|
|
|
if (tablesInfo[i].ctts > 0) {
|
|
|
|
sampleCount[i] = 1;// index is not base zero
|
|
|
|
sampleExtra[i] = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (auxBuffer == null) {
|
|
|
|
outRestore();
|
|
|
|
}
|
|
|
|
|
|
|
|
outWrite(make_mdat(totalSampleSize, is64));
|
|
|
|
|
|
|
|
int[] sampleIndex = new int[readers.length];
|
|
|
|
int[] sizes = new int[SAMPLES_PER_CHUNK];
|
|
|
|
int[] sync = new int[SAMPLES_PER_CHUNK];
|
|
|
|
|
|
|
|
int written = readers.length;
|
|
|
|
while (written > 0) {
|
|
|
|
written = 0;
|
|
|
|
|
|
|
|
for (int i = 0; i < readers.length; i++) {
|
|
|
|
if (sampleIndex[i] < 0) {
|
|
|
|
continue;// track is done
|
|
|
|
}
|
|
|
|
|
|
|
|
long chunkOffset = writeOffset;
|
|
|
|
int syncCount = 0;
|
|
|
|
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
|
|
|
|
|
|
|
int j = 0;
|
|
|
|
for (; j < limit; j++) {
|
|
|
|
Mp4DashSample sample = getNextSample(i);
|
|
|
|
|
|
|
|
if (sample == null) {
|
|
|
|
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
|
|
|
|
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
|
|
|
|
}
|
|
|
|
sampleIndex[i] = -1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
sampleIndex[i]++;
|
|
|
|
|
|
|
|
if (tablesInfo[i].ctts > 0) {
|
|
|
|
if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) {
|
|
|
|
sampleCount[i]++;
|
|
|
|
} else {
|
|
|
|
if (sampleExtra[i] >= 0) {
|
|
|
|
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
|
|
|
|
outRestore();
|
|
|
|
}
|
|
|
|
sampleCount[i] = 1;
|
|
|
|
sampleExtra[i] = sample.info.sampleCompositionTimeOffset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) {
|
|
|
|
sync[syncCount++] = sampleIndex[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tablesInfo[i].stsz > 0) {
|
|
|
|
sizes[j] = sample.data.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
outWrite(sample.data, 0, sample.data.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (j > 0) {
|
|
|
|
written++;
|
|
|
|
|
|
|
|
if (tablesInfo[i].stsz > 0) {
|
|
|
|
tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (syncCount > 0) {
|
|
|
|
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is64) {
|
|
|
|
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
|
|
|
} else {
|
|
|
|
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
outRestore();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (auxBuffer != null) {
|
|
|
|
// dump moov
|
|
|
|
outSeek(ftyp_size);
|
|
|
|
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
|
|
|
|
auxBuffer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private Mp4DashSample getNextSample(int track) throws IOException {
|
|
|
|
if (readersChunks[track] == null) {
|
|
|
|
readersChunks[track] = readers[track].getNextChunk(false);
|
|
|
|
if (readersChunks[track] == null) {
|
|
|
|
return null;// EOF reached
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Mp4DashSample sample = readersChunks[track].getNextSample();
|
|
|
|
if (sample == null) {
|
|
|
|
readersChunks[track] = null;
|
|
|
|
return getNextSample(track);
|
|
|
|
} else {
|
|
|
|
return sample;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
|
|
|
private int writeEntry64(int offset, long value) throws IOException {
|
|
|
|
outBackup();
|
|
|
|
|
|
|
|
auxSeek(offset);
|
|
|
|
auxWrite(ByteBuffer.allocate(8).putLong(value).array());
|
|
|
|
|
|
|
|
return offset + 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int writeEntryArray(int offset, int count, int... values) throws IOException {
|
|
|
|
outBackup();
|
|
|
|
|
|
|
|
auxSeek(offset);
|
|
|
|
|
|
|
|
int size = count * 4;
|
|
|
|
ByteBuffer buffer = ByteBuffer.allocate(size);
|
|
|
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
buffer.putInt(values[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
auxWrite(buffer.array());
|
|
|
|
|
|
|
|
return offset + size;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void outBackup() {
|
|
|
|
if (auxBuffer == null && lastWriteOffset < 0) {
|
|
|
|
lastWriteOffset = writeOffset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Restore to the previous position before the first call to writeEntry64()
|
|
|
|
* or writeEntryArray() methods.
|
|
|
|
*/
|
|
|
|
private void outRestore() throws IOException {
|
|
|
|
if (lastWriteOffset > 0) {
|
|
|
|
outSeek(lastWriteOffset);
|
|
|
|
lastWriteOffset = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// </editor-fold>
|
|
|
|
|
|
|
|
// <editor-fold defaultstate="expanded" desc="Utils">
|
|
|
|
private void outWrite(byte[] buffer) throws IOException {
|
|
|
|
outWrite(buffer, 0, buffer.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
|
|
|
writeOffset += count;
|
|
|
|
outStream.write(buffer, offset, count);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void outSeek(long offset) throws IOException {
|
|
|
|
if (outStream.canSeek()) {
|
|
|
|
outStream.seek(offset);
|
|
|
|
writeOffset = offset;
|
|
|
|
} else if (outStream.canRewind()) {
|
|
|
|
outStream.rewind();
|
|
|
|
writeOffset = 0;
|
|
|
|
outSkip(offset);
|
|
|
|
} else {
|
|
|
|
throw new IOException("cannot seek or rewind the output stream");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void outSkip(long amount) throws IOException {
|
|
|
|
outStream.skip(amount);
|
|
|
|
writeOffset += amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int lengthFor(int offset) throws IOException {
|
|
|
|
int size = auxOffset() - offset;
|
|
|
|
|
|
|
|
if (moovSimulation) {
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
auxSeek(offset);
|
|
|
|
auxWrite(size);
|
|
|
|
auxSkip(size - 4);
|
|
|
|
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int make(int type, int extra, int columns, int rows) throws IOException {
|
|
|
|
final byte base = 16;
|
|
|
|
int size = columns * rows * 4;
|
|
|
|
int total = size + base;
|
|
|
|
int offset = auxOffset();
|
|
|
|
|
|
|
|
if (extra >= 0) {
|
|
|
|
total += 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
auxWrite(ByteBuffer.allocate(12)
|
|
|
|
.putInt(total)
|
|
|
|
.putInt(type)
|
|
|
|
.putInt(0x00)// default version & flags
|
|
|
|
.array()
|
|
|
|
);
|
|
|
|
|
|
|
|
if (extra >= 0) {
|
|
|
|
//size += 4;// commented for auxiliar buffer !!!
|
|
|
|
offset += 4;
|
|
|
|
auxWrite(extra);
|
|
|
|
}
|
|
|
|
|
|
|
|
auxWrite(rows);
|
|
|
|
auxSkip(size);
|
|
|
|
|
|
|
|
return offset + base;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void auxWrite(int value) throws IOException {
|
|
|
|
auxWrite(ByteBuffer.allocate(4)
|
|
|
|
.putInt(value)
|
|
|
|
.array()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void auxWrite(byte[] buffer) throws IOException {
|
|
|
|
if (moovSimulation) {
|
|
|
|
writeOffset += buffer.length;
|
|
|
|
} else if (auxBuffer == null) {
|
|
|
|
outWrite(buffer, 0, buffer.length);
|
|
|
|
} else {
|
|
|
|
auxBuffer.put(buffer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void auxSeek(int offset) throws IOException {
|
|
|
|
if (moovSimulation) {
|
|
|
|
writeOffset = offset;
|
|
|
|
} else if (auxBuffer == null) {
|
|
|
|
outSeek(offset);
|
|
|
|
} else {
|
|
|
|
auxBuffer.position(offset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void auxSkip(int amount) throws IOException {
|
|
|
|
if (moovSimulation) {
|
|
|
|
writeOffset += amount;
|
|
|
|
} else if (auxBuffer == null) {
|
|
|
|
outSkip(amount);
|
|
|
|
} else {
|
|
|
|
auxBuffer.position(auxBuffer.position() + amount);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private int auxOffset() {
|
|
|
|
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
|
|
|
|
}
|
|
|
|
// </editor-fold>
|
|
|
|
|
|
|
|
// <editor-fold defaultstate="expanded" desc="Box makers">
|
|
|
|
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)
|
|
|
|
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
|
|
|
|
|
|
|
|
outWrite(buffer);
|
|
|
|
|
|
|
|
return buffer.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
private byte[] make_mdat(long refSize, boolean is64) {
|
|
|
|
if (is64) {
|
|
|
|
refSize += 16;
|
|
|
|
} else {
|
|
|
|
refSize += 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
|
|
|
|
.putInt(is64 ? 0x01 : (int) refSize)
|
|
|
|
.putInt(0x6D646174);// mdat
|
|
|
|
|
|
|
|
if (is64) {
|
|
|
|
buffer.putLong(refSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
return buffer.array();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void make_mvhd(long longestTrack) throws IOException {
|
|
|
|
auxWrite(new byte[]{
|
|
|
|
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
|
|
|
});
|
|
|
|
auxWrite(ByteBuffer.allocate(28)
|
|
|
|
.putLong(time)
|
|
|
|
.putLong(time)
|
|
|
|
.putInt(DEFAULT_TIMESCALE)
|
|
|
|
.putLong(longestTrack)
|
|
|
|
.array()
|
|
|
|
);
|
|
|
|
|
|
|
|
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
|
|
|
|
// default matrix
|
|
|
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
0x40, 0x00, 0x00, 0x00
|
|
|
|
});
|
|
|
|
auxWrite(new byte[24]);// predefined
|
|
|
|
auxWrite(ByteBuffer.allocate(4)
|
|
|
|
.putInt(tracks.length + 1)
|
|
|
|
.array()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
|
|
|
|
int start = auxOffset();
|
|
|
|
|
|
|
|
auxWrite(new byte[]{
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
|
|
|
});
|
|
|
|
|
|
|
|
long longestTrack = 0;
|
|
|
|
long[] durations = new long[tracks.length];
|
|
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
|
|
if (durations[i] > longestTrack) {
|
|
|
|
longestTrack = durations[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
make_mvhd(longestTrack);
|
|
|
|
|
|
|
|
for (int i = 0; i < tracks.length; i++) {
|
|
|
|
if (tracks[i].trak.tkhd.matrix.length != 36) {
|
|
|
|
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
|
|
|
|
}
|
|
|
|
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
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
|
|
|
|
});
|
|
|
|
|
|
|
|
ByteBuffer buffer = ByteBuffer.allocate(48);
|
|
|
|
buffer.putLong(time);
|
|
|
|
buffer.putLong(time);
|
|
|
|
buffer.putInt(index + 1);
|
|
|
|
buffer.position(24);
|
|
|
|
buffer.putLong(duration);
|
|
|
|
buffer.position(40);
|
|
|
|
buffer.putShort(tracks[index].trak.tkhd.bLayer);
|
|
|
|
buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup);
|
|
|
|
buffer.putShort(tracks[index].trak.tkhd.bVolume);
|
|
|
|
auxWrite(buffer.array());
|
|
|
|
|
|
|
|
auxWrite(tracks[index].trak.tkhd.matrix);
|
|
|
|
auxWrite(ByteBuffer.allocate(8)
|
|
|
|
.putInt(tracks[index].trak.tkhd.bWidth)
|
|
|
|
.putInt(tracks[index].trak.tkhd.bHeight)
|
|
|
|
.array()
|
|
|
|
);
|
|
|
|
|
|
|
|
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
|
|
|
|
});
|
|
|
|
|
|
|
|
int bMediaRate;
|
|
|
|
int mediaTime;
|
|
|
|
|
|
|
|
if (tracks[index].trak.edst_elst == null) {
|
|
|
|
// is a audio track ¿is edst/elst opcional for audio tracks?
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
auxWrite(ByteBuffer
|
|
|
|
.allocate(12)
|
|
|
|
.putInt((int) duration)
|
|
|
|
.putInt(mediaTime)
|
|
|
|
.putInt(bMediaRate)
|
|
|
|
.array()
|
|
|
|
);
|
|
|
|
|
|
|
|
make_mdia(tracks[index].trak.mdia, tables, is64);
|
|
|
|
|
|
|
|
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
|
|
|
|
auxWrite(mdia.mdhd);
|
|
|
|
auxWrite(make_hdlr(mdia.hdlr));
|
|
|
|
|
|
|
|
int start_minf = 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);
|
|
|
|
|
|
|
|
//
|
|
|
|
// 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);
|
|
|
|
if (tablesInfo.stss > 0) {
|
|
|
|
make(0x73747373, -1, 1, tablesInfo.stss);
|
|
|
|
}
|
|
|
|
if (tablesInfo.ctts > 0) {
|
|
|
|
make(0x63747473, -1, 2, tablesInfo.ctts);
|
|
|
|
}
|
|
|
|
make(0x73747363, -1, 3, tablesInfo.stsc);
|
|
|
|
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
|
|
|
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
|
|
|
} else {
|
|
|
|
tablesInfo.stts = make(0x73747473, -1, 2, 1);
|
|
|
|
if (tablesInfo.stss > 0) {
|
|
|
|
tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss);
|
|
|
|
}
|
|
|
|
if (tablesInfo.ctts > 0) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
lengthFor(start_stbl);
|
|
|
|
lengthFor(start_minf);
|
|
|
|
lengthFor(start_mdia);
|
|
|
|
}
|
|
|
|
|
|
|
|
private byte[] make_hdlr(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
|
|
|
|
});
|
|
|
|
|
|
|
|
buffer.position(12);
|
|
|
|
buffer.putInt(hdlr.type);
|
|
|
|
buffer.putInt(hdlr.subType);
|
|
|
|
buffer.put(hdlr.bReserved);// always is a zero array
|
|
|
|
|
|
|
|
return buffer.array();
|
|
|
|
}
|
|
|
|
//</editor-fold>
|
|
|
|
|
|
|
|
class TablesInfo {
|
|
|
|
|
|
|
|
public int stts;
|
|
|
|
public int stsc;
|
|
|
|
public int[] stsc_bEntries;
|
|
|
|
public int ctts;
|
|
|
|
public int stsz;
|
|
|
|
public int stsz_default;
|
|
|
|
public int stss;
|
|
|
|
public int stco;
|
|
|
|
}
|
|
|
|
}
|