Merge branch 'dev' into dev
This commit is contained in:
commit
98d7e6bcc6
@ -62,7 +62,7 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
})
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c420340ceb39'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:b6d3252'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
|
||||
|
||||
@ -68,6 +68,7 @@ import java.util.Locale;
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
@ -762,12 +763,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
Stream selectedStream;
|
||||
Stream secondaryStream = null;
|
||||
char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
MissionRecoveryInfo[] recoveryInfo;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
@ -778,18 +780,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
if (secondary != null) {
|
||||
secondaryStream = secondary.getStream();
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
@ -801,8 +805,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||
// does not work on slow networks but is later updated in the downloader
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondary.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -824,13 +828,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
return;
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
urls = new String[]{selectedStream.getUrl()};
|
||||
if (secondaryStream == null) {
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream)
|
||||
};
|
||||
} else {
|
||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl(), secondaryStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
|
||||
};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
DownloadManagerService.startMission(
|
||||
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
|
||||
);
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@ -26,14 +26,13 @@ import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@ -45,6 +44,10 @@ import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
@ -285,6 +288,17 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (captionPopupMenu == null) return;
|
||||
captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId);
|
||||
|
||||
String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.caption_user_set_key), null);
|
||||
/*
|
||||
* only search for autogenerated cc as fallback
|
||||
* if "(auto-generated)" was not already selected
|
||||
* we are only looking for "(" instead of "(auto-generated)" to hopefully get all
|
||||
* internationalized variants such as "(automatisch-erzeugt)" and so on
|
||||
*/
|
||||
boolean searchForAutogenerated = userPreferredLanguage != null &&
|
||||
!userPreferredLanguage.contains("(");
|
||||
|
||||
// Add option for turning off caption
|
||||
MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
|
||||
0, Menu.NONE, R.string.caption_none);
|
||||
@ -294,6 +308,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, true));
|
||||
}
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit();
|
||||
return true;
|
||||
});
|
||||
|
||||
@ -308,9 +324,26 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
|
||||
captionLanguage).commit();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// apply caption language from previous user preference
|
||||
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) ||
|
||||
searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) ||
|
||||
userPreferredLanguage.contains("(") &&
|
||||
captionLanguage.startsWith(userPreferredLanguage.substring(0,
|
||||
userPreferredLanguage.indexOf('('))))) {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
}
|
||||
searchForAutogenerated = false;
|
||||
}
|
||||
}
|
||||
captionPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
|
||||
*/
|
||||
public class Mp4DashReader {
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Constants">
|
||||
private static final int ATOM_MOOF = 0x6D6F6F66;
|
||||
private static final int ATOM_MFHD = 0x6D666864;
|
||||
private static final int ATOM_TRAF = 0x74726166;
|
||||
@ -50,7 +49,7 @@ public class Mp4DashReader {
|
||||
private static final int HANDLER_VIDE = 0x76696465;
|
||||
private static final int HANDLER_SOUN = 0x736F756E;
|
||||
private static final int HANDLER_SUBT = 0x73756274;
|
||||
// </editor-fold>
|
||||
|
||||
|
||||
private final DataReader stream;
|
||||
|
||||
@ -293,7 +292,8 @@ public class Mp4DashReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Utils">
|
||||
|
||||
|
||||
private long readUint() throws IOException {
|
||||
return stream.readInt() & 0xffffffffL;
|
||||
}
|
||||
@ -392,9 +392,7 @@ public class Mp4DashReader {
|
||||
return readBox();
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||
|
||||
private Moof parse_moof(Box ref, int trackId) throws IOException {
|
||||
Moof obj = new Moof();
|
||||
@ -795,9 +793,8 @@ public class Mp4DashReader {
|
||||
return readFullBox(b);
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Helper classes">
|
||||
|
||||
class Box {
|
||||
|
||||
int type;
|
||||
@ -1013,5 +1010,5 @@ public class Mp4DashReader {
|
||||
public TrunEntry info;
|
||||
public byte[] data;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ 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.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -22,6 +23,7 @@ public class Mp4FromDashWriter {
|
||||
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 final long time;
|
||||
|
||||
@ -145,7 +147,7 @@ public class Mp4FromDashWriter {
|
||||
// not allowed for very short tracks (less than 0.5 seconds)
|
||||
//
|
||||
outStream = output;
|
||||
int 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];
|
||||
@ -157,7 +159,9 @@ public class Mp4FromDashWriter {
|
||||
tablesInfo[i] = new TablesInfo();
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
||||
boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio;
|
||||
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
int samplesSize = 0;
|
||||
int sampleSizeChanges = 0;
|
||||
@ -210,14 +214,21 @@ public class Mp4FromDashWriter {
|
||||
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
|
||||
|
||||
tmp = tmp % SAMPLES_PER_CHUNK;
|
||||
if (tmp == 0) {
|
||||
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
|
||||
};
|
||||
} else {
|
||||
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
|
||||
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,
|
||||
@ -244,7 +255,7 @@ public class Mp4FromDashWriter {
|
||||
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
|
||||
boolean is64 = read > THRESHOLD_FOR_CO64;
|
||||
|
||||
@ -268,10 +279,10 @@ public class Mp4FromDashWriter {
|
||||
} else {*/
|
||||
if (auxSize > 0) {
|
||||
int length = auxSize;
|
||||
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
||||
byte[] buffer = new byte[64 * 1024];// 64 KiB
|
||||
while (length > 0) {
|
||||
int count = Math.min(length, buffer.length);
|
||||
outWrite(buffer, 0, count);
|
||||
outWrite(buffer, count);
|
||||
length -= count;
|
||||
}
|
||||
}
|
||||
@ -280,7 +291,7 @@ public class Mp4FromDashWriter {
|
||||
outSeek(ftyp_size);
|
||||
}
|
||||
|
||||
// tablesInfo contais row counts
|
||||
// tablesInfo contains row counts
|
||||
// and after returning from make_moov() will contain table offsets
|
||||
make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
@ -291,7 +302,7 @@ public class Mp4FromDashWriter {
|
||||
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
|
||||
sampleCount[i] = 1;// the index is not base zero
|
||||
sampleExtra[i] = -1;
|
||||
}
|
||||
}
|
||||
@ -303,8 +314,8 @@ public class Mp4FromDashWriter {
|
||||
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[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
|
||||
|
||||
int written = readers.length;
|
||||
while (written > 0) {
|
||||
@ -317,7 +328,12 @@ public class Mp4FromDashWriter {
|
||||
|
||||
long chunkOffset = writeOffset;
|
||||
int syncCount = 0;
|
||||
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
int limit;
|
||||
if (singleChunk) {
|
||||
limit = SINGLE_CHUNK_SAMPLE_BUFFER;
|
||||
} else {
|
||||
limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
}
|
||||
|
||||
int j = 0;
|
||||
for (; j < limit; j++) {
|
||||
@ -354,7 +370,7 @@ public class Mp4FromDashWriter {
|
||||
sizes[j] = sample.data.length;
|
||||
}
|
||||
|
||||
outWrite(sample.data, 0, sample.data.length);
|
||||
outWrite(sample.data, sample.data.length);
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
@ -368,10 +384,16 @@ public class Mp4FromDashWriter {
|
||||
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);
|
||||
if (tablesInfo[i].stco > 0) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
outRestore();
|
||||
@ -404,7 +426,7 @@ public class Mp4FromDashWriter {
|
||||
}
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
||||
|
||||
private int writeEntry64(int offset, long value) throws IOException {
|
||||
outBackup();
|
||||
|
||||
@ -447,16 +469,16 @@ public class Mp4FromDashWriter {
|
||||
lastWriteOffset = -1;
|
||||
}
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Utils">
|
||||
|
||||
|
||||
private void outWrite(byte[] buffer) throws IOException {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
outWrite(buffer, buffer.length);
|
||||
}
|
||||
|
||||
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||
private void outWrite(byte[] buffer, int count) throws IOException {
|
||||
writeOffset += count;
|
||||
outStream.write(buffer, offset, count);
|
||||
outStream.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
private void outSeek(long offset) throws IOException {
|
||||
@ -509,7 +531,6 @@ public class Mp4FromDashWriter {
|
||||
);
|
||||
|
||||
if (extra >= 0) {
|
||||
//size += 4;// commented for auxiliar buffer !!!
|
||||
offset += 4;
|
||||
auxWrite(extra);
|
||||
}
|
||||
@ -531,7 +552,7 @@ public class Mp4FromDashWriter {
|
||||
if (moovSimulation) {
|
||||
writeOffset += buffer.length;
|
||||
} else if (auxBuffer == null) {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
outWrite(buffer, buffer.length);
|
||||
} else {
|
||||
auxBuffer.put(buffer);
|
||||
}
|
||||
@ -560,9 +581,9 @@ public class Mp4FromDashWriter {
|
||||
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
|
||||
@ -703,7 +724,7 @@ public class Mp4FromDashWriter {
|
||||
int mediaTime;
|
||||
|
||||
if (tracks[index].trak.edst_elst == null) {
|
||||
// is a audio track ¿is edst/elst opcional for audio tracks?
|
||||
// is a audio track ¿is edst/elst optional for audio tracks?
|
||||
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
|
||||
bMediaRate = 0x00010000;
|
||||
} else {
|
||||
@ -794,17 +815,17 @@ public class Mp4FromDashWriter {
|
||||
|
||||
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;
|
||||
int stts;
|
||||
int stsc;
|
||||
int[] stsc_bEntries;
|
||||
int ctts;
|
||||
int stsz;
|
||||
int stsz_default;
|
||||
int stss;
|
||||
int stco;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,431 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import javax.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 final static int TIME_SCALE_NS = 1000000000;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private SharpStream source;
|
||||
private SharpStream output;
|
||||
|
||||
private int sequence_count = 0;
|
||||
private final int STREAM_ID;
|
||||
private byte packet_flag = 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 long webm_block_last_timecode = 0;
|
||||
private long webm_block_near_duration = 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 final int[] crc32_table = new int[256];
|
||||
|
||||
public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) {
|
||||
if (!source.canRead() || !source.canRewind()) {
|
||||
throw new IllegalArgumentException("source stream must be readable and allows seeking");
|
||||
}
|
||||
if (!target.canWrite() || !target.canRewind()) {
|
||||
throw new IllegalArgumentException("output stream must be writable and allows seeking");
|
||||
}
|
||||
|
||||
this.source = source;
|
||||
this.output = target;
|
||||
|
||||
this.STREAM_ID = (int) System.currentTimeMillis();
|
||||
|
||||
populate_crc32_table();
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("source must be parsed first");
|
||||
}
|
||||
|
||||
return webm.getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSource() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
webm = new WebMReader(source);
|
||||
webm.parse();
|
||||
webm_segment = webm.getNextSegment();
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTrack(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) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
switch (webm.getAvailableTracks()[trackIndex].kind) {
|
||||
case Audio:
|
||||
case Video:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("the track must an audio or video stream");
|
||||
}
|
||||
|
||||
try {
|
||||
webm_track = webm.selectTrack(trackIndex);
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
webm_track = null;
|
||||
webm = null;
|
||||
|
||||
if (!output.isClosed()) {
|
||||
output.flush();
|
||||
}
|
||||
|
||||
source.close();
|
||||
output.close();
|
||||
}
|
||||
|
||||
public void build() throws IOException {
|
||||
float resolution;
|
||||
SimpleBlock bloq;
|
||||
ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
|
||||
ByteBuffer page = ByteBuffer.allocate(64 * 1024);
|
||||
|
||||
header.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
/* step 1: get the amount of frames per seconds */
|
||||
switch (webm_track.kind) {
|
||||
case Audio:
|
||||
resolution = getSampleFrequencyFromTrack(webm_track.bMetadata);
|
||||
if (resolution == 0f) {
|
||||
throw new RuntimeException("cannot get the audio sample rate");
|
||||
}
|
||||
break;
|
||||
case Video:
|
||||
// WARNING: untested
|
||||
if (webm_track.defaultDuration == 0) {
|
||||
throw new RuntimeException("missing default frame time");
|
||||
}
|
||||
resolution = 1000f / ((float) webm_track.defaultDuration / webm_segment.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);
|
||||
write(header);
|
||||
output.write(webm_track.codecPrivate);
|
||||
}
|
||||
|
||||
/* step 3: create packet with metadata */
|
||||
byte[] buffer = make_metadata();
|
||||
if (buffer != null) {
|
||||
addPacketSegment(buffer.length);
|
||||
make_packetHeader(0x00, header, buffer);
|
||||
write(header);
|
||||
output.write(buffer);
|
||||
}
|
||||
|
||||
/* step 4: calculate amount of packets */
|
||||
while (webm_segment != null) {
|
||||
bloq = getNextBlock();
|
||||
|
||||
if (bloq != null && addPacketSegment(bloq)) {
|
||||
int pos = page.position();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
bloq.data.read(page.array(), pos, bloq.dataSize);
|
||||
page.position(pos + bloq.dataSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// calculate the current packet duration using the next block
|
||||
double elapsed_ns = webm_track.codecDelay;
|
||||
|
||||
if (bloq == null) {
|
||||
packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed
|
||||
elapsed_ns += webm_block_last_timecode;
|
||||
|
||||
if (webm_track.defaultDuration > 0) {
|
||||
elapsed_ns += webm_track.defaultDuration;
|
||||
} else {
|
||||
// hardcoded way, guess the sample duration
|
||||
elapsed_ns += webm_block_near_duration;
|
||||
}
|
||||
} else {
|
||||
elapsed_ns += bloq.absoluteTimeCodeNs;
|
||||
}
|
||||
|
||||
// get the sample count in the page
|
||||
elapsed_ns = elapsed_ns / TIME_SCALE_NS;
|
||||
elapsed_ns = Math.ceil(elapsed_ns * resolution);
|
||||
|
||||
// create header and calculate page checksum
|
||||
int checksum = make_packetHeader((long) elapsed_ns, header, null);
|
||||
checksum = calc_crc32(checksum, page.array(), page.position());
|
||||
|
||||
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
|
||||
|
||||
// dump data
|
||||
write(header);
|
||||
write(page);
|
||||
|
||||
webm_block = bloq;
|
||||
}
|
||||
}
|
||||
|
||||
private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) {
|
||||
short length = HEADER_SIZE;
|
||||
|
||||
buffer.putInt(0x5367674f);// "OggS" binary string in little-endian
|
||||
buffer.put((byte) 0x00);// version
|
||||
buffer.put(packet_flag);// type
|
||||
|
||||
buffer.putLong(gran_pos);// granulate position
|
||||
|
||||
buffer.putInt(STREAM_ID);// bitstream serial number
|
||||
buffer.putInt(sequence_count++);// page sequence number
|
||||
|
||||
buffer.putInt(0x00);// page checksum
|
||||
|
||||
buffer.put((byte) segment_table_size);// segment table
|
||||
buffer.put(segment_table, 0, segment_table_size);// segment size
|
||||
|
||||
length += segment_table_size;
|
||||
|
||||
clearSegmentTable();// clear segment table for next header
|
||||
|
||||
int checksum_crc32 = calc_crc32(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;
|
||||
}
|
||||
|
||||
return checksum_crc32;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private byte[] make_metadata() {
|
||||
if ("A_OPUS".equals(webm_track.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)
|
||||
};
|
||||
} else if ("A_VORBIS".equals(webm_track.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// ????????
|
||||
};
|
||||
}
|
||||
|
||||
// not implemented for the desired codec
|
||||
return null;
|
||||
}
|
||||
|
||||
private void write(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;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (webm_segment == null) {
|
||||
webm_segment = webm.getNextSegment();
|
||||
if (webm_segment == 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;
|
||||
return getNextBlock();
|
||||
}
|
||||
}
|
||||
|
||||
res = webm_cluster.getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
webm_cluster = null;
|
||||
return getNextBlock();
|
||||
}
|
||||
|
||||
webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode;
|
||||
webm_block_last_timecode = res.absoluteTimeCodeNs;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private float getSampleFrequencyFromTrack(byte[] bMetadata) {
|
||||
// hardcoded way
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
|
||||
|
||||
while (buffer.remaining() >= 6) {
|
||||
int id = buffer.getShort() & 0xFFFF;
|
||||
if (id == 0x0000B584) {
|
||||
return buffer.getFloat();
|
||||
}
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void clearSegmentTable() {
|
||||
segment_table_next_timestamp += TIME_SCALE_NS;
|
||||
packet_flag = FLAG_UNSET;
|
||||
segment_table_size = 0;
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(SimpleBlock block) {
|
||||
long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay;
|
||||
|
||||
if (timestamp >= segment_table_next_timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return addPacketSegment(block.dataSize);
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(int size) {
|
||||
if (size > 65025) {
|
||||
throw new UnsupportedOperationException("page size cannot be larger than 65025");
|
||||
}
|
||||
|
||||
int available = (segment_table.length - segment_table_size) * 255;
|
||||
boolean extra = (size % 255) == 0;
|
||||
|
||||
if (extra) {
|
||||
// add a zero byte entry in the table
|
||||
// required to indicate the sample size is multiple of 255
|
||||
available -= 255;
|
||||
}
|
||||
|
||||
// check if possible add the segment, without overflow the table
|
||||
if (available < size) {
|
||||
return false;// not enough space on the page
|
||||
}
|
||||
|
||||
for (; size > 0; size -= 255) {
|
||||
segment_table[segment_table_size++] = (byte) Math.min(size, 255);
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
segment_table[segment_table_size++] = 0x00;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void populate_crc32_table() {
|
||||
for (int i = 0; i < 0x100; i++) {
|
||||
int crc = i << 24;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
long b = crc >>> 31;
|
||||
crc <<= 1;
|
||||
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
|
||||
}
|
||||
crc32_table[i] = crc;
|
||||
}
|
||||
}
|
||||
|
||||
private int calc_crc32(int initial_crc, byte[] buffer, int size) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
int reg = (initial_crc >>> 24) & 0xff;
|
||||
initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)];
|
||||
}
|
||||
|
||||
return initial_crc;
|
||||
}
|
||||
|
||||
}
|
||||
@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
|
||||
*/
|
||||
public class WebMReader {
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="constants">
|
||||
private final static int ID_EMBL = 0x0A45DFA3;
|
||||
private final static int ID_EMBLReadVersion = 0x02F7;
|
||||
private final static int ID_EMBLDocType = 0x0282;
|
||||
@ -37,11 +36,14 @@ public class WebMReader {
|
||||
private final static int ID_Audio = 0x61;
|
||||
private final static int ID_DefaultDuration = 0x3E383;
|
||||
private final static int ID_FlagLacing = 0x1C;
|
||||
private final static int ID_CodecDelay = 0x16AA;
|
||||
|
||||
private final static int ID_Cluster = 0x0F43B675;
|
||||
private final static int ID_Timecode = 0x67;
|
||||
private final static int ID_SimpleBlock = 0x23;
|
||||
//</editor-fold>
|
||||
private final static int ID_Block = 0x21;
|
||||
private final static int ID_GroupBlock = 0x20;
|
||||
|
||||
|
||||
public enum TrackKind {
|
||||
Audio/*2*/, Video/*1*/, Other
|
||||
@ -96,7 +98,7 @@ public class WebMReader {
|
||||
}
|
||||
|
||||
ensure(segment.ref);
|
||||
|
||||
// WARNING: track cannot be the same or have different index in new segments
|
||||
Element elem = untilElement(null, ID_Segment);
|
||||
if (elem == null) {
|
||||
done = true;
|
||||
@ -107,7 +109,8 @@ public class WebMReader {
|
||||
return segment;
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="utils">
|
||||
|
||||
|
||||
private long readNumber(Element parent) throws IOException {
|
||||
int length = (int) parent.contentSize;
|
||||
long value = 0;
|
||||
@ -189,6 +192,9 @@ public class WebMReader {
|
||||
Element elem;
|
||||
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
||||
elem = readElement();
|
||||
if (expected.length < 1) {
|
||||
return elem;
|
||||
}
|
||||
for (int type : expected) {
|
||||
if (elem.type == type) {
|
||||
return elem;
|
||||
@ -219,9 +225,9 @@ public class WebMReader {
|
||||
|
||||
stream.skipBytes(skip);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="elements readers">
|
||||
|
||||
|
||||
private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException {
|
||||
Element elem = untilElement(ref, ID_EMBLReadVersion);
|
||||
if (elem == null) {
|
||||
@ -300,9 +306,7 @@ public class WebMReader {
|
||||
WebMTrack entry = new WebMTrack();
|
||||
boolean drop = false;
|
||||
Element elem;
|
||||
while ((elem = untilElement(elem_trackEntry,
|
||||
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
|
||||
)) != null) {
|
||||
while ((elem = untilElement(elem_trackEntry)) != null) {
|
||||
switch (elem.type) {
|
||||
case ID_TrackNumber:
|
||||
entry.trackNumber = readNumber(elem);
|
||||
@ -326,8 +330,9 @@ public class WebMReader {
|
||||
case ID_FlagLacing:
|
||||
drop = readNumber(elem) != lacingExpected;
|
||||
break;
|
||||
case ID_CodecDelay:
|
||||
entry.codecDelay = readNumber(elem);
|
||||
default:
|
||||
System.out.println();
|
||||
break;
|
||||
}
|
||||
ensure(elem);
|
||||
@ -360,12 +365,13 @@ public class WebMReader {
|
||||
|
||||
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
|
||||
SimpleBlock obj = new SimpleBlock(ref);
|
||||
obj.dataSize = stream.position();
|
||||
obj.trackNumber = readEncodedNumber();
|
||||
obj.relativeTimeCode = stream.readShort();
|
||||
obj.flags = (byte) stream.read();
|
||||
obj.dataSize = (ref.offset + ref.size) - stream.position();
|
||||
obj.dataSize = (int) ((ref.offset + ref.size) - stream.position());
|
||||
obj.createdFromBlock = ref.type == ID_Block;
|
||||
|
||||
// NOTE: lacing is not implemented, and will be mixed with the stream data
|
||||
if (obj.dataSize < 0) {
|
||||
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
||||
}
|
||||
@ -383,9 +389,9 @@ public class WebMReader {
|
||||
|
||||
return obj;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="class helpers">
|
||||
|
||||
|
||||
class Element {
|
||||
|
||||
int type;
|
||||
@ -409,6 +415,7 @@ public class WebMReader {
|
||||
public byte[] bMetadata;
|
||||
public TrackKind kind;
|
||||
public long defaultDuration;
|
||||
public long codecDelay;
|
||||
}
|
||||
|
||||
public class Segment {
|
||||
@ -448,6 +455,7 @@ public class WebMReader {
|
||||
public class SimpleBlock {
|
||||
|
||||
public InputStream data;
|
||||
public boolean createdFromBlock;
|
||||
|
||||
SimpleBlock(Element ref) {
|
||||
this.ref = ref;
|
||||
@ -455,8 +463,9 @@ public class WebMReader {
|
||||
|
||||
public long trackNumber;
|
||||
public short relativeTimeCode;
|
||||
public long absoluteTimeCodeNs;
|
||||
public byte flags;
|
||||
public long dataSize;
|
||||
public int dataSize;
|
||||
private final Element ref;
|
||||
|
||||
public boolean isKeyframe() {
|
||||
@ -468,33 +477,55 @@ public class WebMReader {
|
||||
|
||||
Element ref;
|
||||
SimpleBlock currentSimpleBlock = null;
|
||||
Element currentBlockGroup = null;
|
||||
public long timecode;
|
||||
|
||||
Cluster(Element ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
boolean check() {
|
||||
boolean insideClusterBounds() {
|
||||
return stream.position() >= (ref.offset + ref.size);
|
||||
}
|
||||
|
||||
public SimpleBlock getNextSimpleBlock() throws IOException {
|
||||
if (check()) {
|
||||
if (insideClusterBounds()) {
|
||||
return null;
|
||||
}
|
||||
if (currentSimpleBlock != null) {
|
||||
|
||||
if (currentBlockGroup != null) {
|
||||
ensure(currentBlockGroup);
|
||||
currentBlockGroup = null;
|
||||
currentSimpleBlock = null;
|
||||
} else if (currentSimpleBlock != null) {
|
||||
ensure(currentSimpleBlock.ref);
|
||||
}
|
||||
|
||||
while (!check()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock);
|
||||
while (!insideClusterBounds()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock);
|
||||
if (elem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (elem.type == ID_GroupBlock) {
|
||||
currentBlockGroup = elem;
|
||||
elem = untilElement(currentBlockGroup, ID_Block);
|
||||
|
||||
if (elem == null) {
|
||||
ensure(currentBlockGroup);
|
||||
currentBlockGroup = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentSimpleBlock = readSimpleBlock(elem);
|
||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
||||
|
||||
// calculate the timestamp in nanoseconds
|
||||
currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode;
|
||||
currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale;
|
||||
|
||||
return currentSimpleBlock;
|
||||
}
|
||||
|
||||
@ -505,5 +536,5 @@ public class WebMReader {
|
||||
}
|
||||
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -17,7 +18,7 @@ import java.util.ArrayList;
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMWriter {
|
||||
public class WebMWriter implements Closeable {
|
||||
|
||||
private final static int BUFFER_SIZE = 8 * 1024;
|
||||
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
|
||||
@ -35,7 +36,7 @@ public class WebMWriter {
|
||||
private long written = 0;
|
||||
|
||||
private Segment[] readersSegment;
|
||||
private Cluster[] readersCluter;
|
||||
private Cluster[] readersCluster;
|
||||
|
||||
private int[] predefinedDurations;
|
||||
|
||||
@ -81,7 +82,7 @@ public class WebMWriter {
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
try {
|
||||
readersSegment = new Segment[readers.length];
|
||||
readersCluter = new Cluster[readers.length];
|
||||
readersCluster = new Cluster[readers.length];
|
||||
predefinedDurations = new int[readers.length];
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
@ -102,6 +103,7 @@ public class WebMWriter {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
done = true;
|
||||
parsed = true;
|
||||
@ -114,7 +116,7 @@ public class WebMWriter {
|
||||
readers = null;
|
||||
infoTracks = null;
|
||||
readersSegment = null;
|
||||
readersCluter = null;
|
||||
readersCluster = null;
|
||||
outBuffer = null;
|
||||
}
|
||||
|
||||
@ -247,7 +249,7 @@ public class WebMWriter {
|
||||
nextCueTime += DEFAULT_CUES_EACH_MS;
|
||||
}
|
||||
keyFrames.add(
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 8, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -334,17 +336,17 @@ public class WebMWriter {
|
||||
}
|
||||
}
|
||||
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
if (readersCluster[internalTrackId] == null) {
|
||||
readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluster[internalTrackId] == null) {
|
||||
readersSegment[internalTrackId] = null;
|
||||
return getNextBlockFrom(internalTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
|
||||
SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
readersCluter[internalTrackId] = null;
|
||||
readersCluster[internalTrackId] = null;
|
||||
return new Block();// fake block to indicate the end of the cluster
|
||||
}
|
||||
|
||||
@ -353,16 +355,11 @@ public class WebMWriter {
|
||||
bloq.dataSize = (int) res.dataSize;
|
||||
bloq.trackNumber = internalTrackId;
|
||||
bloq.flags = res.flags;
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
||||
bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE;
|
||||
|
||||
return bloq;
|
||||
}
|
||||
|
||||
private short convertTimecode(int time, long oldTimeScale) {
|
||||
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
|
||||
}
|
||||
|
||||
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||
if (stream.canSeek()) {
|
||||
stream.seek(offset);
|
||||
|
||||
@ -52,10 +52,12 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||
}
|
||||
}
|
||||
|
||||
if (m4v) return null;
|
||||
|
||||
// retry, but this time in reverse order
|
||||
for (int i = audioStreams.size() - 1; i >= 0; i--) {
|
||||
AudioStream audio = audioStreams.get(i);
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
|
||||
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -13,6 +15,7 @@ import java.nio.channels.ClosedByInterruptException;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
public class DownloadInitializer extends Thread {
|
||||
private final static String TAG = "DownloadInitializer";
|
||||
@ -28,9 +31,9 @@ public class DownloadInitializer extends Thread {
|
||||
mConn = null;
|
||||
}
|
||||
|
||||
private static void safeClose(HttpURLConnection con) {
|
||||
private void dispose() {
|
||||
try {
|
||||
con.getInputStream().close();
|
||||
mConn.getInputStream().close();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
@ -51,9 +54,9 @@ public class DownloadInitializer extends Thread {
|
||||
long lowestSize = Long.MAX_VALUE;
|
||||
|
||||
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
|
||||
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
|
||||
mConn = mMission.openConnection(mMission.urls[i], true, -1, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (Thread.interrupted()) return;
|
||||
long length = Utility.getContentLength(mConn);
|
||||
@ -81,9 +84,9 @@ public class DownloadInitializer extends Thread {
|
||||
}
|
||||
} else {
|
||||
// ask for the current resource length
|
||||
mConn = mMission.openConnection(mId, -1, -1);
|
||||
mConn = mMission.openConnection(true, -1, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
@ -107,9 +110,9 @@ public class DownloadInitializer extends Thread {
|
||||
}
|
||||
} else {
|
||||
// Open again
|
||||
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
||||
mConn = mMission.openConnection(true, mMission.length - 10, mMission.length);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
@ -151,12 +154,33 @@ public class DownloadInitializer extends Thread {
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
if (!mMission.unknownLength && mMission.recoveryInfo != null) {
|
||||
String entityTag = mConn.getHeaderField("ETAG");
|
||||
String lastModified = mConn.getHeaderField("Last-Modified");
|
||||
MissionRecoveryInfo recovery = mMission.recoveryInfo[mMission.current];
|
||||
|
||||
if (!TextUtils.isEmpty(entityTag)) {
|
||||
recovery.validateCondition = entityTag;
|
||||
} else if (!TextUtils.isEmpty(lastModified)) {
|
||||
recovery.validateCondition = lastModified;// Note: this is less precise
|
||||
} else {
|
||||
recovery.validateCondition = null;
|
||||
}
|
||||
}
|
||||
|
||||
mMission.running = false;
|
||||
break;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running) return;
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired
|
||||
interrupt();
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
||||
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
||||
@ -179,13 +203,6 @@ public class DownloadInitializer extends Thread {
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
|
||||
if (mConn != null) {
|
||||
try {
|
||||
mConn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
if (mConn != null) dispose();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,313 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||
|
||||
public class DownloadMissionRecover extends Thread {
|
||||
private static final String TAG = "DownloadMissionRecover";
|
||||
static final int mID = -3;
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final boolean mNotInitialized;
|
||||
|
||||
private final int mErrCode;
|
||||
|
||||
private HttpURLConnection mConn;
|
||||
private MissionRecoveryInfo mRecovery;
|
||||
private StreamExtractor mExtractor;
|
||||
|
||||
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
||||
mMission = mission;
|
||||
mNotInitialized = mission.blocks == null && mission.current == 0;
|
||||
mErrCode = errCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMission.source == null) {
|
||||
mMission.notifyError(mErrCode, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception err = null;
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt++ < mMission.maxRetry) {
|
||||
try {
|
||||
tryRecover();
|
||||
return;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
// give up
|
||||
mMission.notifyError(mErrCode, err);
|
||||
}
|
||||
|
||||
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
||||
if (mExtractor == null) {
|
||||
try {
|
||||
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
||||
mExtractor = svr.getStreamExtractor(mMission.source);
|
||||
mExtractor.fetchPage();
|
||||
} catch (ExtractionException e) {
|
||||
mExtractor = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// maybe the following check is redundant
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (!mNotInitialized) {
|
||||
// set the current download url to null in case if the recovery
|
||||
// process is canceled. Next time start() method is called the
|
||||
// recovery will be executed, saving time
|
||||
mMission.urls[mMission.current] = null;
|
||||
|
||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||
resolveStream();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "mission is not fully initialized, this will take a while");
|
||||
|
||||
try {
|
||||
for (; mMission.current < mMission.urls.length; mMission.current++) {
|
||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||
|
||||
if (test()) continue;
|
||||
if (!mMission.running) return;
|
||||
|
||||
resolveStream();
|
||||
if (!mMission.running) return;
|
||||
|
||||
// before continue, check if the current stream was resolved
|
||||
if (mMission.urls[mMission.current] == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mMission.current = 0;
|
||||
}
|
||||
|
||||
mMission.writeThisToFile();
|
||||
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
||||
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
||||
/*if (mExtractor.getErrorMessage() != null) {
|
||||
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
||||
return;
|
||||
}*/
|
||||
|
||||
String url = null;
|
||||
|
||||
switch (mRecovery.kind) {
|
||||
case 'a':
|
||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
||||
if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) {
|
||||
url = audio.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
List<VideoStream> videoStreams;
|
||||
if (mRecovery.desired2)
|
||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||
else
|
||||
videoStreams = mExtractor.getVideoStreams();
|
||||
for (VideoStream video : videoStreams) {
|
||||
if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) {
|
||||
url = video.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) {
|
||||
String tag = subtitles.getLanguageTag();
|
||||
if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) {
|
||||
url = subtitles.getURL();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown stream type");
|
||||
}
|
||||
|
||||
resolve(url);
|
||||
}
|
||||
|
||||
private void resolve(String url) throws IOException, HttpError {
|
||||
if (mRecovery.validateCondition == null) {
|
||||
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
||||
}
|
||||
|
||||
if (mMission.unknownLength || mRecovery.validateCondition == null) {
|
||||
recover(url, false);
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
////// Validate the http resource doing a range request
|
||||
/////////////////////
|
||||
try {
|
||||
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
|
||||
mConn.setRequestProperty("If-Range", mRecovery.validateCondition);
|
||||
mMission.establishConnection(mID, mConn);
|
||||
|
||||
int code = mConn.getResponseCode();
|
||||
|
||||
switch (code) {
|
||||
case 200:
|
||||
case 413:
|
||||
// stale
|
||||
recover(url, true);
|
||||
return;
|
||||
case 206:
|
||||
// in case of validation using the Last-Modified date, check the resource length
|
||||
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
|
||||
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
|
||||
|
||||
recover(url, lengthMismatch);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new HttpError(code);
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void recover(String url, boolean stale) {
|
||||
Log.i(TAG,
|
||||
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
|
||||
);
|
||||
|
||||
mMission.urls[mMission.current] = url;
|
||||
|
||||
if (url == null) {
|
||||
mMission.urls = new String[0];
|
||||
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mNotInitialized) return;
|
||||
|
||||
if (stale) {
|
||||
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
||||
}
|
||||
|
||||
mMission.writeThisToFile();
|
||||
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private long[] parseContentRange(String value) {
|
||||
long[] range = new long[3];
|
||||
|
||||
if (value == null) {
|
||||
// this never should happen
|
||||
return range;
|
||||
}
|
||||
|
||||
try {
|
||||
value = value.trim();
|
||||
|
||||
if (!value.startsWith("bytes")) {
|
||||
return range;// unknown range type
|
||||
}
|
||||
|
||||
int space = value.lastIndexOf(' ') + 1;
|
||||
int dash = value.indexOf('-', space) + 1;
|
||||
int bar = value.indexOf('/', dash);
|
||||
|
||||
// start
|
||||
range[0] = Long.parseLong(value.substring(space, dash - 1));
|
||||
|
||||
// end
|
||||
range[1] = Long.parseLong(value.substring(dash, bar));
|
||||
|
||||
// resource length
|
||||
value = value.substring(bar + 1);
|
||||
if (value.equals("*")) {
|
||||
range[2] = -1;// unknown length received from the server but should be valid
|
||||
} else {
|
||||
range[2] = Long.parseLong(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private boolean test() {
|
||||
if (mMission.urls[mMission.current] == null) return false;
|
||||
|
||||
try {
|
||||
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
|
||||
mMission.establishConnection(mID, mConn);
|
||||
|
||||
if (mConn.getResponseCode() == 200) return true;
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
try {
|
||||
try {
|
||||
mConn.getInputStream().close();
|
||||
} finally {
|
||||
mConn.disconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
mConn = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
if (mConn != null) disconnect();
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,10 @@ import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.Block;
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
|
||||
/**
|
||||
@ -19,7 +21,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
* an error occurs or the process is stopped.
|
||||
*/
|
||||
public class DownloadRunnable extends Thread {
|
||||
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
||||
private static final String TAG = "DownloadRunnable";
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final int mId;
|
||||
@ -41,13 +43,7 @@ public class DownloadRunnable extends Thread {
|
||||
public void run() {
|
||||
boolean retry = false;
|
||||
Block block = null;
|
||||
|
||||
int retryCount = 0;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||
}
|
||||
|
||||
SharpStream f;
|
||||
|
||||
try {
|
||||
@ -84,13 +80,14 @@ public class DownloadRunnable extends Thread {
|
||||
}
|
||||
|
||||
try {
|
||||
mConn = mMission.openConnection(mId, start, end);
|
||||
mConn = mMission.openConnection(false, start, end);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416) {
|
||||
if (block.done > 0) {
|
||||
// try again from the start (of the block)
|
||||
mMission.notifyProgress(-block.done);
|
||||
block.done = 0;
|
||||
retry = true;
|
||||
mConn.disconnect();
|
||||
@ -118,7 +115,7 @@ public class DownloadRunnable extends Thread {
|
||||
int len;
|
||||
|
||||
// use always start <= end
|
||||
// fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly
|
||||
// fixes a deadlock because in some videos, youtube is sending one byte alone
|
||||
while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
|
||||
f.write(buf, 0, len);
|
||||
start += len;
|
||||
@ -133,6 +130,17 @@ public class DownloadRunnable extends Thread {
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
f.close();
|
||||
|
||||
if (mId == 1) {
|
||||
// only the first thread will execute the recovery procedure
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
break;
|
||||
@ -144,11 +152,7 @@ public class DownloadRunnable extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
f.close();
|
||||
} catch (Exception err) {
|
||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "thread " + mId + " exited from main download loop");
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -10,9 +11,11 @@ import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
/**
|
||||
* Single-threaded fallback mode
|
||||
@ -33,7 +36,11 @@ public class DownloadRunnableFallback extends Thread {
|
||||
|
||||
private void dispose() {
|
||||
try {
|
||||
if (mIs != null) mIs.close();
|
||||
try {
|
||||
if (mIs != null) mIs.close();
|
||||
} finally {
|
||||
mConn.disconnect();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
@ -41,22 +48,10 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (mF != null) mF.close();
|
||||
}
|
||||
|
||||
private long loadPosition() {
|
||||
synchronized (mMission.LOCK) {
|
||||
return mMission.fallbackResumeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private void savePosition(long position) {
|
||||
synchronized (mMission.LOCK) {
|
||||
mMission.fallbackResumeOffset = position;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean done;
|
||||
long start = loadPosition();
|
||||
long start = mMission.fallbackResumeOffset;
|
||||
|
||||
if (DEBUG && !mMission.unknownLength && start > 0) {
|
||||
Log.i(TAG, "Resuming a single-thread download at " + start);
|
||||
@ -66,11 +61,18 @@ public class DownloadRunnableFallback extends Thread {
|
||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||
|
||||
int mId = 1;
|
||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||
mConn = mMission.openConnection(false, rangeStart, -1);
|
||||
|
||||
if (mRetryCount == 0 && rangeStart == -1) {
|
||||
// workaround: bypass android connection pool
|
||||
mConn.setRequestProperty("Range", "bytes=0-");
|
||||
}
|
||||
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416 && start > 0) {
|
||||
mMission.notifyProgress(-start);
|
||||
start = 0;
|
||||
mRetryCount--;
|
||||
throw new DownloadMission.HttpError(416);
|
||||
@ -80,12 +82,17 @@ public class DownloadRunnableFallback extends Thread {
|
||||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
if (mMission.unknownLength || mConn.getResponseCode() == 200) {
|
||||
// restart amount of bytes downloaded
|
||||
mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0];
|
||||
}
|
||||
|
||||
mF = mMission.storage.getStream();
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
mIs = mConn.getInputStream();
|
||||
|
||||
byte[] buf = new byte[64 * 1024];
|
||||
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
|
||||
int len = 0;
|
||||
|
||||
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
|
||||
@ -94,15 +101,24 @@ public class DownloadRunnableFallback extends Thread {
|
||||
mMission.notifyProgress(len);
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
// if thread goes interrupted check if the last part is written. This avoid re-download the whole file
|
||||
done = len == -1;
|
||||
} catch (Exception e) {
|
||||
dispose();
|
||||
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
dispose();
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRetryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
@ -116,12 +132,10 @@ public class DownloadRunnableFallback extends Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
if (done) {
|
||||
mMission.notifyFinished();
|
||||
} else {
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,17 +2,17 @@ package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class FinishedMission extends Mission {
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
public FinishedMission(@NonNull DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
length = mission.length;
|
||||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
115
app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
Normal file
115
app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
Normal file
@ -0,0 +1,115 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class MissionRecoveryInfo implements Serializable, Parcelable {
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
MediaFormat format;
|
||||
String desired;
|
||||
boolean desired2;
|
||||
int desiredBitrate;
|
||||
byte kind;
|
||||
String validateCondition = null;
|
||||
|
||||
public MissionRecoveryInfo(@NonNull Stream stream) {
|
||||
if (stream instanceof AudioStream) {
|
||||
desiredBitrate = ((AudioStream) stream).average_bitrate;
|
||||
desired2 = false;
|
||||
kind = 'a';
|
||||
} else if (stream instanceof VideoStream) {
|
||||
desired = ((VideoStream) stream).getResolution();
|
||||
desired2 = ((VideoStream) stream).isVideoOnly();
|
||||
kind = 'v';
|
||||
} else if (stream instanceof SubtitlesStream) {
|
||||
desired = ((SubtitlesStream) stream).getLanguageTag();
|
||||
desired2 = ((SubtitlesStream) stream).isAutoGenerated();
|
||||
kind = 's';
|
||||
} else {
|
||||
throw new RuntimeException("Unknown stream kind");
|
||||
}
|
||||
|
||||
format = stream.getFormat();
|
||||
if (format == null) throw new NullPointerException("Stream format cannot be null");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
String info;
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("{type=");
|
||||
switch (kind) {
|
||||
case 'a':
|
||||
str.append("audio");
|
||||
info = "bitrate=" + desiredBitrate;
|
||||
break;
|
||||
case 'v':
|
||||
str.append("video");
|
||||
info = "quality=" + desired + " videoOnly=" + desired2;
|
||||
break;
|
||||
case 's':
|
||||
str.append("subtitles");
|
||||
info = "language=" + desired + " autoGenerated=" + desired2;
|
||||
break;
|
||||
default:
|
||||
info = "";
|
||||
str.append("other");
|
||||
}
|
||||
|
||||
str.append(" format=")
|
||||
.append(format.getName())
|
||||
.append(' ')
|
||||
.append(info)
|
||||
.append('}');
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeInt(this.format.ordinal());
|
||||
parcel.writeString(this.desired);
|
||||
parcel.writeInt(this.desired2 ? 0x01 : 0x00);
|
||||
parcel.writeInt(this.desiredBitrate);
|
||||
parcel.writeByte(this.kind);
|
||||
parcel.writeString(this.validateCondition);
|
||||
}
|
||||
|
||||
private MissionRecoveryInfo(Parcel parcel) {
|
||||
this.format = MediaFormat.values()[parcel.readInt()];
|
||||
this.desired = parcel.readString();
|
||||
this.desired2 = parcel.readInt() != 0x00;
|
||||
this.desiredBitrate = parcel.readInt();
|
||||
this.kind = parcel.readByte();
|
||||
this.validateCondition = parcel.readString();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MissionRecoveryInfo> CREATOR = new Parcelable.Creator<MissionRecoveryInfo>() {
|
||||
@Override
|
||||
public MissionRecoveryInfo createFromParcel(Parcel source) {
|
||||
return new MissionRecoveryInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MissionRecoveryInfo[] newArray(int size) {
|
||||
return new MissionRecoveryInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
private static final int REPORT_INTERVAL = 256 * 1024;
|
||||
|
||||
private SharpStream source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||
this(target, start, target.length());
|
||||
}
|
||||
private long progressReport;
|
||||
private final ProgressReport onProgress;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException {
|
||||
source = target;
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
onProgress = callback;
|
||||
progressReport = REPORT_INTERVAL;
|
||||
|
||||
if (length < 1) {
|
||||
source.close();
|
||||
@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if ((position + len) > length) {
|
||||
len = (int) (length - position);
|
||||
}
|
||||
@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream {
|
||||
int res = source.read(b, off, len);
|
||||
position += res;
|
||||
|
||||
if (onProgress != null && position > progressReport) {
|
||||
onProgress.report(position);
|
||||
progressReport = position + REPORT_INTERVAL;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[]) throws IOException {
|
||||
public void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
if (onProgress != null) {
|
||||
onProgress.report(-out.length - aux.length);// rollback the whole progress
|
||||
onProgress.report(0);// rollback the whole progress
|
||||
}
|
||||
|
||||
seek(0);
|
||||
@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream {
|
||||
long check();
|
||||
}
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
|
||||
public interface WriteErrorHandle {
|
||||
|
||||
/**
|
||||
@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream {
|
||||
|
||||
class BufferedFile {
|
||||
|
||||
protected final SharpStream target;
|
||||
final SharpStream target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
long length;
|
||||
|
||||
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
private int queueSize;
|
||||
@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void close() {
|
||||
void close() {
|
||||
queue = null;
|
||||
target.close();
|
||||
}
|
||||
|
||||
protected void write(byte b[], int off, int len) throws IOException {
|
||||
void write(byte[] b, int off, int len) throws IOException {
|
||||
while (len > 0) {
|
||||
// if the queue is full, the method available() will flush the queue
|
||||
int read = Math.min(available(), len);
|
||||
@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected int available() throws IOException {
|
||||
int available() throws IOException {
|
||||
if (queueSize >= queue.length) {
|
||||
flush();
|
||||
return queue.length;
|
||||
@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream {
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
void seek(long absoluteOffset) throws IOException {
|
||||
if (absoluteOffset == offset) {
|
||||
return;// nothing to do
|
||||
}
|
||||
|
||||
11
app/src/main/java/us/shandian/giga/io/ProgressReport.java
Normal file
11
app/src/main/java/us/shandian/giga/io/ProgressReport.java
Normal file
@ -0,0 +1,11 @@
|
||||
package us.shandian.giga.io;
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.OggFromWebMWriter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
class OggFromWebmDemuxer extends Postprocessing {
|
||||
|
||||
OggFromWebmDemuxer() {
|
||||
super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean test(SharpStream... sources) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
sources[0].read(buffer.array());
|
||||
|
||||
// youtube uses WebM as container, but the file extension (format suffix) is "*.opus"
|
||||
// check if the file is a webm/mkv file before proceed
|
||||
|
||||
switch (buffer.getInt()) {
|
||||
case 0x1a45dfa3:
|
||||
return true;// webm/mkv
|
||||
case 0x4F676753:
|
||||
return false;// ogg
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream");
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
|
||||
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
|
||||
demuxer.parseSource();
|
||||
demuxer.selectTrack(0);
|
||||
demuxer.build();
|
||||
|
||||
return OK_RESULT;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.io.CircularFileWriter;
|
||||
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.io.ProgressReport;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
|
||||
public abstract class Postprocessing implements Serializable {
|
||||
|
||||
@ -28,6 +28,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";
|
||||
|
||||
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
|
||||
Postprocessing instance;
|
||||
@ -45,6 +46,9 @@ public abstract class Postprocessing implements Serializable {
|
||||
case ALGORITHM_M4A_NO_DASH:
|
||||
instance = new M4aNoDash();
|
||||
break;
|
||||
case ALGORITHM_OGG_FROM_WEBM_DEMUXER:
|
||||
instance = new OggFromWebmDemuxer();
|
||||
break;
|
||||
/*case "example-algorithm":
|
||||
instance = new ExampleAlgorithm();*/
|
||||
default:
|
||||
@ -59,22 +63,22 @@ public abstract class Postprocessing implements Serializable {
|
||||
* Get a boolean value that indicate if the given algorithm work on the same
|
||||
* file
|
||||
*/
|
||||
public final boolean worksOnSameFile;
|
||||
public boolean worksOnSameFile;
|
||||
|
||||
/**
|
||||
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
|
||||
*/
|
||||
public final boolean reserveSpace;
|
||||
public boolean reserveSpace;
|
||||
|
||||
/**
|
||||
* Gets the given algorithm short name
|
||||
*/
|
||||
private final String name;
|
||||
private String name;
|
||||
|
||||
|
||||
private String[] args;
|
||||
|
||||
protected transient DownloadMission mission;
|
||||
private transient DownloadMission mission;
|
||||
|
||||
private File tempFile;
|
||||
|
||||
@ -105,16 +109,24 @@ public abstract class Postprocessing implements Serializable {
|
||||
long finalLength = -1;
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = mission.storage.length();
|
||||
|
||||
long length = mission.storage.length() - mission.offsets[0];
|
||||
mission.length = length > mission.nearLength ? length : mission.nearLength;
|
||||
|
||||
final ProgressReport readProgress = (long position) -> {
|
||||
position -= mission.offsets[0];
|
||||
if (position > mission.done) mission.done = position;
|
||||
};
|
||||
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||
for (int i = 0, j = 1; i < sources.length; i++, j++) {
|
||||
SharpStream source = mission.storage.getStream();
|
||||
long end = j < sources.length ? mission.offsets[j] : source.length();
|
||||
|
||||
sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress);
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||
|
||||
if (test(sources)) {
|
||||
for (SharpStream source : sources) source.rewind();
|
||||
@ -136,7 +148,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
};
|
||||
|
||||
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
|
||||
out.onProgress = this::progressReport;
|
||||
out.onProgress = (long position) -> mission.done = position;
|
||||
|
||||
out.onWriteError = (err) -> {
|
||||
mission.psState = 3;
|
||||
@ -183,11 +195,10 @@ public abstract class Postprocessing implements Serializable {
|
||||
|
||||
if (result == OK_RESULT) {
|
||||
if (finalLength != -1) {
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
}
|
||||
} else {
|
||||
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errCode = ERROR_POSTPROCESSING;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
@ -212,7 +223,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
*
|
||||
* @param out output stream
|
||||
* @param sources files to be processed
|
||||
* @return a error code, 0 means the operation was successful
|
||||
* @return an error code, {@code OK_RESULT} means the operation was successful
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||
@ -225,23 +236,12 @@ public abstract class Postprocessing implements Serializable {
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
mission.done = done;
|
||||
if (mission.length < mission.done) mission.length = mission.done;
|
||||
|
||||
Message m = new Message();
|
||||
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
||||
m.obj = mission;
|
||||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append("name=").append(name).append('[');
|
||||
str.append("{ name=").append(name).append('[');
|
||||
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
@ -251,6 +251,6 @@ public abstract class Postprocessing implements Serializable {
|
||||
str.delete(0, 1);
|
||||
}
|
||||
|
||||
return str.append(']').toString();
|
||||
return str.append("] }").toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,11 @@ package us.shandian.giga.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -37,6 +35,7 @@ public class DownloadManager {
|
||||
|
||||
public static final String TAG_AUDIO = "audio";
|
||||
public static final String TAG_VIDEO = "video";
|
||||
private static final String DOWNLOADS_METADATA_FOLDER = "pending_downloads";
|
||||
|
||||
private final FinishedMissionStore mFinishedMissionStore;
|
||||
|
||||
@ -74,25 +73,35 @@ public class DownloadManager {
|
||||
mMissionsFinished = loadFinishedMissions();
|
||||
mPendingMissionsDir = getPendingDir(context);
|
||||
|
||||
if (!Utility.mkdir(mPendingMissionsDir, false)) {
|
||||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||
}
|
||||
|
||||
loadPendingMissions(context);
|
||||
}
|
||||
|
||||
private static File getPendingDir(@NonNull Context context) {
|
||||
//File dir = new File(ContextCompat.getDataDir(context), "pending_downloads");
|
||||
File dir = context.getExternalFilesDir("pending_downloads");
|
||||
File dir = context.getExternalFilesDir(DOWNLOADS_METADATA_FOLDER);
|
||||
if (testDir(dir)) return dir;
|
||||
|
||||
if (dir == null) {
|
||||
// One of the following paths are not accessible ¿unmounted internal memory?
|
||||
// /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
// /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
Log.w(TAG, "path to pending downloads are not accessible");
|
||||
dir = new File(context.getFilesDir(), DOWNLOADS_METADATA_FOLDER);
|
||||
if (testDir(dir)) return dir;
|
||||
|
||||
throw new RuntimeException("path to pending downloads are not accessible");
|
||||
}
|
||||
|
||||
private static boolean testDir(@Nullable File dir) {
|
||||
if (dir == null) return false;
|
||||
|
||||
try {
|
||||
if (!Utility.mkdir(dir, false)) {
|
||||
Log.e(TAG, "testDir() cannot create the directory in path: " + dir.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
|
||||
File tmp = new File(dir, ".tmp");
|
||||
if (!tmp.createNewFile()) return false;
|
||||
return tmp.delete();// if the file was created, SHOULD BE deleted too
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "testDir() failed: " + dir.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,6 +141,7 @@ public class DownloadManager {
|
||||
|
||||
for (File sub : subs) {
|
||||
if (!sub.isFile()) continue;
|
||||
if (sub.getName().equals(".tmp")) continue;
|
||||
|
||||
DownloadMission mis = Utility.readFromFile(sub);
|
||||
if (mis == null || mis.isFinished()) {
|
||||
@ -140,6 +150,8 @@ public class DownloadManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
mis.threads = new Thread[0];
|
||||
|
||||
boolean exists;
|
||||
try {
|
||||
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||
@ -158,8 +170,6 @@ public class DownloadManager {
|
||||
// is Java IO (avoid showing the "Save as..." dialog)
|
||||
if (exists && mis.storage.isDirect() && !mis.storage.delete())
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
|
||||
mis.psState = 0;
|
||||
@ -177,7 +187,6 @@ public class DownloadManager {
|
||||
mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
|
||||
}
|
||||
|
||||
mis.recovered = exists;
|
||||
mis.metadata = sub;
|
||||
mis.maxRetry = mPrefMaxRetry;
|
||||
mis.mHandler = mHandler;
|
||||
@ -232,7 +241,6 @@ public class DownloadManager {
|
||||
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||
|
||||
if (canDownloadInCurrentNetwork() && start) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
@ -241,7 +249,6 @@ public class DownloadManager {
|
||||
|
||||
public void resumeMission(DownloadMission mission) {
|
||||
if (!mission.running) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
@ -250,7 +257,6 @@ public class DownloadManager {
|
||||
if (mission.running) {
|
||||
mission.setEnqueued(false);
|
||||
mission.pause();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,7 +269,6 @@ public class DownloadManager {
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.delete();
|
||||
}
|
||||
}
|
||||
@ -280,7 +285,6 @@ public class DownloadManager {
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.storage = null;
|
||||
mission.delete();
|
||||
}
|
||||
@ -363,35 +367,29 @@ public class DownloadManager {
|
||||
}
|
||||
|
||||
public void pauseAllMissions(boolean force) {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
|
||||
|
||||
if (force) mission.threads = null;// avoid waiting for threads
|
||||
if (force) {
|
||||
// avoid waiting for threads
|
||||
mission.init = null;
|
||||
mission.threads = new Thread[0];
|
||||
}
|
||||
|
||||
mission.pause();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
public void startAllMissions() {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || mission.isCorrupt()) continue;
|
||||
|
||||
flag = true;
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -472,28 +470,18 @@ public class DownloadManager {
|
||||
|
||||
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
|
||||
|
||||
int running = 0;
|
||||
int paused = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.isCorrupt() || mission.isPsRunning()) continue;
|
||||
|
||||
if (mission.running && isMetered) {
|
||||
paused++;
|
||||
mission.pause();
|
||||
} else if (!mission.running && !isMetered && mission.enqueued) {
|
||||
running++;
|
||||
mission.start();
|
||||
if (mPrefQueueLimit) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (running > 0) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
return;
|
||||
}
|
||||
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
void updateMaximumAttempts() {
|
||||
@ -502,22 +490,6 @@ public class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast check for pending downloads. If exists, the user will be notified
|
||||
* TODO: call this method in somewhere
|
||||
*
|
||||
* @param context the application context
|
||||
*/
|
||||
public static void notifyUserPendingDownloads(Context context) {
|
||||
int pending = getPendingDir(context).list().length;
|
||||
if (pending < 1) return;
|
||||
|
||||
Toast.makeText(context, context.getString(
|
||||
R.string.msg_pending_downloads,
|
||||
String.valueOf(pending)
|
||||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(storage);
|
||||
|
||||
@ -23,15 +23,17 @@ import android.os.Handler;
|
||||
import android.os.Handler.Callback;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
@ -42,6 +44,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
@ -54,11 +57,11 @@ public class DownloadManagerService extends Service {
|
||||
|
||||
private static final String TAG = "DownloadManagerService";
|
||||
|
||||
public static final int MESSAGE_RUNNING = 0;
|
||||
public static final int MESSAGE_PAUSED = 1;
|
||||
public static final int MESSAGE_FINISHED = 2;
|
||||
public static final int MESSAGE_PROGRESS = 3;
|
||||
public static final int MESSAGE_ERROR = 4;
|
||||
public static final int MESSAGE_DELETED = 5;
|
||||
public static final int MESSAGE_ERROR = 3;
|
||||
public static final int MESSAGE_DELETED = 4;
|
||||
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1000;
|
||||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||
@ -73,6 +76,7 @@ public class DownloadManagerService extends Service {
|
||||
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
|
||||
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
||||
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
||||
|
||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||
@ -212,9 +216,11 @@ public class DownloadManagerService extends Service {
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -245,6 +251,7 @@ public class DownloadManagerService extends Service {
|
||||
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
||||
if (icLauncher != null) icLauncher.recycle();
|
||||
|
||||
mHandler = null;
|
||||
mManager.pauseAllMissions(true);
|
||||
}
|
||||
|
||||
@ -269,6 +276,8 @@ public class DownloadManagerService extends Service {
|
||||
}
|
||||
|
||||
private boolean handleMessage(@NonNull Message msg) {
|
||||
if (mHandler == null) return true;
|
||||
|
||||
DownloadMission mission = (DownloadMission) msg.obj;
|
||||
|
||||
switch (msg.what) {
|
||||
@ -279,7 +288,7 @@ public class DownloadManagerService extends Service {
|
||||
handleConnectivityState(false);
|
||||
updateForegroundState(mManager.runMissions());
|
||||
break;
|
||||
case MESSAGE_PROGRESS:
|
||||
case MESSAGE_RUNNING:
|
||||
updateForegroundState(true);
|
||||
break;
|
||||
case MESSAGE_ERROR:
|
||||
@ -295,11 +304,8 @@ public class DownloadManagerService extends Service {
|
||||
if (msg.what != MESSAGE_ERROR)
|
||||
mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
|
||||
|
||||
synchronized (mEchoObservers) {
|
||||
for (Callback observer : mEchoObservers) {
|
||||
observer.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
for (Callback observer : mEchoObservers)
|
||||
observer.handleMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -364,18 +370,20 @@ public class DownloadManagerService extends Service {
|
||||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param context the activity context
|
||||
* @param urls the list of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
* @param context the activity context
|
||||
* @param urls array of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
* @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download
|
||||
*/
|
||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind,
|
||||
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
||||
char kind, int threads, String source, String psName,
|
||||
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.putExtra(EXTRA_URLS, urls);
|
||||
@ -385,6 +393,7 @@ public class DownloadManagerService extends Service {
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
||||
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
||||
intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo);
|
||||
|
||||
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
|
||||
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||
@ -404,6 +413,7 @@ public class DownloadManagerService extends Service {
|
||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
|
||||
|
||||
StoredFileHelper storage;
|
||||
try {
|
||||
@ -418,10 +428,15 @@ public class DownloadManagerService extends Service {
|
||||
else
|
||||
ps = Postprocessing.getAlgorithm(psName, psArgs);
|
||||
|
||||
MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length];
|
||||
for (int i = 0; i < parcelRecovery.length; i++)
|
||||
recovery[i] = (MissionRecoveryInfo) parcelRecovery[i];
|
||||
|
||||
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
|
||||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.nearLength = nearLength;
|
||||
mission.recoveryInfo = recovery;
|
||||
|
||||
if (ps != null)
|
||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||
@ -509,16 +524,6 @@ public class DownloadManagerService extends Service {
|
||||
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private void manageObservers(Callback handler, boolean add) {
|
||||
synchronized (mEchoObservers) {
|
||||
if (add) {
|
||||
mEchoObservers.add(handler);
|
||||
} else {
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void manageLock(boolean acquire) {
|
||||
if (acquire == mLockAcquired) return;
|
||||
|
||||
@ -591,11 +596,11 @@ public class DownloadManagerService extends Service {
|
||||
}
|
||||
|
||||
public void addMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, true);
|
||||
mEchoObservers.add(handler);
|
||||
}
|
||||
|
||||
public void removeMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, false);
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
|
||||
public void clearDownloadNotifications() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,10 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -113,7 +114,7 @@ public class Deleter {
|
||||
show();
|
||||
}
|
||||
|
||||
private void pause() {
|
||||
public void pause() {
|
||||
running = false;
|
||||
mHandler.removeCallbacks(rNext);
|
||||
mHandler.removeCallbacks(rShow);
|
||||
@ -126,13 +127,11 @@ public class Deleter {
|
||||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||
}
|
||||
|
||||
public void dispose(boolean commitChanges) {
|
||||
public void dispose() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
|
||||
if (!commitChanges) return;
|
||||
|
||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||
items = null;
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable {
|
||||
mForegroundColor = foreground;
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
mProgress = progress;
|
||||
public void setProgress(double progress) {
|
||||
mProgress = (float) progress;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
|
||||
@ -12,11 +12,6 @@ import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@ -24,6 +19,12 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@ -72,8 +73,7 @@ public class MissionsFragment extends Fragment {
|
||||
mBinder = (DownloadManagerBinder) binder;
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
||||
mAdapter.deleterLoad(getView());
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView());
|
||||
|
||||
mAdapter.setRecover(MissionsFragment.this::recoverMission);
|
||||
|
||||
@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment {
|
||||
* Added in API level 23.
|
||||
*/
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Bug: in api< 23 this is never called
|
||||
@ -147,7 +147,7 @@ public class MissionsFragment extends Fragment {
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
public void onAttach(@NonNull Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
mContext = activity;
|
||||
@ -162,7 +162,7 @@ public class MissionsFragment extends Fragment {
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mBinder.enableNotifications(true);
|
||||
mContext.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(true);
|
||||
mAdapter.onDestroy();
|
||||
|
||||
mBinder = null;
|
||||
mAdapter = null;
|
||||
@ -196,13 +196,11 @@ public class MissionsFragment extends Fragment {
|
||||
prompt.create().show();
|
||||
return true;
|
||||
case R.id.start_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().startAllMissions();
|
||||
return true;
|
||||
case R.id.pause_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||
mAdapter.ensurePausedMissions();// update items view
|
||||
mAdapter.refreshMissionItems();// update items view
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@ -271,23 +269,12 @@ public class MissionsFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterDispose(false);
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterResume();
|
||||
mAdapter.onResume();
|
||||
|
||||
if (mForceUpdate) {
|
||||
mForceUpdate = false;
|
||||
@ -303,7 +290,13 @@ public class MissionsFragment extends Fragment {
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (mAdapter != null) mAdapter.onPaused();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mAdapter.onPaused();
|
||||
}
|
||||
|
||||
if (mBinder != null) mBinder.enableNotifications(true);
|
||||
}
|
||||
|
||||
|
||||
@ -4,13 +4,14 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
@ -26,6 +27,7 @@ import java.io.Serializable;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
@ -39,26 +41,28 @@ public class Utility {
|
||||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (bytes < 1024) {
|
||||
return String.format("%d B", bytes);
|
||||
return String.format(locale, "%d B", bytes);
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f kB", bytes / 1024d);
|
||||
return String.format(locale, "%.2f kB", bytes / 1024d);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", bytes / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f MB", bytes / 1024d / 1024d);
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatSpeed(float speed) {
|
||||
public static String formatSpeed(double speed) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (speed < 1024) {
|
||||
return String.format("%.2f B/s", speed);
|
||||
return String.format(locale, "%.2f B/s", speed);
|
||||
} else if (speed < 1024 * 1024) {
|
||||
return String.format("%.2f kB/s", speed / 1024);
|
||||
return String.format(locale, "%.2f kB/s", speed / 1024);
|
||||
} else if (speed < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB/s", speed / 1024 / 1024);
|
||||
return String.format(locale, "%.2f MB/s", speed / 1024 / 1024);
|
||||
} else {
|
||||
return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,12 +192,11 @@ public class Utility {
|
||||
switch (type) {
|
||||
case MUSIC:
|
||||
return R.drawable.music;
|
||||
default:
|
||||
case VIDEO:
|
||||
return R.drawable.video;
|
||||
case SUBTITLE:
|
||||
return R.drawable.subtitle;
|
||||
default:
|
||||
return R.drawable.video;
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,4 +277,25 @@ public class Utility {
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String pad(int number) {
|
||||
return number < 10 ? ("0" + number) : String.valueOf(number);
|
||||
}
|
||||
|
||||
public static String stringifySeconds(double seconds) {
|
||||
int h = (int) Math.floor(seconds / 3600);
|
||||
int m = (int) Math.floor((seconds - (h * 3600)) / 60);
|
||||
int s = (int) (seconds - (h * 3600) - (m * 60));
|
||||
|
||||
String str = "";
|
||||
|
||||
if (h < 1 && m < 1) {
|
||||
str = "00:";
|
||||
} else {
|
||||
if (h > 0) str = pad(h) + ":";
|
||||
if (m > 0) str += pad(m) + ":";
|
||||
}
|
||||
|
||||
return str + pad(s);
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,11 +468,9 @@
|
||||
<string name="error_connect_host">لا يمكن الاتصال بالخادم</string>
|
||||
<string name="error_http_no_content">الخادم لايقوم بإرسال البيانات</string>
|
||||
<string name="error_http_unsupported_range">الخادم لا يقبل التنزيل المتعدد، إعادة المحاولة مع @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">عدم استيفاء النطاق المطلوب</string>
|
||||
<string name="error_http_not_found">غير موجود</string>
|
||||
<string name="error_postprocessing_failed">فشلت المعالجة الاولية</string>
|
||||
<string name="clear_finished_download">حذف التنزيلات المنتهية</string>
|
||||
<string name="msg_pending_downloads">"قم بإستكمال %s حيثما يتم التحويل من التنزيلات"</string>
|
||||
<string name="stop">توقف</string>
|
||||
<string name="max_retry_msg">أقصى عدد للمحاولات</string>
|
||||
<string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string>
|
||||
|
||||
@ -455,11 +455,9 @@
|
||||
<string name="error_connect_host">Немагчыма злучыцца з серверам</string>
|
||||
<string name="error_http_no_content">Не атрымалася атрымаць дадзеныя з сервера</string>
|
||||
<string name="error_http_unsupported_range">Сервер не падтрымлівае шматструменную загрузку, паспрабуйце з @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Запытаны дыяпазон недапушчальны</string>
|
||||
<string name="error_http_not_found">Не знойдзена</string>
|
||||
<string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string>
|
||||
<string name="clear_finished_download">Ачысціць завершаныя</string>
|
||||
<string name="msg_pending_downloads">Аднавіць прыпыненыя загрузкі (%s)</string>
|
||||
<string name="stop">Спыніць</string>
|
||||
<string name="max_retry_msg">Максімум спробаў</string>
|
||||
<string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string>
|
||||
|
||||
@ -460,8 +460,6 @@
|
||||
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
|
||||
<string name="error_path_creation">无法创建目标文件夹</string>
|
||||
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string>
|
||||
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>
|
||||
<string name="pause_downloads_on_mobile_desc">切换至移动数据时有用,尽管一些下载无法被暂停</string>
|
||||
<string name="show_comments_title">显示评论</string>
|
||||
<string name="show_comments_summary">禁用停止显示评论</string>
|
||||
|
||||
@ -463,11 +463,9 @@ otevření ve vyskakovacím okně</string>
|
||||
<string name="error_connect_host">Nelze se připojit k serveru</string>
|
||||
<string name="error_http_no_content">Server neposílá data</string>
|
||||
<string name="error_http_unsupported_range">Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Požadovaný rozsah nelze splnit</string>
|
||||
<string name="error_http_not_found">Nenalezeno</string>
|
||||
<string name="error_postprocessing_failed">Post-processing selhal</string>
|
||||
<string name="clear_finished_download">Vyčistit dokončená stahování</string>
|
||||
<string name="msg_pending_downloads">Pokračovat ve stahování %s souborů, čekajících na stažení</string>
|
||||
<string name="stop">Zastavit</string>
|
||||
<string name="max_retry_msg">Maximální počet pokusů o opakování</string>
|
||||
<string name="max_retry_desc">Maximální počet pokusů před zrušením stahování</string>
|
||||
|
||||
@ -380,7 +380,6 @@
|
||||
<string name="error_connect_host">Kan ikke forbinde til serveren</string>
|
||||
<string name="error_http_no_content">Serveren sender ikke data</string>
|
||||
<string name="error_http_unsupported_range">Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Det anmodede interval er ikke gyldigt</string>
|
||||
<string name="error_http_not_found">Ikke fundet</string>
|
||||
<string name="error_postprocessing_failed">Efterbehandling fejlede</string>
|
||||
<string name="stop">Stop</string>
|
||||
@ -448,7 +447,6 @@
|
||||
<string name="paused">sat på pause</string>
|
||||
<string name="queued">sat i kø</string>
|
||||
<string name="clear_finished_download">Ryd færdige downloads</string>
|
||||
<string name="msg_pending_downloads">Fortsæt dine %s ventende overførsler fra Downloads</string>
|
||||
<string name="max_retry_msg">Maksimalt antal genforsøg</string>
|
||||
<string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</string>
|
||||
<string name="pause_downloads_on_mobile">Sæt på pause ved skift til mobildata</string>
|
||||
|
||||
@ -454,11 +454,9 @@
|
||||
<string name="error_connect_host">Kann nicht mit dem Server verbinden</string>
|
||||
<string name="error_http_no_content">Der Server sendet keine Daten</string>
|
||||
<string name="error_http_unsupported_range">Der Server erlaubt kein mehrfädiges Herunterladen – wiederhole mit @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Gewünschter Bereich ist nicht verfügbar</string>
|
||||
<string name="error_http_not_found">Nicht gefunden</string>
|
||||
<string name="error_postprocessing_failed">Nachbearbeitung fehlgeschlagen</string>
|
||||
<string name="clear_finished_download">Um fertige Downloads bereinigen</string>
|
||||
<string name="msg_pending_downloads">Setze deine %s ausstehenden Übertragungen von Downloads fort</string>
|
||||
<string name="stop">Anhalten</string>
|
||||
<string name="max_retry_msg">Maximale Wiederholungen</string>
|
||||
<string name="max_retry_desc">Maximalanzahl der Versuche, bevor der Download abgebrochen wird</string>
|
||||
|
||||
@ -456,11 +456,9 @@
|
||||
<string name="error_connect_host">Αδυναμία σύνδεσης με τον εξυπηρετητή</string>
|
||||
<string name="error_http_no_content">Ο εξυπηρετητής δεν μπορεί να στείλει τα δεδομένα</string>
|
||||
<string name="error_http_unsupported_range">Ο εξυπηρετητής δέν υποστηρίζει πολυνηματικές λήψεις, ξαναπροσπαθήστε με @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Το ζητούμενο εύρος δεν μπορεί να εξυπηρετηθεί</string>
|
||||
<string name="error_http_not_found">Δεν βρέθηκε</string>
|
||||
<string name="error_postprocessing_failed">Μετεπεξεργασία απέτυχε</string>
|
||||
<string name="clear_finished_download">Εκκαθάριση ολοκληρωμένων λήψεων</string>
|
||||
<string name="msg_pending_downloads">Συνέχιση των %s εκκρεμών σας λήψεων</string>
|
||||
<string name="stop">Διακοπή</string>
|
||||
<string name="max_retry_msg">Μέγιστες επαναπροσπάθειες</string>
|
||||
<string name="max_retry_desc">Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης</string>
|
||||
|
||||
@ -351,8 +351,8 @@
|
||||
\n3. Inicie sesión cuando se le pida
|
||||
\n4. Copie la URL del perfil a la que fue redireccionado.</string>
|
||||
<string name="import_soundcloud_instructions_hint">suID, soundcloud.com/suID</string>
|
||||
<string name="import_network_expensive_warning">Observe que esta operación puede causar un uso intensivo de la red.
|
||||
\n
|
||||
<string name="import_network_expensive_warning">Observe que esta operación puede causar un uso intensivo de la red.
|
||||
\n
|
||||
\n¿Quiere continuar\?</string>
|
||||
<string name="download_thumbnail_title">Cargar miniaturas</string>
|
||||
<string name="download_thumbnail_summary">Desactívela para evitar la carga de miniaturas y ahorrar datos y memoria. Se vaciará la antememoria de imágenes en la memoria volátil y en el disco.</string>
|
||||
@ -406,6 +406,7 @@
|
||||
<string name="paused">pausado</string>
|
||||
<string name="queued">en cola</string>
|
||||
<string name="post_processing">posprocesamiento</string>
|
||||
<string name="recovering">recuperando</string>
|
||||
<string name="enqueue">Añadir a cola</string>
|
||||
<string name="permission_denied">Acción denegada por el sistema</string>
|
||||
<string name="file_deleted">Se eliminó el archivo</string>
|
||||
@ -424,7 +425,6 @@
|
||||
<string name="grid">Mostrar como grilla</string>
|
||||
<string name="list">Mostrar como lista</string>
|
||||
<string name="clear_finished_download">Limpiar descargas finalizadas</string>
|
||||
<string name="msg_pending_downloads">Tienes %s descargas pendientes, ve a Descargas para continuarlas</string>
|
||||
<string name="confirm_prompt">¿Lo confirma\?</string>
|
||||
<string name="stop">Detener</string>
|
||||
<string name="max_retry_msg">Intentos máximos</string>
|
||||
@ -444,8 +444,8 @@
|
||||
<string name="error_ssl_exception">Fallo la conexión segura</string>
|
||||
<string name="error_unknown_host">No se pudo encontrar el servidor</string>
|
||||
<string name="error_connect_host">No se puede conectar con el servidor</string>
|
||||
<string name="error_http_no_content">El servidor no está enviando datos</string>
|
||||
<string name="error_http_unsupported_range">El servidor no acepta descargas multiproceso; intente de nuevo con @string/msg_threads = 1</string>
|
||||
<string name="error_http_no_content">El servidor no devolvio datos</string>
|
||||
<string name="error_http_unsupported_range">El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">No se puede satisfacer el intervalo seleccionado</string>
|
||||
<string name="error_http_not_found">No encontrado</string>
|
||||
<string name="error_postprocessing_failed">Falló el posprocesamiento</string>
|
||||
@ -453,6 +453,7 @@
|
||||
<string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string>
|
||||
<string name="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string>
|
||||
<string name="error_timeout">Tiempo de espera excedido</string>
|
||||
<string name="error_download_resource_gone">No se puede recuperar esta descarga</string>
|
||||
<string name="downloads_storage_ask_title">Preguntar dónde descargar</string>
|
||||
<string name="downloads_storage_ask_summary">Se preguntará dónde guardar cada descarga</string>
|
||||
<string name="downloads_storage_ask_summary_kitkat">Se le preguntará dónde guardar cada descarga.
|
||||
|
||||
@ -457,11 +457,9 @@
|
||||
<string name="error_connect_host">Serveriga ei saadud ühendust</string>
|
||||
<string name="error_http_no_content">Server ei saada andmeid</string>
|
||||
<string name="error_http_unsupported_range">Server ei toeta mitmelõimelisi allalaadimisi. Proovi uuesti kasutades @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Taotletud vahemik ei ole rahuldatav</string>
|
||||
<string name="error_http_not_found">Ei leitud</string>
|
||||
<string name="error_postprocessing_failed">Järeltöötlemine nurjus</string>
|
||||
<string name="clear_finished_download">Eemalda lõpetatud allalaadimised</string>
|
||||
<string name="msg_pending_downloads">Jätka %s pooleliolevat allalaadimist</string>
|
||||
<string name="stop">Stopp</string>
|
||||
<string name="max_retry_msg">Korduskatseid</string>
|
||||
<string name="max_retry_desc">Suurim katsete arv enne allalaadimise tühistamist</string>
|
||||
|
||||
@ -456,11 +456,9 @@
|
||||
<string name="error_connect_host">Ezin da zerbitzariarekin konektatu</string>
|
||||
<string name="error_http_no_content">Zerbitzariak ez du daturik bidaltzen</string>
|
||||
<string name="error_http_unsupported_range">Zerbitzariak ez ditu hainbat hariko deskargak onartzen, saiatu @string/msg_threads = 1 erabilita</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Eskatutako barrutia ezin da bete</string>
|
||||
<string name="error_http_not_found">Ez aurkitua</string>
|
||||
<string name="error_postprocessing_failed">Post-prozesuak huts egin du</string>
|
||||
<string name="clear_finished_download">Garbitu amaitutako deskargak</string>
|
||||
<string name="msg_pending_downloads">Berrekin burutzeke dauden %s transferentzia deskargetatik</string>
|
||||
<string name="stop">Gelditu</string>
|
||||
<string name="max_retry_msg">Gehienezko saiakerak</string>
|
||||
<string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string>
|
||||
|
||||
@ -466,8 +466,6 @@
|
||||
<string name="max_retry_desc">Nombre maximum de tentatives avant d’annuler le téléchargement</string>
|
||||
<string name="saved_tabs_invalid_json">Utilisation des onglets par défaut, erreur lors de la lecture des onglets enregistrés</string>
|
||||
<string name="error_http_unsupported_range">Le serveur n’accepte pas les téléchargements multi-fils, veuillez réessayer avec @string/msg_threads = 1</string>
|
||||
<string name="msg_pending_downloads">Continuer vos %s transferts en attente depuis Téléchargement</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Le domaine désiré n\'est pas disponible</string>
|
||||
<string name="show_comments_title">Afficher les commentaires</string>
|
||||
<string name="show_comments_summary">Désactiver pour ne pas afficher les commentaires</string>
|
||||
<string name="autoplay_title">Lecture automatique</string>
|
||||
|
||||
@ -461,11 +461,9 @@
|
||||
<string name="error_connect_host">לא ניתן להתחבר לשרת</string>
|
||||
<string name="error_http_no_content">השרת לא שולח נתונים</string>
|
||||
<string name="error_http_unsupported_range">"השרת לא מקבל הורדות רב ערוציות, מוטב לנסות שוב עם @string/msg_threads = 1 "</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">הטווח המבוקש לא מתאים</string>
|
||||
<string name="error_http_not_found">לא נמצא</string>
|
||||
<string name="error_postprocessing_failed">העיבוד המאוחר נכשל</string>
|
||||
<string name="clear_finished_download">פינוי ההורדות שהסתיימו</string>
|
||||
<string name="msg_pending_downloads">ניתן להמשיך את %s ההורדות הממתינות שלך דרך ההורדות</string>
|
||||
<string name="stop">עצירה</string>
|
||||
<string name="max_retry_msg">מספר הניסיונות החוזרים המרבי</string>
|
||||
<string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string>
|
||||
|
||||
@ -454,11 +454,9 @@
|
||||
<string name="error_connect_host">Nije moguće povezati se s serverom</string>
|
||||
<string name="error_http_no_content">Server ne šalje podatke</string>
|
||||
<string name="error_http_unsupported_range">Poslužitelj ne prihvaća preuzimanja s više niti, pokušaj ponovo s @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Traženi raspon nije zadovoljavajući</string>
|
||||
<string name="error_http_not_found">Nije pronađeno</string>
|
||||
<string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string>
|
||||
<string name="clear_finished_download">Obriši završena preuzimanja</string>
|
||||
<string name="msg_pending_downloads">Nastavite s prijenosima na čekanju za %s s preuzimanja</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maksimalnih ponovnih pokušaja</string>
|
||||
<string name="max_retry_desc">Maksimalni broj pokušaja prije poništavanja preuzimanja</string>
|
||||
|
||||
@ -450,11 +450,9 @@
|
||||
<string name="error_connect_host">Tidak dapat terhubung ke server</string>
|
||||
<string name="error_http_no_content">Server tidak mengirim data</string>
|
||||
<string name="error_http_unsupported_range">Server tidak menerima unduhan multi-utas, coba lagi dengan @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Rentang yang diminta tidak memuaskan</string>
|
||||
<string name="error_http_not_found">Tidak ditemukan</string>
|
||||
<string name="error_postprocessing_failed">Pengolahan-pasca gagal</string>
|
||||
<string name="clear_finished_download">Hapus unduhan yang sudah selesai</string>
|
||||
<string name="msg_pending_downloads">Lanjutkan %s transfer anda yang tertunda dari Unduhan</string>
|
||||
<string name="stop">Berhenti</string>
|
||||
<string name="max_retry_msg">Percobaan maksimum</string>
|
||||
<string name="max_retry_desc">Jumlah upaya maksimum sebelum membatalkan unduhan</string>
|
||||
|
||||
@ -454,11 +454,9 @@
|
||||
<string name="error_connect_host">Impossibile connettersi al server</string>
|
||||
<string name="error_http_no_content">Il server non invia dati</string>
|
||||
<string name="error_http_unsupported_range">Il server non accetta download multipli, riprovare con @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Intervallo richiesto non soddisfatto</string>
|
||||
<string name="error_http_not_found">Non trovato</string>
|
||||
<string name="error_postprocessing_failed">Post-processing fallito</string>
|
||||
<string name="clear_finished_download">Pulisci i download completati</string>
|
||||
<string name="msg_pending_downloads">Continua i %s trasferimenti in corso dai Download</string>
|
||||
<string name="stop">Ferma</string>
|
||||
<string name="max_retry_msg">Tentativi massimi</string>
|
||||
<string name="max_retry_desc">Tentativi massimi prima di cancellare il download</string>
|
||||
|
||||
@ -440,7 +440,6 @@
|
||||
<string name="error_connect_host">サーバに接続できません</string>
|
||||
<string name="error_http_no_content">サーバがデータを送信していません</string>
|
||||
<string name="error_http_unsupported_range">サーバが同時接続ダウンロードを受け付けません。再試行してください @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">必要な範囲が満たされていません</string>
|
||||
<string name="error_http_not_found">見つかりません</string>
|
||||
<string name="error_postprocessing_failed">保存処理に失敗しました</string>
|
||||
<string name="clear_finished_download">完了済みを一覧から削除します</string>
|
||||
@ -457,7 +456,6 @@
|
||||
<string name="saved_tabs_invalid_json">デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました</string>
|
||||
<string name="main_page_content_summary">メインページに表示されるタブ</string>
|
||||
<string name="updates_setting_description">新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します</string>
|
||||
<string name="msg_pending_downloads">ダウンロードから %s の保留中の転送を続行します</string>
|
||||
<string name="pause_downloads_on_mobile">従量制課金ネットワークの割り込み</string>
|
||||
<string name="pause_downloads_on_mobile_desc">モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません</string>
|
||||
<string name="show_comments_title">コメントを表示</string>
|
||||
|
||||
@ -451,11 +451,9 @@
|
||||
<string name="error_connect_host">서버에 접속할 수 없습니다</string>
|
||||
<string name="error_http_no_content">서버가 데이터를 전송하지 않고 있습니다</string>
|
||||
<string name="error_http_unsupported_range">서버가 다중 스레드 다운로드를 받아들이지 않습니다, @string/msg_threads = 1 를 사용해 다시 시도해보세요</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">요청된 HTTP 범위가 충분하지 않습니다</string>
|
||||
<string name="error_http_not_found">HTTP 찾을 수 없습니다</string>
|
||||
<string name="error_postprocessing_failed">후처리 작업이 실패하였습니다</string>
|
||||
<string name="clear_finished_download">완료된 다운로드 비우기</string>
|
||||
<string name="msg_pending_downloads">대기중인 %s 다운로드를 지속하세요</string>
|
||||
<string name="stop">멈추기</string>
|
||||
<string name="max_retry_msg">최대 재시도 횟수</string>
|
||||
<string name="max_retry_desc">다운로드를 취소하기 전까지 다시 시도할 최대 횟수</string>
|
||||
|
||||
@ -450,11 +450,9 @@
|
||||
<string name="error_connect_host">Tidak dapat menyambung ke server</string>
|
||||
<string name="error_http_no_content">Server tidak menghantar data</string>
|
||||
<string name="error_http_unsupported_range">Server tidak menerima muat turun berbilang thread, cuba lagi dengan @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Julat yang diminta tidak memuaskan</string>
|
||||
<string name="error_http_not_found">Tidak ditemui</string>
|
||||
<string name="error_postprocessing_failed">Pemprosesan-pasca gagal</string>
|
||||
<string name="clear_finished_download">Hapuskan senarai muat turun yang selesai</string>
|
||||
<string name="msg_pending_downloads">Teruskan %s pemindahan anda yang menunggu dari muat turun</string>
|
||||
<string name="stop">Berhenti</string>
|
||||
<string name="max_retry_msg">Percubaan maksimum</string>
|
||||
<string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string>
|
||||
|
||||
@ -458,7 +458,6 @@
|
||||
<string name="error_http_not_found">Ikke funnet</string>
|
||||
<string name="error_postprocessing_failed">Etterbehandling mislyktes</string>
|
||||
<string name="clear_finished_download">Tøm fullførte nedlastinger</string>
|
||||
<string name="msg_pending_downloads">Fortsett dine %s ventende overføringer fra Nedlastinger</string>
|
||||
<string name="stop">Stopp</string>
|
||||
<string name="max_retry_msg">Maksimalt antall forsøk</string>
|
||||
<string name="max_retry_desc">Maksimalt antall tilkoblingsforsøk før nedlastingen avblåses</string>
|
||||
@ -496,7 +495,7 @@
|
||||
<string name="pause_downloads">Sett nedlastinger på pause</string>
|
||||
<string name="downloads_storage_ask_title">Spør om hvor ting skal lastes ned til</string>
|
||||
<string name="downloads_storage_ask_summary">Du vil bli spurt om hvor hver nedlasting skal plasseres</string>
|
||||
<string name="downloads_storage_ask_summary_kitkat">Du vil bli spurt om hvor hver nedlasting skal plasseres.
|
||||
<string name="downloads_storage_ask_summary_kitkat">Du vil bli spurt om hvor hver nedlasting skal plasseres.
|
||||
\nSkru på SAF hvis du vil laste ned til eksternt SD-kort</string>
|
||||
<string name="downloads_storage_use_saf_title">Bruk SAF</string>
|
||||
<string name="downloads_storage_use_saf_summary">Lagringstilgangsrammeverk (SAF) tillater nedlastinger til eksternt SD-kort.
|
||||
|
||||
@ -454,11 +454,9 @@
|
||||
<string name="error_connect_host">Kan geen verbinding maken met de server</string>
|
||||
<string name="error_http_no_content">De server verzendt geen gegevens</string>
|
||||
<string name="error_http_unsupported_range">De server aanvaardt geen meerdradige downloads, probeert het opnieuw met @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Gevraagd bereik niet beschikbaar</string>
|
||||
<string name="error_http_not_found">Niet gevonden</string>
|
||||
<string name="error_postprocessing_failed">Nabewerking mislukt</string>
|
||||
<string name="clear_finished_download">Voltooide downloads wissen</string>
|
||||
<string name="msg_pending_downloads">Zet uw %s wachtende downloads verder via Downloads</string>
|
||||
<string name="stop">Stoppen</string>
|
||||
<string name="max_retry_msg">Maximaal aantal pogingen</string>
|
||||
<string name="max_retry_desc">Maximaal aantal pogingen vooraleer dat den download wordt geannuleerd</string>
|
||||
|
||||
@ -454,11 +454,9 @@
|
||||
<string name="error_connect_host">Kan niet met de server verbinden</string>
|
||||
<string name="error_http_no_content">De server verzendt geen gegevens</string>
|
||||
<string name="error_http_unsupported_range">De server accepteert geen multi-threaded downloads, probeer het opnieuw met @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Gevraagde bereik niet beschikbaar</string>
|
||||
<string name="error_http_not_found">Niet gevonden</string>
|
||||
<string name="error_postprocessing_failed">Nabewerking mislukt</string>
|
||||
<string name="clear_finished_download">Voltooide downloads wissen</string>
|
||||
<string name="msg_pending_downloads">Zet je %s wachtende downloads voort via Downloads</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum aantal keer proberen</string>
|
||||
<string name="max_retry_desc">Maximum aantal pogingen voordat de download wordt geannuleerd</string>
|
||||
|
||||
@ -450,11 +450,9 @@
|
||||
<string name="error_connect_host">ਸਰਵਰ ਨਾਲ ਜੁੜ ਨਹੀਂ ਸਕਦਾ</string>
|
||||
<string name="error_http_no_content">ਸਰਵਰ ਨੇ ਡਾਟਾ ਨਹੀਂ ਭੇਜਿਆ</string>
|
||||
<string name="error_http_unsupported_range">ਸਰਵਰ ਮਲਟੀ-Threaded ਡਾਊਨਲੋਡਸ ਨੂੰ ਸਵੀਕਾਰ ਨਹੀਂ ਕਰਦਾ, ਇਸ ਨਾਲ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">ਬੇਨਤੀ ਕੀਤੀ ਸੀਮਾ ਤਸੱਲੀਬਖਸ਼ ਨਹੀਂ ਹੈ</string>
|
||||
<string name="error_http_not_found">ਨਹੀਂ ਲਭਿਆ</string>
|
||||
<string name="error_postprocessing_failed">Post-processing ਫੇਲ੍ਹ</string>
|
||||
<string name="clear_finished_download">ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string>
|
||||
<string name="msg_pending_downloads">ਡਾਉਨਲੋਡਸ ਤੋਂ ਆਪਣੀਆਂ %s ਬਕਾਇਆ ਟ੍ਰਾਂਸਫਰ ਜਾਰੀ ਰੱਖੋ</string>
|
||||
<string name="stop">ਰੁੱਕੋ</string>
|
||||
<string name="max_retry_msg">ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string>
|
||||
<string name="max_retry_desc">ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ</string>
|
||||
|
||||
@ -456,11 +456,9 @@
|
||||
<string name="error_connect_host">Nie można połączyć się z serwerem</string>
|
||||
<string name="error_http_no_content">Serwer nie wysyła danych</string>
|
||||
<string name="error_http_unsupported_range">Serwer nie akceptuje pobierania wielowątkowego, spróbuj ponownie za pomocą @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Niewłaściwy zakres</string>
|
||||
<string name="error_http_not_found">Nie znaleziono</string>
|
||||
<string name="error_postprocessing_failed">Przetwarzanie końcowe nie powiodło się</string>
|
||||
<string name="clear_finished_download">Wyczyść ukończone pobieranie</string>
|
||||
<string name="msg_pending_downloads">Kontynuuj %s oczekujące transfery z plików do pobrania</string>
|
||||
<string name="stop">Zatrzymaj</string>
|
||||
<string name="max_retry_msg">Maksymalna liczba powtórzeń</string>
|
||||
<string name="max_retry_desc">Maksymalna liczba prób przed anulowaniem pobierania</string>
|
||||
|
||||
@ -463,11 +463,9 @@ abrir em modo popup</string>
|
||||
<string name="error_connect_host">Não foi possível conectar ao servidor</string>
|
||||
<string name="error_http_no_content">O servidor não envia dados</string>
|
||||
<string name="error_http_unsupported_range">O servidor não aceita downloads em multi-thread, tente com @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Intervalo solicitado não aceito</string>
|
||||
<string name="error_http_not_found">Não encontrado</string>
|
||||
<string name="error_postprocessing_failed">Falha no pós processamento</string>
|
||||
<string name="clear_finished_download">Limpar downloads finalizados</string>
|
||||
<string name="msg_pending_downloads">Continuar seus %s downloads pendentes</string>
|
||||
<string name="stop">Parar</string>
|
||||
<string name="max_retry_msg">Tentativas Máximas</string>
|
||||
<string name="max_retry_desc">Número máximo de tentativas antes de cancelar o download</string>
|
||||
|
||||
@ -452,11 +452,9 @@
|
||||
<string name="error_connect_host">Não é possível ligar ao servidor</string>
|
||||
<string name="error_http_no_content">O servidor não envia dados</string>
|
||||
<string name="error_http_unsupported_range">O servidor não aceita transferências de vários processos, tente novamente com @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Intervalo solicitado não satisfatório</string>
|
||||
<string name="error_http_not_found">Não encontrado</string>
|
||||
<string name="error_postprocessing_failed">Pós-processamento falhado</string>
|
||||
<string name="clear_finished_download">Limpar transferências concluídas</string>
|
||||
<string name="msg_pending_downloads">Continue as suas %s transferências pendentes das Transferências</string>
|
||||
<string name="stop">Parar</string>
|
||||
<string name="max_retry_msg">Tentativas máximas</string>
|
||||
<string name="max_retry_desc">Número máximo de tentativas antes de cancelar a transferência</string>
|
||||
|
||||
@ -454,7 +454,6 @@
|
||||
<string name="error_permission_denied">Доступ запрещён системой</string>
|
||||
<string name="error_unknown_host">Сервер не найден</string>
|
||||
<string name="error_http_unsupported_range">Сервер не принимает многопоточные загрузки, повторная попытка с @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Запрашиваемый диапазон недопустим</string>
|
||||
<string name="error_http_not_found">Не найдено</string>
|
||||
<string name="clear_finished_download">Очистить завершённые</string>
|
||||
<string name="stop">Остановить</string>
|
||||
@ -465,7 +464,6 @@
|
||||
<string name="download_finished">Загрузка завершена</string>
|
||||
<string name="download_finished_more">%s загрузок завершено</string>
|
||||
<string name="generate_unique_name">Создать уникальное имя</string>
|
||||
<string name="msg_pending_downloads">Возобновить приостановленные загрузки (%s)</string>
|
||||
<string name="max_retry_msg">Максимум попыток</string>
|
||||
<string name="max_retry_desc">Количество попыток перед отменой загрузки</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Некоторые загрузки не поддерживают докачку и начнутся с начала</string>
|
||||
|
||||
@ -462,11 +462,9 @@
|
||||
<string name="error_connect_host">Nepodarilo sa pripojiť k serveru</string>
|
||||
<string name="error_http_no_content">Server neposiela údaje</string>
|
||||
<string name="error_http_unsupported_range">Server neakceptuje preberanie viacerých vlákien, zopakujte s @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Požadovaný rozsah nie je uspokojivý</string>
|
||||
<string name="error_http_not_found">Nenájdené</string>
|
||||
<string name="error_postprocessing_failed">Post-spracovanie zlyhalo</string>
|
||||
<string name="clear_finished_download">Vyčistiť dokončené sťahovania</string>
|
||||
<string name="msg_pending_downloads">Pokračujte v preberaní %s zo súborov na prevzatie</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum opakovaní</string>
|
||||
<string name="max_retry_desc">Maximálny počet pokusov pred zrušením stiahnutia</string>
|
||||
|
||||
@ -449,11 +449,9 @@
|
||||
<string name="error_connect_host">Sunucuya bağlanılamıyor</string>
|
||||
<string name="error_http_no_content">Sunucu veri göndermiyor</string>
|
||||
<string name="error_http_unsupported_range">Sunucu, çok iş parçacıklı indirmeleri kabul etmez, @string/msg_threads = 1 ile yeniden deneyin</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">İstenen aralık karşılanamıyor</string>
|
||||
<string name="error_http_not_found">Bulunamadı</string>
|
||||
<string name="error_postprocessing_failed">İşlem sonrası başarısız</string>
|
||||
<string name="clear_finished_download">Tamamlanan indirmeleri temizle</string>
|
||||
<string name="msg_pending_downloads">Beklemedeki %s transferinize İndirmeler\'den devam edin</string>
|
||||
<string name="stop">Durdur</string>
|
||||
<string name="max_retry_msg">Azami deneme sayısı</string>
|
||||
<string name="max_retry_desc">İndirmeyi iptal etmeden önce maksimum deneme sayısı</string>
|
||||
|
||||
@ -471,8 +471,6 @@
|
||||
<string name="saved_tabs_invalid_json">Помилка зчитування збережених вкладок. Використовую типові вкладки.</string>
|
||||
<string name="main_page_content_summary">Вкладки, що відображаються на головній сторінці</string>
|
||||
<string name="updates_setting_description">Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Запитуваний діапазон неприпустимий</string>
|
||||
<string name="msg_pending_downloads">Продовжити ваші %s відкладених переміщень із Завантажень</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені</string>
|
||||
<string name="show_comments_title">Показувати коментарі</string>
|
||||
<string name="show_comments_summary">Вимнути відображення дописів</string>
|
||||
|
||||
@ -449,11 +449,9 @@
|
||||
<string name="error_connect_host">Không thế kết nối với máy chủ</string>
|
||||
<string name="error_http_no_content">Máy chủ không gửi dữ liệu về</string>
|
||||
<string name="error_http_unsupported_range">Máy chủ không chấp nhận tải đa luồng, thử lại với số luồng = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">(HTTP) Không thể đáp ứng khoảng dữ liệu đã yêu cầu</string>
|
||||
<string name="error_http_not_found">Không tìm thấy</string>
|
||||
<string name="error_postprocessing_failed">Xử lý thất bại</string>
|
||||
<string name="clear_finished_download">Dọn các tải về đã hoàn thành</string>
|
||||
<string name="msg_pending_downloads">Hãy tiếp tục %s tải về đang chờ</string>
|
||||
<string name="stop">Dừng</string>
|
||||
<string name="max_retry_msg">Số lượt thử lại tối đa</string>
|
||||
<string name="max_retry_desc">Số lượt thử lại trước khi hủy tải về</string>
|
||||
|
||||
@ -447,11 +447,9 @@
|
||||
<string name="error_connect_host">無法連線到伺服器</string>
|
||||
<string name="error_http_no_content">伺服器沒有傳送資料</string>
|
||||
<string name="error_http_unsupported_range">伺服器不接受多執行緒下載,請以 @string/msg_threads = 1 重試</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">請求範圍無法滿足</string>
|
||||
<string name="error_http_not_found">找不到</string>
|
||||
<string name="error_postprocessing_failed">後處理失敗</string>
|
||||
<string name="clear_finished_download">清除已結束的下載</string>
|
||||
<string name="msg_pending_downloads">繼續從您所擱置中的下載 %s 傳輸</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="max_retry_msg">最大重試次數</string>
|
||||
<string name="max_retry_desc">在取消下載前的最大嘗試數</string>
|
||||
|
||||
@ -133,6 +133,7 @@
|
||||
|
||||
<!-- Caption Size -->
|
||||
<string name="caption_settings_key" translatable="false">caption_settings_key</string>
|
||||
<string name="caption_user_set_key" translatable="false">caption_user_set_key</string>
|
||||
|
||||
<!-- Content & History -->
|
||||
<string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string>
|
||||
|
||||
@ -527,6 +527,7 @@
|
||||
<string name="paused">paused</string>
|
||||
<string name="queued">queued</string>
|
||||
<string name="post_processing">post-processing</string>
|
||||
<string name="recovering">recovering</string>
|
||||
<string name="enqueue">Queue</string>
|
||||
<string name="permission_denied">Action denied by the system</string>
|
||||
<!-- download notifications -->
|
||||
@ -552,16 +553,15 @@
|
||||
<string name="error_connect_host">Can not connect to the server</string>
|
||||
<string name="error_http_no_content">The server does not send data</string>
|
||||
<string name="error_http_unsupported_range">The server does not accept multi-threaded downloads, retry with @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Requested range not satisfiable</string>
|
||||
<string name="error_http_not_found">Not found</string>
|
||||
<string name="error_postprocessing_failed">Post-processing failed</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string>
|
||||
<string name="error_insufficient_storage">No space left on device</string>
|
||||
<string name="error_progress_lost">Progress lost, because the file was deleted</string>
|
||||
<string name="error_timeout">Connection timeout</string>
|
||||
<string name="error_download_resource_gone">Cannot recover this download</string>
|
||||
<string name="clear_finished_download">Clear finished downloads</string>
|
||||
<string name="confirm_prompt">Are you sure?</string>
|
||||
<string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum retries</string>
|
||||
<string name="max_retry_desc">Maximum number of attempts before canceling the download</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user