mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +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:
		| @@ -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; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 kapodamy
					kapodamy