Add documentation and replace magic numbers by constants
This commit is contained in:
parent
a91161525a
commit
cb71440619
@ -21,11 +21,13 @@ import java.util.ArrayList;
|
||||
/**
|
||||
* MP4 muxer that builds a standard MP4 file from DASH fragmented MP4 sources.
|
||||
*
|
||||
* @author kapodamy
|
||||
* <p>
|
||||
* See <a href="https://atomicparsley.sourceforge.net/mpeg-4files.html">
|
||||
* https://atomicparsley.sourceforge.net/mpeg-4files.html</a> for information on
|
||||
* the MP4 file format and its specification.
|
||||
* </p>
|
||||
*
|
||||
* @implNote See <a href="https://atomicparsley.sourceforge.net/mpeg-4files.html">
|
||||
* https://atomicparsley.sourceforge.net/mpeg-4files.html</a> for information on
|
||||
* the MP4 file format and its specification.
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class Mp4FromDashWriter {
|
||||
private static final int EPOCH_OFFSET = 2082844800;
|
||||
@ -783,7 +785,7 @@ public class Mp4FromDashWriter {
|
||||
final int mediaTime;
|
||||
|
||||
if (tracks[index].trak.edstElst == null) {
|
||||
// is a audio track ¿is edst/elst optional for audio tracks?
|
||||
// is an audio track; is edst/elst optional for audio tracks?
|
||||
mediaTime = 0x00; // ffmpeg set this value as zero, instead of defaultMediaTime
|
||||
bMediaRate = 0x00010000;
|
||||
} else {
|
||||
@ -891,28 +893,35 @@ public class Mp4FromDashWriter {
|
||||
return offset + 0x14;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Sample Group Description Box.
|
||||
*
|
||||
* <p>
|
||||
* What does it do?
|
||||
* <br>
|
||||
* The table inside of this box gives information about the
|
||||
* characteristics of sample groups. The descriptive information is any other
|
||||
* information needed to define or characterize the sample group.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* ¿is replicable this box?
|
||||
* <br>
|
||||
* NO due lacks of documentation about this box but...
|
||||
* most of m4a encoders and ffmpeg uses this box with dummy values (same values)
|
||||
* </p>
|
||||
*
|
||||
* @return byte array with the 'sgpd' box
|
||||
*/
|
||||
private byte[] makeSgpd() {
|
||||
/*
|
||||
* Sample Group Description Box
|
||||
*
|
||||
* ¿whats does?
|
||||
* the table inside of this box gives information about the
|
||||
* characteristics of sample groups. The descriptive information is any other
|
||||
* information needed to define or characterize the sample group.
|
||||
*
|
||||
* ¿is replicable this box?
|
||||
* NO due lacks of documentation about this box but...
|
||||
* most of m4a encoders and ffmpeg uses this box with dummy values (same values)
|
||||
*/
|
||||
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(new byte[] {
|
||||
0x00, 0x00, 0x00, 0x1A, // box size
|
||||
0x73, 0x67, 0x70, 0x64, // "sgpd"
|
||||
0x01, 0x00, 0x00, 0x00, // box flags (unknown flag sets)
|
||||
0x72, 0x6F, 0x6C, 0x6C, // ¿¿group type??
|
||||
0x00, 0x00, 0x00, 0x02, // ¿¿??
|
||||
0x00, 0x00, 0x00, 0x01, // ¿¿??
|
||||
(byte) 0xFF, (byte) 0xFF // ¿¿??
|
||||
0x72, 0x6F, 0x6C, 0x6C, // group type??
|
||||
0x00, 0x00, 0x00, 0x02, // ??
|
||||
0x00, 0x00, 0x00, 0x01, // ??
|
||||
(byte) 0xFF, (byte) 0xFF // ??
|
||||
});
|
||||
|
||||
return buffer.array();
|
||||
@ -955,6 +964,7 @@ public class Mp4FromDashWriter {
|
||||
writeMetaItem("©ART", artist);
|
||||
}
|
||||
if (date != null && !date.isEmpty()) {
|
||||
// this means 'year' in mp4 metadata, who the hell thought that?
|
||||
writeMetaItem("©day", date);
|
||||
}
|
||||
|
||||
@ -1037,8 +1047,11 @@ public class Mp4FromDashWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to write cover image inside the 'udta' box.
|
||||
*
|
||||
* Helper to add cover image inside the 'udta' box.
|
||||
* <p>
|
||||
* This method writes the 'covr' metadata item which contains the cover image.
|
||||
* The cover image is displayed as thumbnail in many media players and file managers.
|
||||
* </p>
|
||||
* <pre>
|
||||
* [size][key] [data_box]
|
||||
* data_box = [size]["data"][type(4bytes)][locale(4bytes)=0][payload]
|
||||
|
||||
@ -29,18 +29,64 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* This class is used to convert a WebM stream containing Opus or Vorbis audio
|
||||
* into an Ogg stream.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The following specifications are used for the implementation:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>FLAC: <a href="https://www.rfc-editor.org/rfc/rfc9639">RFC 9639</a></li>
|
||||
* <li>Opus: All specs can be found at <a href="https://opus-codec.org/docs/">
|
||||
* https://opus-codec.org/docs/</a>.
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc7845.html">RFC7845</a>
|
||||
* defines the Ogg encapsulation for Opus streams, i.e.the container format and metadata.
|
||||
* </li>
|
||||
* <li>Vorbis: <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html">Vorbis I</a></li>
|
||||
* </ul>
|
||||
*
|
||||
* @author kapodamy
|
||||
* @author tobigr
|
||||
*/
|
||||
public class OggFromWebMWriter implements Closeable {
|
||||
private static final String TAG = OggFromWebMWriter.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* No flags set.
|
||||
*/
|
||||
private static final byte FLAG_UNSET = 0x00;
|
||||
//private static final byte FLAG_CONTINUED = 0x01;
|
||||
/**
|
||||
* The packet is continued from previous the previous page.
|
||||
*/
|
||||
private static final byte FLAG_CONTINUED = 0x01;
|
||||
/**
|
||||
* BOS (beginning of stream).
|
||||
*/
|
||||
private static final byte FLAG_FIRST = 0x02;
|
||||
private static final byte FLAG_LAST = 0x04;
|
||||
/**
|
||||
* EOS (end of stream).
|
||||
*/
|
||||
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 static final int TIME_SCALE_NS = 1_000_000_000;
|
||||
|
||||
/**
|
||||
* The maximum size of a segment in the Ogg page, in bytes.
|
||||
* This is a fixed value defined by the Ogg specification.
|
||||
*/
|
||||
private static final int OGG_SEGMENT_SIZE = 255;
|
||||
|
||||
/**
|
||||
* The maximum size of the Opus packet in bytes, to be included in the Ogg page.
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-6">
|
||||
* RFC7845 6. Packet Size Limits</a>
|
||||
*/
|
||||
private static final int OPUS_MAX_PACKETS_PAGE_SIZE = 65_025;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
@ -62,7 +108,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
private long webmBlockNearDuration = 0;
|
||||
|
||||
private short segmentTableSize = 0;
|
||||
private final byte[] segmentTable = new byte[255];
|
||||
private final byte[] segmentTable = new byte[OGG_SEGMENT_SIZE];
|
||||
private long segmentTableNextTimestamp = TIME_SCALE_NS;
|
||||
|
||||
private final int[] crc32Table = new int[256];
|
||||
@ -203,16 +249,16 @@ public class OggFromWebMWriter implements Closeable {
|
||||
/* step 2: create packet with code init data */
|
||||
if (webmTrack.codecPrivate != null) {
|
||||
addPacketSegment(webmTrack.codecPrivate.length);
|
||||
makePacketheader(0x00, header, webmTrack.codecPrivate);
|
||||
makePacketHeader(0x00, header, webmTrack.codecPrivate);
|
||||
write(header);
|
||||
output.write(webmTrack.codecPrivate);
|
||||
}
|
||||
|
||||
/* step 3: create packet with metadata */
|
||||
final byte[] buffer = makeMetadata();
|
||||
final byte[] buffer = makeCommentHeader();
|
||||
if (buffer != null) {
|
||||
addPacketSegment(buffer.length);
|
||||
makePacketheader(0x00, header, buffer);
|
||||
makePacketHeader(0x00, header, buffer);
|
||||
write(header);
|
||||
output.write(buffer);
|
||||
}
|
||||
@ -251,7 +297,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
elapsedNs = Math.ceil(elapsedNs * resolution);
|
||||
|
||||
// create header and calculate page checksum
|
||||
int checksum = makePacketheader((long) elapsedNs, header, null);
|
||||
int checksum = makePacketHeader((long) elapsedNs, header, null);
|
||||
checksum = calcCrc32(checksum, page.array(), page.position());
|
||||
|
||||
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
|
||||
@ -264,7 +310,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
|
||||
private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffer,
|
||||
final byte[] immediatePage) {
|
||||
short length = HEADER_SIZE;
|
||||
|
||||
@ -297,10 +343,24 @@ public class OggFromWebMWriter implements Closeable {
|
||||
return checksumCrc32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the metadata header for the selected codec (Opus or Vorbis).
|
||||
*
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
|
||||
* RFC7845 5.2. Comment Header</a> for OPUS metadata header format
|
||||
* @see <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2">
|
||||
* Vorbis I 4.2. Header decode and decode setup</a> and
|
||||
* <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-820005">
|
||||
* Vorbis 5. comment field and header specification</a>
|
||||
* for VORBIS metadata header format
|
||||
*
|
||||
* @return the metadata header as a byte array, or null if the codec is not supported
|
||||
* for metadata generation
|
||||
*/
|
||||
@Nullable
|
||||
private byte[] makeMetadata() {
|
||||
private byte[] makeCommentHeader() {
|
||||
if (DEBUG) {
|
||||
Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId);
|
||||
Log.d(TAG, "Downloading media with codec ID " + webmTrack.codecId);
|
||||
}
|
||||
|
||||
if ("A_OPUS".equals(webmTrack.codecId)) {
|
||||
@ -315,19 +375,22 @@ public class OggFromWebMWriter implements Closeable {
|
||||
.getLocalDateTime()
|
||||
.format(DateTimeFormatter.ISO_DATE)));
|
||||
if (thumbnail != null) {
|
||||
metadata.add(makeOpusPictureTag(thumbnail));
|
||||
metadata.add(makeFlacPictureTag(thumbnail));
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d("OggFromWebMWriter", "Creating metadata header with this data:");
|
||||
metadata.forEach(p -> Log.d("OggFromWebMWriter", p.first + "=" + p.second));
|
||||
Log.d(TAG, "Creating metadata header with this data:");
|
||||
metadata.forEach(p -> Log.d(TAG, p.first + "=" + p.second));
|
||||
}
|
||||
|
||||
return makeOpusTagsHeader(metadata);
|
||||
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
|
||||
// See https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
|
||||
// for the Vorbis comment header format
|
||||
// TODO: add Vorbis metadata: same as Opus, but with the Vorbis comment header format
|
||||
return new byte[]{
|
||||
0x03, // ???
|
||||
0x03, // packet type for Vorbis comment header
|
||||
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)
|
||||
@ -358,27 +421,37 @@ public class OggFromWebMWriter implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@code METADATA_BLOCK_PICTURE} tag to the Opus metadata,
|
||||
* containing the provided bitmap as cover art.
|
||||
* Generates a FLAC picture block for the provided bitmap.
|
||||
*
|
||||
* <p>
|
||||
* One could also use the COVERART tag instead, but it is not as widely supported
|
||||
* as METADATA_BLOCK_PICTURE.
|
||||
* The {@code METADATA_BLOCK_PICTURE} tag is defined in the FLAC specification (RFC 9639)
|
||||
* and is supported by Opus and Vorbis metadata headers.
|
||||
* The picture block contains the image data which is converted to JPEG
|
||||
* and associated metadata such as picture type, dimensions, and color depth.
|
||||
* The image data is Base64-encoded as per specification.
|
||||
* </p>
|
||||
*
|
||||
* @param bitmap The bitmap to use as cover art
|
||||
* @return The key-value pair representing the tag
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8">
|
||||
* RFC 9639 8.8 Picture</a>
|
||||
*
|
||||
* @param bitmap The bitmap to use for the picture block
|
||||
* @return The key-value pair representing the tag.
|
||||
* The key is {@code METADATA_BLOCK_PICTURE}
|
||||
* and the value is the Base64-encoded FLAC picture block.
|
||||
*/
|
||||
private static Pair<String, String> makeOpusPictureTag(final Bitmap bitmap) {
|
||||
private static Pair<String, String> makeFlacPictureTag(final Bitmap bitmap) {
|
||||
// FLAC picture block format (big-endian):
|
||||
// uint32 picture_type
|
||||
// uint32 mime_length, mime_string
|
||||
// uint32 desc_length, desc_string
|
||||
// uint32 mime_length,
|
||||
// mime_string
|
||||
// uint32 desc_length,
|
||||
// desc_string
|
||||
// uint32 width
|
||||
// uint32 height
|
||||
// uint32 color_depth
|
||||
// uint32 colors_indexed
|
||||
// uint32 data_length, data_bytes
|
||||
// uint32 data_length,
|
||||
// data_bytes
|
||||
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
|
||||
@ -389,7 +462,11 @@ public class OggFromWebMWriter implements Closeable {
|
||||
// fixed ints + mime + desc
|
||||
final int headerSize = 4 * 8 + mimeBytes.length + descBytes.length;
|
||||
final ByteBuffer buf = ByteBuffer.allocate(headerSize + imageData.length);
|
||||
buf.putInt(3); // picture type: 3 = Cover (front)
|
||||
// See https://www.rfc-editor.org/rfc/rfc9639.html#table-13 for the complete list
|
||||
// of picture types
|
||||
// TODO: allow specifying other picture types, i.e. cover (front) for music albums;
|
||||
// but this info needs to be provided by the extractor first.
|
||||
buf.putInt(3); // picture type: 0 = Other, 2 = Cover (front)
|
||||
buf.putInt(mimeBytes.length);
|
||||
buf.put(mimeBytes);
|
||||
buf.putInt(descBytes.length);
|
||||
@ -397,10 +474,10 @@ public class OggFromWebMWriter implements Closeable {
|
||||
if (descBytes.length > 0) {
|
||||
buf.put(descBytes);
|
||||
}
|
||||
buf.putInt(bitmap.getWidth()); // width (unknown)
|
||||
buf.putInt(bitmap.getHeight()); // height (unknown)
|
||||
buf.putInt(0); // color depth
|
||||
buf.putInt(0); // colors indexed
|
||||
buf.putInt(bitmap.getWidth());
|
||||
buf.putInt(bitmap.getHeight());
|
||||
buf.putInt(24); // color depth for JPEG and PNG is usually 24 bits
|
||||
buf.putInt(0); // colors indexed (0 for non-indexed images like JPEG)
|
||||
buf.putInt(imageData.length);
|
||||
buf.put(imageData);
|
||||
final String b64 = Base64.getEncoder().encodeToString(buf.array());
|
||||
@ -413,6 +490,9 @@ public class OggFromWebMWriter implements Closeable {
|
||||
* You probably want to use makeOpusMetadata(), which uses this function to create
|
||||
* a header with sensible metadata filled in.
|
||||
*
|
||||
* @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
|
||||
* RFC7845 5.2</a>
|
||||
*
|
||||
* @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping
|
||||
* from one key to multiple values.
|
||||
* @return The binary header
|
||||
@ -431,6 +511,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
final var head = ByteBuffer.allocate(byteCount);
|
||||
head.order(ByteOrder.LITTLE_ENDIAN);
|
||||
// See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
|
||||
head.put(new byte[]{
|
||||
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
|
||||
0x00, 0x00, 0x00, 0x00, // vendor (aka. Encoder) string of length 0
|
||||
@ -514,18 +595,19 @@ public class OggFromWebMWriter implements Closeable {
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(final int size) {
|
||||
if (size > 65025) {
|
||||
throw new UnsupportedOperationException(
|
||||
String.format("page size is %s but cannot be larger than 65025", size));
|
||||
if (size > OPUS_MAX_PACKETS_PAGE_SIZE) {
|
||||
throw new UnsupportedOperationException(String.format(
|
||||
"page size is %s but cannot be larger than %s",
|
||||
size, OPUS_MAX_PACKETS_PAGE_SIZE));
|
||||
}
|
||||
|
||||
int available = (segmentTable.length - segmentTableSize) * 255;
|
||||
final boolean extra = (size % 255) == 0;
|
||||
int available = (segmentTable.length - segmentTableSize) * OGG_SEGMENT_SIZE;
|
||||
final boolean extra = (size % OGG_SEGMENT_SIZE) == 0;
|
||||
|
||||
if (extra) {
|
||||
// add a zero byte entry in the table
|
||||
// required to indicate the sample size is multiple of 255
|
||||
available -= 255;
|
||||
// required to indicate the sample size is multiple of OGG_SEGMENT_SIZE
|
||||
available -= OGG_SEGMENT_SIZE;
|
||||
}
|
||||
|
||||
// check if possible add the segment, without overflow the table
|
||||
@ -533,8 +615,8 @@ public class OggFromWebMWriter implements Closeable {
|
||||
return false; // not enough space on the page
|
||||
}
|
||||
|
||||
for (int seg = size; seg > 0; seg -= 255) {
|
||||
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
|
||||
for (int seg = size; seg > 0; seg -= OGG_SEGMENT_SIZE) {
|
||||
segmentTable[segmentTableSize++] = (byte) Math.min(seg, OGG_SEGMENT_SIZE);
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
|
||||
@ -4,6 +4,7 @@ import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
@ -33,7 +34,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";
|
||||
|
||||
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args,
|
||||
StreamInfo streamInfo) {
|
||||
@NonNull StreamInfo streamInfo) {
|
||||
Postprocessing instance;
|
||||
|
||||
switch (algorithmName) {
|
||||
@ -80,7 +81,18 @@ public abstract class Postprocessing implements Serializable {
|
||||
private final String name;
|
||||
|
||||
private String[] args;
|
||||
|
||||
/**
|
||||
* StreamInfo object related to the current download
|
||||
*/
|
||||
@NonNull
|
||||
protected StreamInfo streamInfo;
|
||||
|
||||
/**
|
||||
* The thumbnail / cover art bitmap associated with the current download.
|
||||
* May be null.
|
||||
*/
|
||||
@Nullable
|
||||
protected Bitmap thumbnail;
|
||||
|
||||
private transient DownloadMission mission;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user