From ec6243d7ee539f3a8c5aca9c3d26213d225d6f15 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 6 Feb 2026 21:56:18 +0100 Subject: [PATCH] Span metadata about multiple pages if needed --- .../newpipe/streams/OggFromWebMWriter.java | 104 +++++++++++++++++- 1 file changed, 99 insertions(+), 5 deletions(-) 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 e2ee364cc..64105f68d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; +import java.util.Arrays; /** *

@@ -68,7 +69,7 @@ public class OggFromWebMWriter implements Closeable { /** * EOS (end of stream). */ - private static final byte FLAG_LAST = 0x04;; + private static final byte FLAG_LAST = 0x04; private static final byte HEADER_CHECKSUM_OFFSET = 22; private static final byte HEADER_SIZE = 27; @@ -257,10 +258,7 @@ public class OggFromWebMWriter implements Closeable { /* step 3: create packet with metadata */ final byte[] buffer = makeCommentHeader(); if (buffer != null) { - addPacketSegment(buffer.length); - makePacketHeader(0x00, header, buffer); - write(header); - output.write(buffer); + addPacketSegmentMultiPage(buffer, header); } /* step 4: calculate amount of packets */ @@ -626,6 +624,102 @@ public class OggFromWebMWriter implements Closeable { return true; } + /** + * Like {@link #addPacketSegment(SimpleBlock)} for large metadata blobs + * splits the provided data into multiple pages if necessary + * and writes them immediately (header + data). + * This method is intended to be used only for metadata (e.g. large thumbnails). + * + * @param data the metadata to add as a packet segment + * @param header a reusable ByteBuffer for writing page headers; this method will write + * the header for each page as needed + */ + private void addPacketSegmentMultiPage(@NonNull final byte[] data, + @NonNull final ByteBuffer header) throws IOException { + int offset = 0; + boolean first = true; + + while (offset < data.length) { + final int remaining = data.length - offset; + final boolean finalChunkCandidate = remaining <= OPUS_MAX_PACKETS_PAGE_SIZE; + final int chunkSize; + if (finalChunkCandidate) { + chunkSize = remaining; // final chunk can be any size + } else { + // For intermediate (non-final) chunks, make the chunk size a multiple + // of OGG_SEGMENT_SIZE so that the last lacing value is 255 and the + // decoder won't treat the packet as finished on that page. + final int maxFullSegments = OPUS_MAX_PACKETS_PAGE_SIZE / OGG_SEGMENT_SIZE; + chunkSize = maxFullSegments * OGG_SEGMENT_SIZE; + } + + final boolean isFinalChunk = (offset + chunkSize) >= data.length; + + // We must reserve appropriate number of lacing values in the segment table. + // For chunks that are exact multiples of OGG_SEGMENT_SIZE and are the final + // chunk of the packet, a trailing 0 lacing entry is required to indicate + // the packet ends exactly on a segment boundary. For intermediate chunks + // (continued across pages) we MUST NOT write that trailing 0 because then + // the packet would appear complete on that page. Instead intermediate + // chunks should end with only 255-valued lacing entries (no trailing 0). + final int fullSegments = chunkSize / OGG_SEGMENT_SIZE; // may be 0 + final int lastSegSize = chunkSize % OGG_SEGMENT_SIZE; // 0..254 + final boolean chunkIsMultiple = (lastSegSize == 0); + + int requiredEntries = fullSegments + (lastSegSize > 0 ? 1 : 0); + if (chunkIsMultiple && isFinalChunk) { + // need an extra zero entry to mark packet end + requiredEntries += 1; + } + + // If the segment table doesn't have enough room, flush the current page + // by writing a header without immediate data. This clears the segment table. + if (requiredEntries > (segmentTable.length - segmentTableSize)) { + // flush current page + int checksum = makePacketHeader(0x00, header, null); + checksum = calcCrc32(checksum, new byte[0], 0); + header.putInt(HEADER_CHECKSUM_OFFSET, checksum); + write(header); + } + + // After ensuring space, if still not enough (edge case), throw + if (requiredEntries > (segmentTable.length - segmentTableSize)) { + throw new IOException("Unable to reserve segment table entries for metadata chunk"); + } + + // Fill the segment table entries for this chunk. For intermediate chunks + // that are an exact multiple of OGG_SEGMENT_SIZE we must NOT append a + // trailing zero entry (that would incorrectly signal packet end). + final int remainingToAssign = chunkSize; + for (int seg = remainingToAssign; seg > 0; seg -= OGG_SEGMENT_SIZE) { + segmentTable[segmentTableSize++] = (byte) Math.min(seg, OGG_SEGMENT_SIZE); + } + + if (chunkIsMultiple && isFinalChunk) { + // Only append the zero terminator for a final chunk that has an exact + // multiple of OGG_SEGMENT_SIZE bytes. + segmentTable[segmentTableSize++] = 0x00; + } + + // For continuation pages (after the first), mark the page as continued. + if (!first) { + packetFlag = FLAG_CONTINUED; + } + + final byte[] chunk = Arrays.copyOfRange(data, offset, offset + chunkSize); + + // Now create header (which will consume and clear the segment table) and write + // header + chunk data. makePacketHeader will compute checksum including chunk + // when an immediatePage is provided. + makePacketHeader(0x00, header, chunk); + write(header); + output.write(chunk); + + offset += chunkSize; + first = false; + } + } + private void populateCrc32Table() { for (int i = 0; i < 0x100; i++) { int crc = i << 24;