From cb71440619b0f5742bb70cc15bfbe4cf9333be2e Mon Sep 17 00:00:00 2001 From: tobigr Date: Fri, 9 Jan 2026 03:02:24 +0100 Subject: [PATCH] Add documentation and replace magic numbers by constants --- .../newpipe/streams/Mp4FromDashWriter.java | 61 ++++--- .../newpipe/streams/OggFromWebMWriter.java | 160 +++++++++++++----- .../giga/postprocessing/Postprocessing.java | 14 +- 3 files changed, 171 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index fd394c47e..87703e218 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -21,11 +21,13 @@ import java.util.ArrayList; /** * MP4 muxer that builds a standard MP4 file from DASH fragmented MP4 sources. * - * @author kapodamy + *

+ * See + * https://atomicparsley.sourceforge.net/mpeg-4files.html for information on + * the MP4 file format and its specification. + *

* - * @implNote See - * https://atomicparsley.sourceforge.net/mpeg-4files.html 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. + * + *

+ * What does it do? + *
+ * 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) + *

+ * + * @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. + *

+ * 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. + *

*
      *     [size][key] [data_box]
      *     data_box = [size]["data"][type(4bytes)][locale(4bytes)=0][payload]
diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java
index 1b597a711..e2ee364cc 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java
@@ -29,18 +29,64 @@ import java.util.List;
 import java.util.stream.Collectors;
 
 /**
+ * 

+ * This class is used to convert a WebM stream containing Opus or Vorbis audio + * into an Ogg stream. + *

+ * + *

+ * The following specifications are used for the implementation: + *

+ * + * * @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 + * RFC7845 6. Packet Size Limits + */ + 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 + * RFC7845 5.2. Comment Header for OPUS metadata header format + * @see + * Vorbis I 4.2. Header decode and decode setup and + * + * Vorbis 5. comment field and header specification + * 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. * *

- * 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. *

* - * @param bitmap The bitmap to use as cover art - * @return The key-value pair representing the tag + * @see + * RFC 9639 8.8 Picture + * + * @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 makeOpusPictureTag(final Bitmap bitmap) { + private static Pair 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 + * RFC7845 5.2 + * * @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) { diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index c6161f19e..af43896f7 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -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;