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
This commit is contained in:
kapodamy 2019-03-22 22:54:07 -03:00
parent 1684a2110c
commit 9e34fee58c
49 changed files with 2715 additions and 1936 deletions

View file

@ -1,6 +1,7 @@
package us.shandian.giga.postprocessing;
import android.os.Message;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
@ -9,17 +10,22 @@ import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
import us.shandian.giga.postprocessing.io.CircularFile;
import us.shandian.giga.postprocessing.io.CircularFileWriter;
import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker;
import us.shandian.giga.service.DownloadManagerService;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
public abstract class Postprocessing {
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
static final byte OK_RESULT = ERROR_NOTHING;
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
public static final String ALGORITHM_MP4_MUXER = "mp4";
public static final String ALGORITHM_WEBM_MUXER = "webm";
public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) {
@ -27,14 +33,14 @@ public abstract class Postprocessing {
} else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
return new TtmlConverter(mission);
case ALGORITHM_MP4_DASH_MUXER:
return new Mp4DashMuxer(mission);
case ALGORITHM_MP4_MUXER:
return new Mp4Muxer(mission);
case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission);
case ALGORITHM_MP4_FROM_DASH_MUXER:
return new Mp4FromDashMuxer(mission);
case ALGORITHM_M4A_NO_DASH:
return new M4aNoDash(mission);
/*case "example-algorithm":
return new ExampleAlgorithm(mission);*/
return new ExampleAlgorithm(mission);*/
default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
}
@ -65,7 +71,8 @@ public abstract class Postprocessing {
public void run() throws IOException {
File file = mission.getDownloadedFile();
CircularFile out = null;
File temp = null;
CircularFileWriter out = null;
int result;
long finalLength = -1;
@ -81,29 +88,54 @@ public abstract class Postprocessing {
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0};
CircularFile.OffsetChecker checker = () -> {
while (idx[0] < sources.length) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results
*/
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
idx[0]++;
continue;// the selected source is not used anymore
if (test(sources)) {
for (SharpStream source : sources) source.rewind();
OffsetChecker checker = () -> {
for (ChunkFileInputStream source : sources) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFileWriter can lead to unexpected results
*/
if (source.isDisposed() || source.available() < 1) {
continue;// the selected source is not used anymore
}
return source.getFilePointer() - 1;
}
return sources[idx[0]].getFilePointer() - 1;
}
return -1;
};
return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
temp = new File(mission.location, mission.name + ".tmp");
result = process(out, sources);
out = new CircularFileWriter(file, temp, checker);
out.onProgress = this::progressReport;
if (result == OK_RESULT)
finalLength = out.finalizeFile();
out.onWriteError = (err) -> {
mission.postprocessingState = 3;
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
try {
synchronized (this) {
while (mission.postprocessingState == 3)
wait();
}
} catch (InterruptedException e) {
// nothing to do
Log.e(this.getClass().getSimpleName(), "got InterruptedException");
}
return mission.errCode == ERROR_NOTHING;
};
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} else {
result = OK_RESULT;
}
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
@ -113,17 +145,22 @@ public abstract class Postprocessing {
if (out != null) {
out.dispose();
}
if (temp != null) {
//noinspection ResultOfMethodCallIgnored
temp.delete();
}
}
} else {
result = process(null);
result = test() ? process(null) : OK_RESULT;
}
if (result == OK_RESULT) {
if (finalLength < 0) finalLength = file.length();
mission.done = finalLength;
mission.length = finalLength;
if (finalLength != -1) {
mission.done = finalLength;
mission.length = finalLength;
}
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
@ -134,7 +171,18 @@ public abstract class Postprocessing {
}
/**
* Abstract method to execute the pos-processing algorithm
* Test if the post-processing algorithm can be skipped
*
* @param sources files to be processed
* @return {@code true} if the post-processing is required, otherwise, {@code false}
* @throws IOException if an I/O error occurs.
*/
boolean test(SharpStream... sources) throws IOException {
return true;
}
/**
* Abstract method to execute the post-processing algorithm
*
* @param out output stream
* @param sources files to be processed
@ -151,7 +199,7 @@ public abstract class Postprocessing {
return mission.postprocessingArgs[index];
}
void progressReport(long done) {
private void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;