Fix Lint: Inconsistent line separators
This commit is contained in:
parent
18fb0a13d7
commit
6f3dfad550
|
@ -1,416 +1,416 @@
|
||||||
package org.schabi.newpipe.streams;
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
public class OggFromWebMWriter implements Closeable {
|
public class OggFromWebMWriter implements Closeable {
|
||||||
private static final byte FLAG_UNSET = 0x00;
|
private static final byte FLAG_UNSET = 0x00;
|
||||||
//private static final byte FLAG_CONTINUED = 0x01;
|
//private static final byte FLAG_CONTINUED = 0x01;
|
||||||
private static final byte FLAG_FIRST = 0x02;
|
private static final byte FLAG_FIRST = 0x02;
|
||||||
private static final byte FLAG_LAST = 0x04;
|
private static final byte FLAG_LAST = 0x04;
|
||||||
|
|
||||||
private static final byte HEADER_CHECKSUM_OFFSET = 22;
|
private static final byte HEADER_CHECKSUM_OFFSET = 22;
|
||||||
private static final byte HEADER_SIZE = 27;
|
private static final byte HEADER_SIZE = 27;
|
||||||
|
|
||||||
private static final int TIME_SCALE_NS = 1000000000;
|
private static final int TIME_SCALE_NS = 1000000000;
|
||||||
|
|
||||||
private boolean done = false;
|
private boolean done = false;
|
||||||
private boolean parsed = false;
|
private boolean parsed = false;
|
||||||
|
|
||||||
private final SharpStream source;
|
private final SharpStream source;
|
||||||
private final SharpStream output;
|
private final SharpStream output;
|
||||||
|
|
||||||
private int sequenceCount = 0;
|
private int sequenceCount = 0;
|
||||||
private final int streamId;
|
private final int streamId;
|
||||||
private byte packetFlag = FLAG_FIRST;
|
private byte packetFlag = FLAG_FIRST;
|
||||||
|
|
||||||
private WebMReader webm = null;
|
private WebMReader webm = null;
|
||||||
private WebMTrack webmTrack = null;
|
private WebMTrack webmTrack = null;
|
||||||
private Segment webmSegment = null;
|
private Segment webmSegment = null;
|
||||||
private Cluster webmCluster = null;
|
private Cluster webmCluster = null;
|
||||||
private SimpleBlock webmBlock = null;
|
private SimpleBlock webmBlock = null;
|
||||||
|
|
||||||
private long webmBlockLastTimecode = 0;
|
private long webmBlockLastTimecode = 0;
|
||||||
private long webmBlockNearDuration = 0;
|
private long webmBlockNearDuration = 0;
|
||||||
|
|
||||||
private short segmentTableSize = 0;
|
private short segmentTableSize = 0;
|
||||||
private final byte[] segmentTable = new byte[255];
|
private final byte[] segmentTable = new byte[255];
|
||||||
private long segmentTableNextTimestamp = TIME_SCALE_NS;
|
private long segmentTableNextTimestamp = TIME_SCALE_NS;
|
||||||
|
|
||||||
private final int[] crc32Table = new int[256];
|
private final int[] crc32Table = new int[256];
|
||||||
|
|
||||||
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) {
|
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) {
|
||||||
if (!source.canRead() || !source.canRewind()) {
|
if (!source.canRead() || !source.canRewind()) {
|
||||||
throw new IllegalArgumentException("source stream must be readable and allows seeking");
|
throw new IllegalArgumentException("source stream must be readable and allows seeking");
|
||||||
}
|
}
|
||||||
if (!target.canWrite() || !target.canRewind()) {
|
if (!target.canWrite() || !target.canRewind()) {
|
||||||
throw new IllegalArgumentException("output stream must be writable and allows seeking");
|
throw new IllegalArgumentException("output stream must be writable and allows seeking");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.output = target;
|
this.output = target;
|
||||||
|
|
||||||
this.streamId = (int) System.currentTimeMillis();
|
this.streamId = (int) System.currentTimeMillis();
|
||||||
|
|
||||||
populateCrc32Table();
|
populateCrc32Table();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDone() {
|
public boolean isDone() {
|
||||||
return done;
|
return done;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isParsed() {
|
public boolean isParsed() {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
|
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
throw new IllegalStateException("source must be parsed first");
|
throw new IllegalStateException("source must be parsed first");
|
||||||
}
|
}
|
||||||
|
|
||||||
return webm.getAvailableTracks();
|
return webm.getAvailableTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void parseSource() throws IOException, IllegalStateException {
|
public void parseSource() throws IOException, IllegalStateException {
|
||||||
if (done) {
|
if (done) {
|
||||||
throw new IllegalStateException("already done");
|
throw new IllegalStateException("already done");
|
||||||
}
|
}
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
throw new IllegalStateException("already parsed");
|
throw new IllegalStateException("already parsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
webm = new WebMReader(source);
|
webm = new WebMReader(source);
|
||||||
webm.parse();
|
webm.parse();
|
||||||
webmSegment = webm.getNextSegment();
|
webmSegment = webm.getNextSegment();
|
||||||
} finally {
|
} finally {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectTrack(final int trackIndex) throws IOException {
|
public void selectTrack(final int trackIndex) throws IOException {
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
throw new IllegalStateException("source must be parsed first");
|
throw new IllegalStateException("source must be parsed first");
|
||||||
}
|
}
|
||||||
if (done) {
|
if (done) {
|
||||||
throw new IOException("already done");
|
throw new IOException("already done");
|
||||||
}
|
}
|
||||||
if (webmTrack != null) {
|
if (webmTrack != null) {
|
||||||
throw new IOException("tracks already selected");
|
throw new IOException("tracks already selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (webm.getAvailableTracks()[trackIndex].kind) {
|
switch (webm.getAvailableTracks()[trackIndex].kind) {
|
||||||
case Audio:
|
case Audio:
|
||||||
case Video:
|
case Video:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedOperationException("the track must an audio or video stream");
|
throw new UnsupportedOperationException("the track must an audio or video stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
webmTrack = webm.selectTrack(trackIndex);
|
webmTrack = webm.selectTrack(trackIndex);
|
||||||
} finally {
|
} finally {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
done = true;
|
done = true;
|
||||||
parsed = true;
|
parsed = true;
|
||||||
|
|
||||||
webmTrack = null;
|
webmTrack = null;
|
||||||
webm = null;
|
webm = null;
|
||||||
|
|
||||||
if (!output.isClosed()) {
|
if (!output.isClosed()) {
|
||||||
output.flush();
|
output.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
source.close();
|
source.close();
|
||||||
output.close();
|
output.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void build() throws IOException {
|
public void build() throws IOException {
|
||||||
final float resolution;
|
final float resolution;
|
||||||
SimpleBlock bloq;
|
SimpleBlock bloq;
|
||||||
final ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
|
final ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
|
||||||
final ByteBuffer page = ByteBuffer.allocate(64 * 1024);
|
final ByteBuffer page = ByteBuffer.allocate(64 * 1024);
|
||||||
|
|
||||||
header.order(ByteOrder.LITTLE_ENDIAN);
|
header.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
/* step 1: get the amount of frames per seconds */
|
/* step 1: get the amount of frames per seconds */
|
||||||
switch (webmTrack.kind) {
|
switch (webmTrack.kind) {
|
||||||
case Audio:
|
case Audio:
|
||||||
resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata);
|
resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata);
|
||||||
if (resolution == 0f) {
|
if (resolution == 0f) {
|
||||||
throw new RuntimeException("cannot get the audio sample rate");
|
throw new RuntimeException("cannot get the audio sample rate");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Video:
|
case Video:
|
||||||
// WARNING: untested
|
// WARNING: untested
|
||||||
if (webmTrack.defaultDuration == 0) {
|
if (webmTrack.defaultDuration == 0) {
|
||||||
throw new RuntimeException("missing default frame time");
|
throw new RuntimeException("missing default frame time");
|
||||||
}
|
}
|
||||||
resolution = 1000f / ((float) webmTrack.defaultDuration
|
resolution = 1000f / ((float) webmTrack.defaultDuration
|
||||||
/ webmSegment.info.timecodeScale);
|
/ webmSegment.info.timecodeScale);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("not implemented");
|
throw new RuntimeException("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* step 2: create packet with code init data */
|
/* step 2: create packet with code init data */
|
||||||
if (webmTrack.codecPrivate != null) {
|
if (webmTrack.codecPrivate != null) {
|
||||||
addPacketSegment(webmTrack.codecPrivate.length);
|
addPacketSegment(webmTrack.codecPrivate.length);
|
||||||
makePacketheader(0x00, header, webmTrack.codecPrivate);
|
makePacketheader(0x00, header, webmTrack.codecPrivate);
|
||||||
write(header);
|
write(header);
|
||||||
output.write(webmTrack.codecPrivate);
|
output.write(webmTrack.codecPrivate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* step 3: create packet with metadata */
|
/* step 3: create packet with metadata */
|
||||||
final byte[] buffer = makeMetadata();
|
final byte[] buffer = makeMetadata();
|
||||||
if (buffer != null) {
|
if (buffer != null) {
|
||||||
addPacketSegment(buffer.length);
|
addPacketSegment(buffer.length);
|
||||||
makePacketheader(0x00, header, buffer);
|
makePacketheader(0x00, header, buffer);
|
||||||
write(header);
|
write(header);
|
||||||
output.write(buffer);
|
output.write(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* step 4: calculate amount of packets */
|
/* step 4: calculate amount of packets */
|
||||||
while (webmSegment != null) {
|
while (webmSegment != null) {
|
||||||
bloq = getNextBlock();
|
bloq = getNextBlock();
|
||||||
|
|
||||||
if (bloq != null && addPacketSegment(bloq)) {
|
if (bloq != null && addPacketSegment(bloq)) {
|
||||||
final int pos = page.position();
|
final int pos = page.position();
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
bloq.data.read(page.array(), pos, bloq.dataSize);
|
bloq.data.read(page.array(), pos, bloq.dataSize);
|
||||||
page.position(pos + bloq.dataSize);
|
page.position(pos + bloq.dataSize);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate the current packet duration using the next block
|
// calculate the current packet duration using the next block
|
||||||
double elapsedNs = webmTrack.codecDelay;
|
double elapsedNs = webmTrack.codecDelay;
|
||||||
|
|
||||||
if (bloq == null) {
|
if (bloq == null) {
|
||||||
packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed
|
packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed
|
||||||
elapsedNs += webmBlockLastTimecode;
|
elapsedNs += webmBlockLastTimecode;
|
||||||
|
|
||||||
if (webmTrack.defaultDuration > 0) {
|
if (webmTrack.defaultDuration > 0) {
|
||||||
elapsedNs += webmTrack.defaultDuration;
|
elapsedNs += webmTrack.defaultDuration;
|
||||||
} else {
|
} else {
|
||||||
// hardcoded way, guess the sample duration
|
// hardcoded way, guess the sample duration
|
||||||
elapsedNs += webmBlockNearDuration;
|
elapsedNs += webmBlockNearDuration;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elapsedNs += bloq.absoluteTimeCodeNs;
|
elapsedNs += bloq.absoluteTimeCodeNs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the sample count in the page
|
// get the sample count in the page
|
||||||
elapsedNs = elapsedNs / TIME_SCALE_NS;
|
elapsedNs = elapsedNs / TIME_SCALE_NS;
|
||||||
elapsedNs = Math.ceil(elapsedNs * resolution);
|
elapsedNs = Math.ceil(elapsedNs * resolution);
|
||||||
|
|
||||||
// create header and calculate page checksum
|
// create header and calculate page checksum
|
||||||
int checksum = makePacketheader((long) elapsedNs, header, null);
|
int checksum = makePacketheader((long) elapsedNs, header, null);
|
||||||
checksum = calcCrc32(checksum, page.array(), page.position());
|
checksum = calcCrc32(checksum, page.array(), page.position());
|
||||||
|
|
||||||
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
|
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
|
||||||
|
|
||||||
// dump data
|
// dump data
|
||||||
write(header);
|
write(header);
|
||||||
write(page);
|
write(page);
|
||||||
|
|
||||||
webmBlock = bloq;
|
webmBlock = bloq;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
|
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
|
||||||
final byte[] immediatePage) {
|
final byte[] immediatePage) {
|
||||||
short length = HEADER_SIZE;
|
short length = HEADER_SIZE;
|
||||||
|
|
||||||
buffer.putInt(0x5367674f); // "OggS" binary string in little-endian
|
buffer.putInt(0x5367674f); // "OggS" binary string in little-endian
|
||||||
buffer.put((byte) 0x00); // version
|
buffer.put((byte) 0x00); // version
|
||||||
buffer.put(packetFlag); // type
|
buffer.put(packetFlag); // type
|
||||||
|
|
||||||
buffer.putLong(granPos); // granulate position
|
buffer.putLong(granPos); // granulate position
|
||||||
|
|
||||||
buffer.putInt(streamId); // bitstream serial number
|
buffer.putInt(streamId); // bitstream serial number
|
||||||
buffer.putInt(sequenceCount++); // page sequence number
|
buffer.putInt(sequenceCount++); // page sequence number
|
||||||
|
|
||||||
buffer.putInt(0x00); // page checksum
|
buffer.putInt(0x00); // page checksum
|
||||||
|
|
||||||
buffer.put((byte) segmentTableSize); // segment table
|
buffer.put((byte) segmentTableSize); // segment table
|
||||||
buffer.put(segmentTable, 0, segmentTableSize); // segment size
|
buffer.put(segmentTable, 0, segmentTableSize); // segment size
|
||||||
|
|
||||||
length += segmentTableSize;
|
length += segmentTableSize;
|
||||||
|
|
||||||
clearSegmentTable(); // clear segment table for next header
|
clearSegmentTable(); // clear segment table for next header
|
||||||
|
|
||||||
int checksumCrc32 = calcCrc32(0x00, buffer.array(), length);
|
int checksumCrc32 = calcCrc32(0x00, buffer.array(), length);
|
||||||
|
|
||||||
if (immediatePage != null) {
|
if (immediatePage != null) {
|
||||||
checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length);
|
checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length);
|
||||||
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32);
|
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32);
|
||||||
segmentTableNextTimestamp -= TIME_SCALE_NS;
|
segmentTableNextTimestamp -= TIME_SCALE_NS;
|
||||||
}
|
}
|
||||||
|
|
||||||
return checksumCrc32;
|
return checksumCrc32;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private byte[] makeMetadata() {
|
private byte[] makeMetadata() {
|
||||||
if ("A_OPUS".equals(webmTrack.codecId)) {
|
if ("A_OPUS".equals(webmTrack.codecId)) {
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
|
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
|
||||||
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
|
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
|
||||||
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
|
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
|
||||||
};
|
};
|
||||||
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
|
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x03, // ¿¿¿???
|
0x03, // ¿¿¿???
|
||||||
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
|
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
|
||||||
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
|
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
|
||||||
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
|
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// not implemented for the desired codec
|
// not implemented for the desired codec
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void write(final ByteBuffer buffer) throws IOException {
|
private void write(final ByteBuffer buffer) throws IOException {
|
||||||
output.write(buffer.array(), 0, buffer.position());
|
output.write(buffer.array(), 0, buffer.position());
|
||||||
buffer.position(0);
|
buffer.position(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private SimpleBlock getNextBlock() throws IOException {
|
private SimpleBlock getNextBlock() throws IOException {
|
||||||
SimpleBlock res;
|
SimpleBlock res;
|
||||||
|
|
||||||
if (webmBlock != null) {
|
if (webmBlock != null) {
|
||||||
res = webmBlock;
|
res = webmBlock;
|
||||||
webmBlock = null;
|
webmBlock = null;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webmSegment == null) {
|
if (webmSegment == null) {
|
||||||
webmSegment = webm.getNextSegment();
|
webmSegment = webm.getNextSegment();
|
||||||
if (webmSegment == null) {
|
if (webmSegment == null) {
|
||||||
return null; // no more blocks in the selected track
|
return null; // no more blocks in the selected track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webmCluster == null) {
|
if (webmCluster == null) {
|
||||||
webmCluster = webmSegment.getNextCluster();
|
webmCluster = webmSegment.getNextCluster();
|
||||||
if (webmCluster == null) {
|
if (webmCluster == null) {
|
||||||
webmSegment = null;
|
webmSegment = null;
|
||||||
return getNextBlock();
|
return getNextBlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res = webmCluster.getNextSimpleBlock();
|
res = webmCluster.getNextSimpleBlock();
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
webmCluster = null;
|
webmCluster = null;
|
||||||
return getNextBlock();
|
return getNextBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode;
|
webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode;
|
||||||
webmBlockLastTimecode = res.absoluteTimeCodeNs;
|
webmBlockLastTimecode = res.absoluteTimeCodeNs;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private float getSampleFrequencyFromTrack(final byte[] bMetadata) {
|
private float getSampleFrequencyFromTrack(final byte[] bMetadata) {
|
||||||
// hardcoded way
|
// hardcoded way
|
||||||
final ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
|
final ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
|
||||||
|
|
||||||
while (buffer.remaining() >= 6) {
|
while (buffer.remaining() >= 6) {
|
||||||
final int id = buffer.getShort() & 0xFFFF;
|
final int id = buffer.getShort() & 0xFFFF;
|
||||||
if (id == 0x0000B584) {
|
if (id == 0x0000B584) {
|
||||||
return buffer.getFloat();
|
return buffer.getFloat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearSegmentTable() {
|
private void clearSegmentTable() {
|
||||||
segmentTableNextTimestamp += TIME_SCALE_NS;
|
segmentTableNextTimestamp += TIME_SCALE_NS;
|
||||||
packetFlag = FLAG_UNSET;
|
packetFlag = FLAG_UNSET;
|
||||||
segmentTableSize = 0;
|
segmentTableSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean addPacketSegment(final SimpleBlock block) {
|
private boolean addPacketSegment(final SimpleBlock block) {
|
||||||
final long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay;
|
final long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay;
|
||||||
|
|
||||||
if (timestamp >= segmentTableNextTimestamp) {
|
if (timestamp >= segmentTableNextTimestamp) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addPacketSegment(block.dataSize);
|
return addPacketSegment(block.dataSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean addPacketSegment(final int size) {
|
private boolean addPacketSegment(final int size) {
|
||||||
if (size > 65025) {
|
if (size > 65025) {
|
||||||
throw new UnsupportedOperationException("page size cannot be larger than 65025");
|
throw new UnsupportedOperationException("page size cannot be larger than 65025");
|
||||||
}
|
}
|
||||||
|
|
||||||
int available = (segmentTable.length - segmentTableSize) * 255;
|
int available = (segmentTable.length - segmentTableSize) * 255;
|
||||||
final boolean extra = (size % 255) == 0;
|
final boolean extra = (size % 255) == 0;
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
// add a zero byte entry in the table
|
// add a zero byte entry in the table
|
||||||
// required to indicate the sample size is multiple of 255
|
// required to indicate the sample size is multiple of 255
|
||||||
available -= 255;
|
available -= 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if possible add the segment, without overflow the table
|
// check if possible add the segment, without overflow the table
|
||||||
if (available < size) {
|
if (available < size) {
|
||||||
return false; // not enough space on the page
|
return false; // not enough space on the page
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int seg = size; seg > 0; seg -= 255) {
|
for (int seg = size; seg > 0; seg -= 255) {
|
||||||
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
|
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
segmentTable[segmentTableSize++] = 0x00;
|
segmentTable[segmentTableSize++] = 0x00;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateCrc32Table() {
|
private void populateCrc32Table() {
|
||||||
for (int i = 0; i < 0x100; i++) {
|
for (int i = 0; i < 0x100; i++) {
|
||||||
int crc = i << 24;
|
int crc = i << 24;
|
||||||
for (int j = 0; j < 8; j++) {
|
for (int j = 0; j < 8; j++) {
|
||||||
final long b = crc >>> 31;
|
final long b = crc >>> 31;
|
||||||
crc <<= 1;
|
crc <<= 1;
|
||||||
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
|
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
|
||||||
}
|
}
|
||||||
crc32Table[i] = crc;
|
crc32Table[i] = crc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) {
|
private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) {
|
||||||
int crc = initialCrc;
|
int crc = initialCrc;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
final int reg = (crc >>> 24) & 0xff;
|
final int reg = (crc >>> 24) & 0xff;
|
||||||
crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)];
|
crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return crc;
|
return crc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,313 +1,313 @@
|
||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.ClosedByInterruptException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||||
|
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||||
|
|
||||||
public class DownloadMissionRecover extends Thread {
|
public class DownloadMissionRecover extends Thread {
|
||||||
private static final String TAG = "DownloadMissionRecover";
|
private static final String TAG = "DownloadMissionRecover";
|
||||||
static final int mID = -3;
|
static final int mID = -3;
|
||||||
|
|
||||||
private final DownloadMission mMission;
|
private final DownloadMission mMission;
|
||||||
private final boolean mNotInitialized;
|
private final boolean mNotInitialized;
|
||||||
|
|
||||||
private final int mErrCode;
|
private final int mErrCode;
|
||||||
|
|
||||||
private HttpURLConnection mConn;
|
private HttpURLConnection mConn;
|
||||||
private MissionRecoveryInfo mRecovery;
|
private MissionRecoveryInfo mRecovery;
|
||||||
private StreamExtractor mExtractor;
|
private StreamExtractor mExtractor;
|
||||||
|
|
||||||
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
||||||
mMission = mission;
|
mMission = mission;
|
||||||
mNotInitialized = mission.blocks == null && mission.current == 0;
|
mNotInitialized = mission.blocks == null && mission.current == 0;
|
||||||
mErrCode = errCode;
|
mErrCode = errCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (mMission.source == null) {
|
if (mMission.source == null) {
|
||||||
mMission.notifyError(mErrCode, null);
|
mMission.notifyError(mErrCode, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Exception err = null;
|
Exception err = null;
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
|
|
||||||
while (attempt++ < mMission.maxRetry) {
|
while (attempt++ < mMission.maxRetry) {
|
||||||
try {
|
try {
|
||||||
tryRecover();
|
tryRecover();
|
||||||
return;
|
return;
|
||||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||||
return;
|
return;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!mMission.running || super.isInterrupted()) return;
|
if (!mMission.running || super.isInterrupted()) return;
|
||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// give up
|
// give up
|
||||||
mMission.notifyError(mErrCode, err);
|
mMission.notifyError(mErrCode, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
||||||
if (mExtractor == null) {
|
if (mExtractor == null) {
|
||||||
try {
|
try {
|
||||||
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
||||||
mExtractor = svr.getStreamExtractor(mMission.source);
|
mExtractor = svr.getStreamExtractor(mMission.source);
|
||||||
mExtractor.fetchPage();
|
mExtractor.fetchPage();
|
||||||
} catch (ExtractionException e) {
|
} catch (ExtractionException e) {
|
||||||
mExtractor = null;
|
mExtractor = null;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// maybe the following check is redundant
|
// maybe the following check is redundant
|
||||||
if (!mMission.running || super.isInterrupted()) return;
|
if (!mMission.running || super.isInterrupted()) return;
|
||||||
|
|
||||||
if (!mNotInitialized) {
|
if (!mNotInitialized) {
|
||||||
// set the current download url to null in case if the recovery
|
// set the current download url to null in case if the recovery
|
||||||
// process is canceled. Next time start() method is called the
|
// process is canceled. Next time start() method is called the
|
||||||
// recovery will be executed, saving time
|
// recovery will be executed, saving time
|
||||||
mMission.urls[mMission.current] = null;
|
mMission.urls[mMission.current] = null;
|
||||||
|
|
||||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||||
resolveStream();
|
resolveStream();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "mission is not fully initialized, this will take a while");
|
Log.w(TAG, "mission is not fully initialized, this will take a while");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (; mMission.current < mMission.urls.length; mMission.current++) {
|
for (; mMission.current < mMission.urls.length; mMission.current++) {
|
||||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||||
|
|
||||||
if (test()) continue;
|
if (test()) continue;
|
||||||
if (!mMission.running) return;
|
if (!mMission.running) return;
|
||||||
|
|
||||||
resolveStream();
|
resolveStream();
|
||||||
if (!mMission.running) return;
|
if (!mMission.running) return;
|
||||||
|
|
||||||
// before continue, check if the current stream was resolved
|
// before continue, check if the current stream was resolved
|
||||||
if (mMission.urls[mMission.current] == null) {
|
if (mMission.urls[mMission.current] == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
mMission.current = 0;
|
mMission.current = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
mMission.writeThisToFile();
|
mMission.writeThisToFile();
|
||||||
|
|
||||||
if (!mMission.running || super.isInterrupted()) return;
|
if (!mMission.running || super.isInterrupted()) return;
|
||||||
|
|
||||||
mMission.running = false;
|
mMission.running = false;
|
||||||
mMission.start();
|
mMission.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
||||||
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
||||||
/*if (mExtractor.getErrorMessage() != null) {
|
/*if (mExtractor.getErrorMessage() != null) {
|
||||||
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
||||||
return;
|
return;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
String url = null;
|
String url = null;
|
||||||
|
|
||||||
switch (mRecovery.getKind()) {
|
switch (mRecovery.getKind()) {
|
||||||
case 'a':
|
case 'a':
|
||||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
||||||
if (audio.average_bitrate == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) {
|
if (audio.average_bitrate == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) {
|
||||||
url = audio.getUrl();
|
url = audio.getUrl();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'v':
|
case 'v':
|
||||||
List<VideoStream> videoStreams;
|
List<VideoStream> videoStreams;
|
||||||
if (mRecovery.isDesired2())
|
if (mRecovery.isDesired2())
|
||||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||||
else
|
else
|
||||||
videoStreams = mExtractor.getVideoStreams();
|
videoStreams = mExtractor.getVideoStreams();
|
||||||
for (VideoStream video : videoStreams) {
|
for (VideoStream video : videoStreams) {
|
||||||
if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) {
|
if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) {
|
||||||
url = video.getUrl();
|
url = video.getUrl();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) {
|
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) {
|
||||||
String tag = subtitles.getLanguageTag();
|
String tag = subtitles.getLanguageTag();
|
||||||
if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) {
|
if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) {
|
||||||
url = subtitles.getUrl();
|
url = subtitles.getUrl();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Unknown stream type");
|
throw new RuntimeException("Unknown stream type");
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(url);
|
resolve(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolve(String url) throws IOException, HttpError {
|
private void resolve(String url) throws IOException, HttpError {
|
||||||
if (mRecovery.getValidateCondition() == null) {
|
if (mRecovery.getValidateCondition() == null) {
|
||||||
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMission.unknownLength || mRecovery.getValidateCondition() == null) {
|
if (mMission.unknownLength || mRecovery.getValidateCondition() == null) {
|
||||||
recover(url, false);
|
recover(url, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
////// Validate the http resource doing a range request
|
////// Validate the http resource doing a range request
|
||||||
/////////////////////
|
/////////////////////
|
||||||
try {
|
try {
|
||||||
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
|
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
|
||||||
mConn.setRequestProperty("If-Range", mRecovery.getValidateCondition());
|
mConn.setRequestProperty("If-Range", mRecovery.getValidateCondition());
|
||||||
mMission.establishConnection(mID, mConn);
|
mMission.establishConnection(mID, mConn);
|
||||||
|
|
||||||
int code = mConn.getResponseCode();
|
int code = mConn.getResponseCode();
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 200:
|
case 200:
|
||||||
case 413:
|
case 413:
|
||||||
// stale
|
// stale
|
||||||
recover(url, true);
|
recover(url, true);
|
||||||
return;
|
return;
|
||||||
case 206:
|
case 206:
|
||||||
// in case of validation using the Last-Modified date, check the resource length
|
// in case of validation using the Last-Modified date, check the resource length
|
||||||
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
|
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
|
||||||
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
|
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
|
||||||
|
|
||||||
recover(url, lengthMismatch);
|
recover(url, lengthMismatch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new HttpError(code);
|
throw new HttpError(code);
|
||||||
} finally {
|
} finally {
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recover(String url, boolean stale) {
|
private void recover(String url, boolean stale) {
|
||||||
Log.i(TAG,
|
Log.i(TAG,
|
||||||
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
|
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
|
||||||
);
|
);
|
||||||
|
|
||||||
mMission.urls[mMission.current] = url;
|
mMission.urls[mMission.current] = url;
|
||||||
|
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
mMission.urls = new String[0];
|
mMission.urls = new String[0];
|
||||||
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mNotInitialized) return;
|
if (mNotInitialized) return;
|
||||||
|
|
||||||
if (stale) {
|
if (stale) {
|
||||||
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
||||||
}
|
}
|
||||||
|
|
||||||
mMission.writeThisToFile();
|
mMission.writeThisToFile();
|
||||||
|
|
||||||
if (!mMission.running || super.isInterrupted()) return;
|
if (!mMission.running || super.isInterrupted()) return;
|
||||||
|
|
||||||
mMission.running = false;
|
mMission.running = false;
|
||||||
mMission.start();
|
mMission.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long[] parseContentRange(String value) {
|
private long[] parseContentRange(String value) {
|
||||||
long[] range = new long[3];
|
long[] range = new long[3];
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
// this never should happen
|
// this never should happen
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
|
|
||||||
if (!value.startsWith("bytes")) {
|
if (!value.startsWith("bytes")) {
|
||||||
return range;// unknown range type
|
return range;// unknown range type
|
||||||
}
|
}
|
||||||
|
|
||||||
int space = value.lastIndexOf(' ') + 1;
|
int space = value.lastIndexOf(' ') + 1;
|
||||||
int dash = value.indexOf('-', space) + 1;
|
int dash = value.indexOf('-', space) + 1;
|
||||||
int bar = value.indexOf('/', dash);
|
int bar = value.indexOf('/', dash);
|
||||||
|
|
||||||
// start
|
// start
|
||||||
range[0] = Long.parseLong(value.substring(space, dash - 1));
|
range[0] = Long.parseLong(value.substring(space, dash - 1));
|
||||||
|
|
||||||
// end
|
// end
|
||||||
range[1] = Long.parseLong(value.substring(dash, bar));
|
range[1] = Long.parseLong(value.substring(dash, bar));
|
||||||
|
|
||||||
// resource length
|
// resource length
|
||||||
value = value.substring(bar + 1);
|
value = value.substring(bar + 1);
|
||||||
if (value.equals("*")) {
|
if (value.equals("*")) {
|
||||||
range[2] = -1;// unknown length received from the server but should be valid
|
range[2] = -1;// unknown length received from the server but should be valid
|
||||||
} else {
|
} else {
|
||||||
range[2] = Long.parseLong(value);
|
range[2] = Long.parseLong(value);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean test() {
|
private boolean test() {
|
||||||
if (mMission.urls[mMission.current] == null) return false;
|
if (mMission.urls[mMission.current] == null) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
|
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
|
||||||
mMission.establishConnection(mID, mConn);
|
mMission.establishConnection(mID, mConn);
|
||||||
|
|
||||||
if (mConn.getResponseCode() == 200) return true;
|
if (mConn.getResponseCode() == 200) return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
} finally {
|
} finally {
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disconnect() {
|
private void disconnect() {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
mConn.getInputStream().close();
|
mConn.getInputStream().close();
|
||||||
} finally {
|
} finally {
|
||||||
mConn.disconnect();
|
mConn.disconnect();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
} finally {
|
} finally {
|
||||||
mConn = null;
|
mConn = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void interrupt() {
|
public void interrupt() {
|
||||||
super.interrupt();
|
super.interrupt();
|
||||||
if (mConn != null) disconnect();
|
if (mConn != null) disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class FinishedMission extends Mission {
|
public class FinishedMission extends Mission {
|
||||||
|
|
||||||
public FinishedMission() {
|
public FinishedMission() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public FinishedMission(@NonNull DownloadMission mission) {
|
public FinishedMission(@NonNull DownloadMission mission) {
|
||||||
source = mission.source;
|
source = mission.source;
|
||||||
length = mission.length;
|
length = mission.length;
|
||||||
timestamp = mission.timestamp;
|
timestamp = mission.timestamp;
|
||||||
kind = mission.kind;
|
kind = mission.kind;
|
||||||
storage = mission.storage;
|
storage = mission.storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +1,64 @@
|
||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
import us.shandian.giga.io.StoredFileHelper;
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
|
|
||||||
public abstract class Mission implements Serializable {
|
public abstract class Mission implements Serializable {
|
||||||
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
|
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source url of the resource
|
* Source url of the resource
|
||||||
*/
|
*/
|
||||||
public String source;
|
public String source;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Length of the current resource
|
* Length of the current resource
|
||||||
*/
|
*/
|
||||||
public long length;
|
public long length;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creation timestamp (and maybe unique identifier)
|
* creation timestamp (and maybe unique identifier)
|
||||||
*/
|
*/
|
||||||
public long timestamp;
|
public long timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pre-defined content type
|
* pre-defined content type
|
||||||
*/
|
*/
|
||||||
public char kind;
|
public char kind;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The downloaded file
|
* The downloaded file
|
||||||
*/
|
*/
|
||||||
public StoredFileHelper storage;
|
public StoredFileHelper storage;
|
||||||
|
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the downloaded file
|
* Delete the downloaded file
|
||||||
*
|
*
|
||||||
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
|
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
|
||||||
*/
|
*/
|
||||||
public boolean delete() {
|
public boolean delete() {
|
||||||
if (storage != null) return storage.delete();
|
if (storage != null) return storage.delete();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate if this mission is deleted whatever is stored
|
* Indicate if this mission is deleted whatever is stored
|
||||||
*/
|
*/
|
||||||
public transient boolean deleted = false;
|
public transient boolean deleted = false;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
Calendar calendar = Calendar.getInstance();
|
Calendar calendar = Calendar.getInstance();
|
||||||
calendar.setTimeInMillis(timestamp);
|
calendar.setTimeInMillis(timestamp);
|
||||||
return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
|
return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package us.shandian.giga.io;
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
public interface ProgressReport {
|
public interface ProgressReport {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report the size of the new file
|
* Report the size of the new file
|
||||||
*
|
*
|
||||||
* @param progress the new size
|
* @param progress the new size
|
||||||
*/
|
*/
|
||||||
void report(long progress);
|
void report(long progress);
|
||||||
}
|
}
|
|
@ -1,44 +1,44 @@
|
||||||
package us.shandian.giga.postprocessing;
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.OggFromWebMWriter;
|
import org.schabi.newpipe.streams.OggFromWebMWriter;
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
class OggFromWebmDemuxer extends Postprocessing {
|
class OggFromWebmDemuxer extends Postprocessing {
|
||||||
|
|
||||||
OggFromWebmDemuxer() {
|
OggFromWebmDemuxer() {
|
||||||
super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER);
|
super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean test(SharpStream... sources) throws IOException {
|
boolean test(SharpStream... sources) throws IOException {
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||||
sources[0].read(buffer.array());
|
sources[0].read(buffer.array());
|
||||||
|
|
||||||
// youtube uses WebM as container, but the file extension (format suffix) is "*.opus"
|
// youtube uses WebM as container, but the file extension (format suffix) is "*.opus"
|
||||||
// check if the file is a webm/mkv file before proceed
|
// check if the file is a webm/mkv file before proceed
|
||||||
|
|
||||||
switch (buffer.getInt()) {
|
switch (buffer.getInt()) {
|
||||||
case 0x1a45dfa3:
|
case 0x1a45dfa3:
|
||||||
return true;// webm/mkv
|
return true;// webm/mkv
|
||||||
case 0x4F676753:
|
case 0x4F676753:
|
||||||
return false;// ogg
|
return false;// ogg
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream");
|
throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
|
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
|
||||||
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
|
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
|
||||||
demuxer.parseSource();
|
demuxer.parseSource();
|
||||||
demuxer.selectTrack(0);
|
demuxer.selectTrack(0);
|
||||||
demuxer.build();
|
demuxer.build();
|
||||||
|
|
||||||
return OK_RESULT;
|
return OK_RESULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,138 +1,138 @@
|
||||||
package us.shandian.giga.ui.common;
|
package us.shandian.giga.ui.common;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import us.shandian.giga.get.FinishedMission;
|
import us.shandian.giga.get.FinishedMission;
|
||||||
import us.shandian.giga.get.Mission;
|
import us.shandian.giga.get.Mission;
|
||||||
import us.shandian.giga.service.DownloadManager;
|
import us.shandian.giga.service.DownloadManager;
|
||||||
import us.shandian.giga.service.DownloadManager.MissionIterator;
|
import us.shandian.giga.service.DownloadManager.MissionIterator;
|
||||||
import us.shandian.giga.ui.adapter.MissionAdapter;
|
import us.shandian.giga.ui.adapter.MissionAdapter;
|
||||||
|
|
||||||
public class Deleter {
|
public class Deleter {
|
||||||
private static final int TIMEOUT = 5000;// ms
|
private static final int TIMEOUT = 5000;// ms
|
||||||
private static final int DELAY = 350;// ms
|
private static final int DELAY = 350;// ms
|
||||||
private static final int DELAY_RESUME = 400;// ms
|
private static final int DELAY_RESUME = 400;// ms
|
||||||
|
|
||||||
private Snackbar snackbar;
|
private Snackbar snackbar;
|
||||||
private ArrayList<Mission> items;
|
private ArrayList<Mission> items;
|
||||||
private boolean running = true;
|
private boolean running = true;
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final MissionAdapter mAdapter;
|
private final MissionAdapter mAdapter;
|
||||||
private final DownloadManager mDownloadManager;
|
private final DownloadManager mDownloadManager;
|
||||||
private final MissionIterator mIterator;
|
private final MissionIterator mIterator;
|
||||||
private final Handler mHandler;
|
private final Handler mHandler;
|
||||||
private final View mView;
|
private final View mView;
|
||||||
|
|
||||||
private final Runnable rShow;
|
private final Runnable rShow;
|
||||||
private final Runnable rNext;
|
private final Runnable rNext;
|
||||||
private final Runnable rCommit;
|
private final Runnable rCommit;
|
||||||
|
|
||||||
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
||||||
mView = v;
|
mView = v;
|
||||||
mContext = c;
|
mContext = c;
|
||||||
mAdapter = a;
|
mAdapter = a;
|
||||||
mDownloadManager = d;
|
mDownloadManager = d;
|
||||||
mIterator = i;
|
mIterator = i;
|
||||||
mHandler = h;
|
mHandler = h;
|
||||||
|
|
||||||
// use variables to know the reference of the lambdas
|
// use variables to know the reference of the lambdas
|
||||||
rShow = this::show;
|
rShow = this::show;
|
||||||
rNext = this::next;
|
rNext = this::next;
|
||||||
rCommit = this::commit;
|
rCommit = this::commit;
|
||||||
|
|
||||||
items = new ArrayList<>(2);
|
items = new ArrayList<>(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void append(Mission item) {
|
public void append(Mission item) {
|
||||||
mIterator.hide(item);
|
mIterator.hide(item);
|
||||||
items.add(0, item);
|
items.add(0, item);
|
||||||
|
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forget() {
|
private void forget() {
|
||||||
mIterator.unHide(items.remove(0));
|
mIterator.unHide(items.remove(0));
|
||||||
mAdapter.applyChanges();
|
mAdapter.applyChanges();
|
||||||
|
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void show() {
|
private void show() {
|
||||||
if (items.size() < 1) return;
|
if (items.size() < 1) return;
|
||||||
|
|
||||||
pause();
|
pause();
|
||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
mHandler.postDelayed(rNext, DELAY);
|
mHandler.postDelayed(rNext, DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void next() {
|
private void next() {
|
||||||
if (items.size() < 1) return;
|
if (items.size() < 1) return;
|
||||||
|
|
||||||
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
|
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
|
||||||
|
|
||||||
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
||||||
snackbar.setAction(R.string.undo, s -> forget());
|
snackbar.setAction(R.string.undo, s -> forget());
|
||||||
snackbar.setActionTextColor(Color.YELLOW);
|
snackbar.setActionTextColor(Color.YELLOW);
|
||||||
snackbar.show();
|
snackbar.show();
|
||||||
|
|
||||||
mHandler.postDelayed(rCommit, TIMEOUT);
|
mHandler.postDelayed(rCommit, TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void commit() {
|
private void commit() {
|
||||||
if (items.size() < 1) return;
|
if (items.size() < 1) return;
|
||||||
|
|
||||||
while (items.size() > 0) {
|
while (items.size() > 0) {
|
||||||
Mission mission = items.remove(0);
|
Mission mission = items.remove(0);
|
||||||
if (mission.deleted) continue;
|
if (mission.deleted) continue;
|
||||||
|
|
||||||
mIterator.unHide(mission);
|
mIterator.unHide(mission);
|
||||||
mDownloadManager.deleteMission(mission);
|
mDownloadManager.deleteMission(mission);
|
||||||
|
|
||||||
if (mission instanceof FinishedMission) {
|
if (mission instanceof FinishedMission) {
|
||||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
|
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.size() < 1) {
|
if (items.size() < 1) {
|
||||||
pause();
|
pause();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pause() {
|
public void pause() {
|
||||||
running = false;
|
running = false;
|
||||||
mHandler.removeCallbacks(rNext);
|
mHandler.removeCallbacks(rNext);
|
||||||
mHandler.removeCallbacks(rShow);
|
mHandler.removeCallbacks(rShow);
|
||||||
mHandler.removeCallbacks(rCommit);
|
mHandler.removeCallbacks(rCommit);
|
||||||
if (snackbar != null) snackbar.dismiss();
|
if (snackbar != null) snackbar.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resume() {
|
public void resume() {
|
||||||
if (running) return;
|
if (running) return;
|
||||||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (items.size() < 1) return;
|
if (items.size() < 1) return;
|
||||||
|
|
||||||
pause();
|
pause();
|
||||||
|
|
||||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||||
items = null;
|
items = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/item_name"
|
android:id="@+id/item_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="relative header"
|
android:text="relative header"
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="2dp"
|
android:layout_height="2dp"
|
||||||
android:background="@color/black_settings_accent_color" />
|
android:background="@color/black_settings_accent_color" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
Loading…
Reference in New Issue