NewPipe/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java

417 lines
13 KiB
Java

package org.schabi.newpipe.streams;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
/**
* @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 static final byte HEADER_CHECKSUM_OFFSET = 22;
private static final byte HEADER_SIZE = 27;
private static final int TIME_SCALE_NS = 1000000000;
private boolean done = false;
private boolean parsed = false;
private SharpStream source;
private SharpStream output;
private int sequenceCount = 0;
private final int streamId;
private byte packetFlag = FLAG_FIRST;
private WebMReader webm = null;
private WebMTrack webmTrack = null;
private Segment webmSegment = null;
private Cluster webmCluster = null;
private SimpleBlock webmBlock = null;
private long webmBlockLastTimecode = 0;
private long webmBlockNearDuration = 0;
private short segmentTableSize = 0;
private final byte[] segmentTable = new byte[255];
private long segmentTableNextTimestamp = TIME_SCALE_NS;
private final int[] crc32Table = new int[256];
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final 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.streamId = (int) System.currentTimeMillis();
populateCrc32Table();
}
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();
webmSegment = webm.getNextSegment();
} finally {
parsed = true;
}
}
public void selectTrack(final int trackIndex) throws IOException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
if (done) {
throw new IOException("already done");
}
if (webmTrack != 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 {
webmTrack = webm.selectTrack(trackIndex);
} finally {
parsed = true;
}
}
@Override
public void close() throws IOException {
done = true;
parsed = true;
webmTrack = null;
webm = null;
if (!output.isClosed()) {
output.flush();
}
source.close();
output.close();
}
public void build() throws IOException {
final float resolution;
SimpleBlock bloq;
final ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
final ByteBuffer page = ByteBuffer.allocate(64 * 1024);
header.order(ByteOrder.LITTLE_ENDIAN);
/* step 1: get the amount of frames per seconds */
switch (webmTrack.kind) {
case Audio:
resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata);
if (resolution == 0f) {
throw new RuntimeException("cannot get the audio sample rate");
}
break;
case Video:
// WARNING: untested
if (webmTrack.defaultDuration == 0) {
throw new RuntimeException("missing default frame time");
}
resolution = 1000f / ((float) webmTrack.defaultDuration
/ webmSegment.info.timecodeScale);
break;
default:
throw new RuntimeException("not implemented");
}
/* step 2: create packet with code init data */
if (webmTrack.codecPrivate != null) {
addPacketSegment(webmTrack.codecPrivate.length);
makePacketheader(0x00, header, webmTrack.codecPrivate);
write(header);
output.write(webmTrack.codecPrivate);
}
/* step 3: create packet with metadata */
final byte[] buffer = makeMetadata();
if (buffer != null) {
addPacketSegment(buffer.length);
makePacketheader(0x00, header, buffer);
write(header);
output.write(buffer);
}
/* step 4: calculate amount of packets */
while (webmSegment != null) {
bloq = getNextBlock();
if (bloq != null && addPacketSegment(bloq)) {
final int pos = page.position();
//noinspection ResultOfMethodCallIgnored
bloq.data.read(page.array(), pos, bloq.dataSize);
page.position(pos + bloq.dataSize);
continue;
}
// calculate the current packet duration using the next block
double elapsedNs = webmTrack.codecDelay;
if (bloq == null) {
packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed
elapsedNs += webmBlockLastTimecode;
if (webmTrack.defaultDuration > 0) {
elapsedNs += webmTrack.defaultDuration;
} else {
// hardcoded way, guess the sample duration
elapsedNs += webmBlockNearDuration;
}
} else {
elapsedNs += bloq.absoluteTimeCodeNs;
}
// get the sample count in the page
elapsedNs = elapsedNs / TIME_SCALE_NS;
elapsedNs = Math.ceil(elapsedNs * resolution);
// create header and calculate page checksum
int checksum = makePacketheader((long) elapsedNs, header, null);
checksum = calcCrc32(checksum, page.array(), page.position());
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
// dump data
write(header);
write(page);
webmBlock = bloq;
}
}
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
final byte[] immediatePage) {
short length = HEADER_SIZE;
buffer.putInt(0x5367674f); // "OggS" binary string in little-endian
buffer.put((byte) 0x00); // version
buffer.put(packetFlag); // type
buffer.putLong(granPos); // granulate position
buffer.putInt(streamId); // bitstream serial number
buffer.putInt(sequenceCount++); // page sequence number
buffer.putInt(0x00); // page checksum
buffer.put((byte) segmentTableSize); // segment table
buffer.put(segmentTable, 0, segmentTableSize); // segment size
length += segmentTableSize;
clearSegmentTable(); // clear segment table for next header
int checksumCrc32 = calcCrc32(0x00, buffer.array(), length);
if (immediatePage != null) {
checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length);
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32);
segmentTableNextTimestamp -= TIME_SCALE_NS;
}
return checksumCrc32;
}
@Nullable
private byte[] makeMetadata() {
if ("A_OPUS".equals(webmTrack.codecId)) {
return new byte[]{
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 // additional tags count (zero means no tags)
};
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
return new byte[]{
0x03, // ¿¿¿???
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
}
// not implemented for the desired codec
return null;
}
private void write(final ByteBuffer buffer) throws IOException {
output.write(buffer.array(), 0, buffer.position());
buffer.position(0);
}
@Nullable
private SimpleBlock getNextBlock() throws IOException {
SimpleBlock res;
if (webmBlock != null) {
res = webmBlock;
webmBlock = null;
return res;
}
if (webmSegment == null) {
webmSegment = webm.getNextSegment();
if (webmSegment == null) {
return null; // no more blocks in the selected track
}
}
if (webmCluster == null) {
webmCluster = webmSegment.getNextCluster();
if (webmCluster == null) {
webmSegment = null;
return getNextBlock();
}
}
res = webmCluster.getNextSimpleBlock();
if (res == null) {
webmCluster = null;
return getNextBlock();
}
webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode;
webmBlockLastTimecode = res.absoluteTimeCodeNs;
return res;
}
private float getSampleFrequencyFromTrack(final byte[] bMetadata) {
// hardcoded way
final ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
while (buffer.remaining() >= 6) {
final int id = buffer.getShort() & 0xFFFF;
if (id == 0x0000B584) {
return buffer.getFloat();
}
}
return 0.0f;
}
private void clearSegmentTable() {
segmentTableNextTimestamp += TIME_SCALE_NS;
packetFlag = FLAG_UNSET;
segmentTableSize = 0;
}
private boolean addPacketSegment(final SimpleBlock block) {
final long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay;
if (timestamp >= segmentTableNextTimestamp) {
return false;
}
return addPacketSegment(block.dataSize);
}
private boolean addPacketSegment(final int size) {
if (size > 65025) {
throw new UnsupportedOperationException("page size cannot be larger than 65025");
}
int available = (segmentTable.length - segmentTableSize) * 255;
final boolean extra = (size % 255) == 0;
if (extra) {
// add a zero byte entry in the table
// required to indicate the sample size is multiple of 255
available -= 255;
}
// check if possible add the segment, without overflow the table
if (available < size) {
return false; // not enough space on the page
}
for (int seg = size; seg > 0; seg -= 255) {
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
}
if (extra) {
segmentTable[segmentTableSize++] = 0x00;
}
return true;
}
private void populateCrc32Table() {
for (int i = 0; i < 0x100; i++) {
int crc = i << 24;
for (int j = 0; j < 8; j++) {
final long b = crc >>> 31;
crc <<= 1;
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
}
crc32Table[i] = crc;
}
}
private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) {
int crc = initialCrc;
for (int i = 0; i < size; i++) {
final int reg = (crc >>> 24) & 0xff;
crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)];
}
return crc;
}
}