mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-25 01:20:34 +00:00
implement webm to ogg demuxer
* used for opus audio stream * update WebMReader and WebMWriter * new post-processing algorithm
This commit is contained in:
parent
eeeeeef3a7
commit
773aa1eff0
@ -561,7 +561,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
mainStorage = mainStorageVideo;
|
mainStorage = mainStorageVideo;
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
mime = format.mimeType;
|
mime = format.mimeType;
|
||||||
filename += format.suffix;
|
filename += format == MediaFormat.OPUS ? "ogg" : format.suffix;
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
mainStorage = mainStorageVideo;// subtitle & video files go together
|
mainStorage = mainStorageVideo;// subtitle & video files go together
|
||||||
@ -778,6 +778,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||||
|
} else if (selectedStream.getFormat() == MediaFormat.OPUS) {
|
||||||
|
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
|
@ -0,0 +1,488 @@
|
|||||||
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import android.support.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 java.util.ArrayList;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
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 SEGMENTS_PER_PACKET = 50;// used in ffmpeg, which is near 1 second at 48kHz
|
||||||
|
private final static byte HEADER_CHECKSUM_OFFSET = 22;
|
||||||
|
|
||||||
|
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 WebMReader webm = null;
|
||||||
|
private WebMTrack webm_track = null;
|
||||||
|
private int track_index = 0;
|
||||||
|
|
||||||
|
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 = (new Random(System.currentTimeMillis())).nextInt();
|
||||||
|
|
||||||
|
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);
|
||||||
|
track_index = 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;
|
||||||
|
int read;
|
||||||
|
byte[] buffer;
|
||||||
|
int checksum;
|
||||||
|
byte flag = FLAG_FIRST;// obligatory
|
||||||
|
|
||||||
|
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 1.1: write codec init data, in most cases must be present */
|
||||||
|
if (webm_track.codecPrivate != null) {
|
||||||
|
addPacketSegment(webm_track.codecPrivate.length);
|
||||||
|
dump_packetHeader(flag, 0x00, webm_track.codecPrivate);
|
||||||
|
flag = FLAG_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* step 1.2: write metadata */
|
||||||
|
buffer = make_metadata();
|
||||||
|
if (buffer != null) {
|
||||||
|
addPacketSegment(buffer.length);
|
||||||
|
dump_packetHeader(flag, 0x00, buffer);
|
||||||
|
flag = FLAG_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = new byte[8 * 1024];
|
||||||
|
|
||||||
|
/* step 1.3: write headers */
|
||||||
|
long approx_packets = webm_segment.info.duration / webm_segment.info.timecodeScale;
|
||||||
|
approx_packets = approx_packets / (approx_packets / SEGMENTS_PER_PACKET);
|
||||||
|
|
||||||
|
ArrayList<Long> pending_offsets = new ArrayList<>((int) approx_packets);
|
||||||
|
ArrayList<Integer> pending_checksums = new ArrayList<>((int) approx_packets);
|
||||||
|
ArrayList<Short> data_offsets = new ArrayList<>((int) approx_packets);
|
||||||
|
|
||||||
|
int page_size = 0;
|
||||||
|
SimpleBlock bloq;
|
||||||
|
|
||||||
|
while (webm_segment != null) {
|
||||||
|
bloq = getNextBlock();
|
||||||
|
|
||||||
|
if (bloq != null && addPacketSegment(bloq.dataSize)) {
|
||||||
|
page_size += bloq.dataSize;
|
||||||
|
|
||||||
|
if (segment_table_size < SEGMENTS_PER_PACKET) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the current packet duration using the next block
|
||||||
|
bloq = getNextBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
double elapsed_ns = webm_track.codecDelay;
|
||||||
|
|
||||||
|
if (bloq == null) {
|
||||||
|
flag = FLAG_LAST;
|
||||||
|
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 / 1000000000d) * resolution;
|
||||||
|
elapsed_ns = Math.ceil(elapsed_ns);
|
||||||
|
|
||||||
|
long offset = output_offset + HEADER_CHECKSUM_OFFSET;
|
||||||
|
pending_offsets.add(offset);
|
||||||
|
|
||||||
|
checksum = dump_packetHeader(flag, (long) elapsed_ns, null);
|
||||||
|
pending_checksums.add(checksum);
|
||||||
|
|
||||||
|
data_offsets.add((short) (output_offset - offset));
|
||||||
|
|
||||||
|
// reserve space in the page
|
||||||
|
while (page_size > 0) {
|
||||||
|
int write = Math.min(page_size, buffer.length);
|
||||||
|
out_write(buffer, write);
|
||||||
|
page_size -= write;
|
||||||
|
}
|
||||||
|
|
||||||
|
webm_block = bloq;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* step 2.1: write stream data */
|
||||||
|
output.rewind();
|
||||||
|
output_offset = 0;
|
||||||
|
|
||||||
|
source.rewind();
|
||||||
|
|
||||||
|
webm = new WebMReader(source);
|
||||||
|
webm.parse();
|
||||||
|
webm_track = webm.selectTrack(track_index);
|
||||||
|
|
||||||
|
for (int i = 0; i < pending_offsets.size(); i++) {
|
||||||
|
checksum = pending_checksums.get(i);
|
||||||
|
segment_table_size = 0;
|
||||||
|
|
||||||
|
out_seek(pending_offsets.get(i) + data_offsets.get(i));
|
||||||
|
|
||||||
|
while (segment_table_size < SEGMENTS_PER_PACKET) {
|
||||||
|
bloq = getNextBlock();
|
||||||
|
|
||||||
|
if (bloq == null || !addPacketSegment(bloq.dataSize)) {
|
||||||
|
webm_block = bloq;// use this block later (if not null)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: calling bloq.data.close() is unnecessary
|
||||||
|
while ((read = bloq.data.read(buffer)) != -1) {
|
||||||
|
out_write(buffer, read);
|
||||||
|
checksum = calc_crc32(checksum, buffer, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_checksums.set(i, checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* step 2.2: write every checksum */
|
||||||
|
output.rewind();
|
||||||
|
output_offset = 0;
|
||||||
|
buffer = new byte[4];
|
||||||
|
|
||||||
|
ByteBuffer buff = ByteBuffer.wrap(buffer);
|
||||||
|
buff.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
for (int i = 0; i < pending_checksums.size(); i++) {
|
||||||
|
out_seek(pending_offsets.get(i));
|
||||||
|
buff.putInt(0, pending_checksums.get(i));
|
||||||
|
out_write(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dump_packetHeader(byte flag, long gran_pos, byte[] immediate_page) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(27 + segment_table_size);
|
||||||
|
|
||||||
|
buffer.putInt(0x4F676753);// "OggS" binary string
|
||||||
|
buffer.put((byte) 0x00);// version
|
||||||
|
buffer.put(flag);// type
|
||||||
|
|
||||||
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
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.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
buffer.put((byte) segment_table_size);// segment table
|
||||||
|
buffer.put(segment_table, 0, segment_table_size);// segment size
|
||||||
|
|
||||||
|
segment_table_size = 0;// clear segment table for next header
|
||||||
|
|
||||||
|
byte[] buff = buffer.array();
|
||||||
|
int checksum_crc32 = calc_crc32(0x00, buff, buff.length);
|
||||||
|
|
||||||
|
if (immediate_page != null) {
|
||||||
|
checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length);
|
||||||
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32);
|
||||||
|
|
||||||
|
out_write(buff);
|
||||||
|
out_write(immediate_page);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_write(buff);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold defaultstate="collapsed" desc="WebM track handling">
|
||||||
|
private Segment webm_segment = null;
|
||||||
|
private Cluster webm_cluter = null;
|
||||||
|
private SimpleBlock webm_block = null;
|
||||||
|
private long webm_block_last_timecode = 0;
|
||||||
|
private long webm_block_near_duration = 0;
|
||||||
|
|
||||||
|
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_cluter == null) {
|
||||||
|
webm_cluter = webm_segment.getNextCluster();
|
||||||
|
if (webm_cluter == null) {
|
||||||
|
webm_segment = null;
|
||||||
|
return getNextBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = webm_cluter.getNextSimpleBlock();
|
||||||
|
if (res == null) {
|
||||||
|
webm_cluter = 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;
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
//<editor-fold defaultstate="collapsed" desc="Segment table store">
|
||||||
|
private int segment_table_size = 0;
|
||||||
|
private final byte[] segment_table = new byte[255];
|
||||||
|
|
||||||
|
private boolean addPacketSegment(long size) {
|
||||||
|
// check if possible add the segment, without overflow the table
|
||||||
|
int available = (segment_table.length - segment_table_size) * 255;
|
||||||
|
if (available < size) {
|
||||||
|
return false;// not enough space on the page
|
||||||
|
}
|
||||||
|
|
||||||
|
while (size > 0) {
|
||||||
|
segment_table[segment_table_size++] = (byte) Math.min(size, 255);
|
||||||
|
size -= 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
//<editor-fold defaultstate="collapsed" desc="Output handling">
|
||||||
|
private long output_offset = 0;
|
||||||
|
|
||||||
|
private void out_write(byte[] buffer) throws IOException {
|
||||||
|
output.write(buffer);
|
||||||
|
output_offset += buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void out_write(byte[] buffer, int size) throws IOException {
|
||||||
|
output.write(buffer, 0, size);
|
||||||
|
output_offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void out_seek(long offset) throws IOException {
|
||||||
|
//if (output.canSeek()) { output.seek(offset); }
|
||||||
|
output.skip(offset - output_offset);
|
||||||
|
output_offset = offset;
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
//<editor-fold defaultstate="collapsed" desc="Checksum CRC32">
|
||||||
|
private final int[] crc32_table = new int[256];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
}
|
@ -15,7 +15,7 @@ import java.util.NoSuchElementException;
|
|||||||
*/
|
*/
|
||||||
public class WebMReader {
|
public class WebMReader {
|
||||||
|
|
||||||
//<editor-fold defaultState="collapsed" desc="constants">
|
//<editor-fold defaultstate="collapsed" desc="constants">
|
||||||
private final static int ID_EMBL = 0x0A45DFA3;
|
private final static int ID_EMBL = 0x0A45DFA3;
|
||||||
private final static int ID_EMBLReadVersion = 0x02F7;
|
private final static int ID_EMBLReadVersion = 0x02F7;
|
||||||
private final static int ID_EMBLDocType = 0x0282;
|
private final static int ID_EMBLDocType = 0x0282;
|
||||||
@ -37,10 +37,13 @@ public class WebMReader {
|
|||||||
private final static int ID_Audio = 0x61;
|
private final static int ID_Audio = 0x61;
|
||||||
private final static int ID_DefaultDuration = 0x3E383;
|
private final static int ID_DefaultDuration = 0x3E383;
|
||||||
private final static int ID_FlagLacing = 0x1C;
|
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_Cluster = 0x0F43B675;
|
||||||
private final static int ID_Timecode = 0x67;
|
private final static int ID_Timecode = 0x67;
|
||||||
private final static int ID_SimpleBlock = 0x23;
|
private final static int ID_SimpleBlock = 0x23;
|
||||||
|
private final static int ID_Block = 0x21;
|
||||||
|
private final static int ID_GroupBlock = 0x20;
|
||||||
//</editor-fold>
|
//</editor-fold>
|
||||||
|
|
||||||
public enum TrackKind {
|
public enum TrackKind {
|
||||||
@ -96,7 +99,7 @@ public class WebMReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensure(segment.ref);
|
ensure(segment.ref);
|
||||||
|
// WARNING: track cannot be the same or have different index in new segments
|
||||||
Element elem = untilElement(null, ID_Segment);
|
Element elem = untilElement(null, ID_Segment);
|
||||||
if (elem == null) {
|
if (elem == null) {
|
||||||
done = true;
|
done = true;
|
||||||
@ -189,6 +192,9 @@ public class WebMReader {
|
|||||||
Element elem;
|
Element elem;
|
||||||
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
||||||
elem = readElement();
|
elem = readElement();
|
||||||
|
if (expected.length < 1) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
for (int type : expected) {
|
for (int type : expected) {
|
||||||
if (elem.type == type) {
|
if (elem.type == type) {
|
||||||
return elem;
|
return elem;
|
||||||
@ -300,9 +306,7 @@ public class WebMReader {
|
|||||||
WebMTrack entry = new WebMTrack();
|
WebMTrack entry = new WebMTrack();
|
||||||
boolean drop = false;
|
boolean drop = false;
|
||||||
Element elem;
|
Element elem;
|
||||||
while ((elem = untilElement(elem_trackEntry,
|
while ((elem = untilElement(elem_trackEntry)) != null) {
|
||||||
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
|
|
||||||
)) != null) {
|
|
||||||
switch (elem.type) {
|
switch (elem.type) {
|
||||||
case ID_TrackNumber:
|
case ID_TrackNumber:
|
||||||
entry.trackNumber = readNumber(elem);
|
entry.trackNumber = readNumber(elem);
|
||||||
@ -326,8 +330,9 @@ public class WebMReader {
|
|||||||
case ID_FlagLacing:
|
case ID_FlagLacing:
|
||||||
drop = readNumber(elem) != lacingExpected;
|
drop = readNumber(elem) != lacingExpected;
|
||||||
break;
|
break;
|
||||||
|
case ID_CodecDelay:
|
||||||
|
entry.codecDelay = readNumber(elem);
|
||||||
default:
|
default:
|
||||||
System.out.println();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ensure(elem);
|
ensure(elem);
|
||||||
@ -360,12 +365,13 @@ public class WebMReader {
|
|||||||
|
|
||||||
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
|
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
|
||||||
SimpleBlock obj = new SimpleBlock(ref);
|
SimpleBlock obj = new SimpleBlock(ref);
|
||||||
obj.dataSize = stream.position();
|
|
||||||
obj.trackNumber = readEncodedNumber();
|
obj.trackNumber = readEncodedNumber();
|
||||||
obj.relativeTimeCode = stream.readShort();
|
obj.relativeTimeCode = stream.readShort();
|
||||||
obj.flags = (byte) stream.read();
|
obj.flags = (byte) stream.read();
|
||||||
obj.dataSize = (ref.offset + ref.size) - stream.position();
|
obj.dataSize = (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) {
|
if (obj.dataSize < 0) {
|
||||||
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
||||||
}
|
}
|
||||||
@ -409,6 +415,7 @@ public class WebMReader {
|
|||||||
public byte[] bMetadata;
|
public byte[] bMetadata;
|
||||||
public TrackKind kind;
|
public TrackKind kind;
|
||||||
public long defaultDuration;
|
public long defaultDuration;
|
||||||
|
public long codecDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Segment {
|
public class Segment {
|
||||||
@ -448,6 +455,7 @@ public class WebMReader {
|
|||||||
public class SimpleBlock {
|
public class SimpleBlock {
|
||||||
|
|
||||||
public InputStream data;
|
public InputStream data;
|
||||||
|
public boolean createdFromBlock;
|
||||||
|
|
||||||
SimpleBlock(Element ref) {
|
SimpleBlock(Element ref) {
|
||||||
this.ref = ref;
|
this.ref = ref;
|
||||||
@ -455,6 +463,7 @@ public class WebMReader {
|
|||||||
|
|
||||||
public long trackNumber;
|
public long trackNumber;
|
||||||
public short relativeTimeCode;
|
public short relativeTimeCode;
|
||||||
|
public long absoluteTimeCodeNs;
|
||||||
public byte flags;
|
public byte flags;
|
||||||
public long dataSize;
|
public long dataSize;
|
||||||
private final Element ref;
|
private final Element ref;
|
||||||
@ -468,33 +477,55 @@ public class WebMReader {
|
|||||||
|
|
||||||
Element ref;
|
Element ref;
|
||||||
SimpleBlock currentSimpleBlock = null;
|
SimpleBlock currentSimpleBlock = null;
|
||||||
|
Element currentBlockGroup = null;
|
||||||
public long timecode;
|
public long timecode;
|
||||||
|
|
||||||
Cluster(Element ref) {
|
Cluster(Element ref) {
|
||||||
this.ref = ref;
|
this.ref = ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean check() {
|
boolean insideClusterBounds() {
|
||||||
return stream.position() >= (ref.offset + ref.size);
|
return stream.position() >= (ref.offset + ref.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SimpleBlock getNextSimpleBlock() throws IOException {
|
public SimpleBlock getNextSimpleBlock() throws IOException {
|
||||||
if (check()) {
|
if (insideClusterBounds()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentSimpleBlock != null) {
|
|
||||||
|
if (currentBlockGroup != null) {
|
||||||
|
ensure(currentBlockGroup);
|
||||||
|
currentBlockGroup = null;
|
||||||
|
currentSimpleBlock = null;
|
||||||
|
} else if (currentSimpleBlock != null) {
|
||||||
ensure(currentSimpleBlock.ref);
|
ensure(currentSimpleBlock.ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!check()) {
|
while (!insideClusterBounds()) {
|
||||||
Element elem = untilElement(ref, ID_SimpleBlock);
|
Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock);
|
||||||
if (elem == null) {
|
if (elem == null) {
|
||||||
return 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);
|
currentSimpleBlock = readSimpleBlock(elem);
|
||||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||||
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
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;
|
return currentSimpleBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ 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.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@ -17,7 +18,7 @@ import java.util.ArrayList;
|
|||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
public class WebMWriter {
|
public class WebMWriter implements Closeable {
|
||||||
|
|
||||||
private final static int BUFFER_SIZE = 8 * 1024;
|
private final static int BUFFER_SIZE = 8 * 1024;
|
||||||
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
|
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
|
||||||
@ -35,7 +36,7 @@ public class WebMWriter {
|
|||||||
private long written = 0;
|
private long written = 0;
|
||||||
|
|
||||||
private Segment[] readersSegment;
|
private Segment[] readersSegment;
|
||||||
private Cluster[] readersCluter;
|
private Cluster[] readersCluster;
|
||||||
|
|
||||||
private int[] predefinedDurations;
|
private int[] predefinedDurations;
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ public class WebMWriter {
|
|||||||
public void selectTracks(int... trackIndex) throws IOException {
|
public void selectTracks(int... trackIndex) throws IOException {
|
||||||
try {
|
try {
|
||||||
readersSegment = new Segment[readers.length];
|
readersSegment = new Segment[readers.length];
|
||||||
readersCluter = new Cluster[readers.length];
|
readersCluster = new Cluster[readers.length];
|
||||||
predefinedDurations = new int[readers.length];
|
predefinedDurations = new int[readers.length];
|
||||||
|
|
||||||
for (int i = 0; i < readers.length; i++) {
|
for (int i = 0; i < readers.length; i++) {
|
||||||
@ -102,6 +103,7 @@ public class WebMWriter {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
done = true;
|
done = true;
|
||||||
parsed = true;
|
parsed = true;
|
||||||
@ -114,7 +116,7 @@ public class WebMWriter {
|
|||||||
readers = null;
|
readers = null;
|
||||||
infoTracks = null;
|
infoTracks = null;
|
||||||
readersSegment = null;
|
readersSegment = null;
|
||||||
readersCluter = null;
|
readersCluster = null;
|
||||||
outBuffer = null;
|
outBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,17 +336,17 @@ public class WebMWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readersCluter[internalTrackId] == null) {
|
if (readersCluster[internalTrackId] == null) {
|
||||||
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||||
if (readersCluter[internalTrackId] == null) {
|
if (readersCluster[internalTrackId] == null) {
|
||||||
readersSegment[internalTrackId] = null;
|
readersSegment[internalTrackId] = null;
|
||||||
return getNextBlockFrom(internalTrackId);
|
return getNextBlockFrom(internalTrackId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
|
SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock();
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
readersCluter[internalTrackId] = null;
|
readersCluster[internalTrackId] = null;
|
||||||
return new Block();// fake block to indicate the end of the cluster
|
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.dataSize = (int) res.dataSize;
|
||||||
bloq.trackNumber = internalTrackId;
|
bloq.trackNumber = internalTrackId;
|
||||||
bloq.flags = res.flags;
|
bloq.flags = res.flags;
|
||||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE;
|
||||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
|
||||||
|
|
||||||
return bloq;
|
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 {
|
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||||
if (stream.canSeek()) {
|
if (stream.canSeek()) {
|
||||||
stream.seek(offset);
|
stream.seek(offset);
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
|
import android.support.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(false, 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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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_WEBM_MUXER = "webm";
|
||||||
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
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_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) {
|
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
|
||||||
Postprocessing instance;
|
Postprocessing instance;
|
||||||
@ -45,6 +46,9 @@ public abstract class Postprocessing implements Serializable {
|
|||||||
case ALGORITHM_M4A_NO_DASH:
|
case ALGORITHM_M4A_NO_DASH:
|
||||||
instance = new M4aNoDash();
|
instance = new M4aNoDash();
|
||||||
break;
|
break;
|
||||||
|
case ALGORITHM_OGG_FROM_WEBM_DEMUXER:
|
||||||
|
instance = new OggFromWebmDemuxer();
|
||||||
|
break;
|
||||||
/*case "example-algorithm":
|
/*case "example-algorithm":
|
||||||
instance = new ExampleAlgorithm();*/
|
instance = new ExampleAlgorithm();*/
|
||||||
default:
|
default:
|
||||||
@ -212,7 +216,7 @@ public abstract class Postprocessing implements Serializable {
|
|||||||
*
|
*
|
||||||
* @param out output stream
|
* @param out output stream
|
||||||
* @param sources files to be processed
|
* @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.
|
* @throws IOException if an I/O error occurs.
|
||||||
*/
|
*/
|
||||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||||
|
Loading…
Reference in New Issue
Block a user