|
27 | 27 | import java.util.Base64; |
28 | 28 | import java.util.List; |
29 | 29 | import java.util.stream.Collectors; |
| 30 | +import java.util.Arrays; |
30 | 31 |
|
31 | 32 | /** |
32 | 33 | * <p> |
@@ -68,7 +69,7 @@ public class OggFromWebMWriter implements Closeable { |
68 | 69 | /** |
69 | 70 | * EOS (end of stream). |
70 | 71 | */ |
71 | | - private static final byte FLAG_LAST = 0x04;; |
| 72 | + private static final byte FLAG_LAST = 0x04; |
72 | 73 |
|
73 | 74 | private static final byte HEADER_CHECKSUM_OFFSET = 22; |
74 | 75 | private static final byte HEADER_SIZE = 27; |
@@ -257,10 +258,7 @@ public void build() throws IOException { |
257 | 258 | /* step 3: create packet with metadata */ |
258 | 259 | final byte[] buffer = makeCommentHeader(); |
259 | 260 | if (buffer != null) { |
260 | | - addPacketSegment(buffer.length); |
261 | | - makePacketHeader(0x00, header, buffer); |
262 | | - write(header); |
263 | | - output.write(buffer); |
| 261 | + addPacketSegmentMultiPage(buffer, header); |
264 | 262 | } |
265 | 263 |
|
266 | 264 | /* step 4: calculate amount of packets */ |
@@ -626,6 +624,102 @@ private boolean addPacketSegment(final int size) { |
626 | 624 | return true; |
627 | 625 | } |
628 | 626 |
|
| 627 | + /** |
| 628 | + * Like {@link #addPacketSegment(SimpleBlock)} for large metadata blobs |
| 629 | + * splits the provided data into multiple pages if necessary |
| 630 | + * and writes them immediately (header + data). |
| 631 | + * This method is intended to be used only for metadata (e.g. large thumbnails). |
| 632 | + * |
| 633 | + * @param data the metadata to add as a packet segment |
| 634 | + * @param header a reusable ByteBuffer for writing page headers; this method will write |
| 635 | + * the header for each page as needed |
| 636 | + */ |
| 637 | + private void addPacketSegmentMultiPage(@NonNull final byte[] data, |
| 638 | + @NonNull final ByteBuffer header) throws IOException { |
| 639 | + int offset = 0; |
| 640 | + boolean first = true; |
| 641 | + |
| 642 | + while (offset < data.length) { |
| 643 | + final int remaining = data.length - offset; |
| 644 | + final boolean finalChunkCandidate = remaining <= OPUS_MAX_PACKETS_PAGE_SIZE; |
| 645 | + final int chunkSize; |
| 646 | + if (finalChunkCandidate) { |
| 647 | + chunkSize = remaining; // final chunk can be any size |
| 648 | + } else { |
| 649 | + // For intermediate (non-final) chunks, make the chunk size a multiple |
| 650 | + // of OGG_SEGMENT_SIZE so that the last lacing value is 255 and the |
| 651 | + // decoder won't treat the packet as finished on that page. |
| 652 | + final int maxFullSegments = OPUS_MAX_PACKETS_PAGE_SIZE / OGG_SEGMENT_SIZE; |
| 653 | + chunkSize = maxFullSegments * OGG_SEGMENT_SIZE; |
| 654 | + } |
| 655 | + |
| 656 | + final boolean isFinalChunk = (offset + chunkSize) >= data.length; |
| 657 | + |
| 658 | + // We must reserve appropriate number of lacing values in the segment table. |
| 659 | + // For chunks that are exact multiples of OGG_SEGMENT_SIZE and are the final |
| 660 | + // chunk of the packet, a trailing 0 lacing entry is required to indicate |
| 661 | + // the packet ends exactly on a segment boundary. For intermediate chunks |
| 662 | + // (continued across pages) we MUST NOT write that trailing 0 because then |
| 663 | + // the packet would appear complete on that page. Instead intermediate |
| 664 | + // chunks should end with only 255-valued lacing entries (no trailing 0). |
| 665 | + final int fullSegments = chunkSize / OGG_SEGMENT_SIZE; // may be 0 |
| 666 | + final int lastSegSize = chunkSize % OGG_SEGMENT_SIZE; // 0..254 |
| 667 | + final boolean chunkIsMultiple = (lastSegSize == 0); |
| 668 | + |
| 669 | + int requiredEntries = fullSegments + (lastSegSize > 0 ? 1 : 0); |
| 670 | + if (chunkIsMultiple && isFinalChunk) { |
| 671 | + // need an extra zero entry to mark packet end |
| 672 | + requiredEntries += 1; |
| 673 | + } |
| 674 | + |
| 675 | + // If the segment table doesn't have enough room, flush the current page |
| 676 | + // by writing a header without immediate data. This clears the segment table. |
| 677 | + if (requiredEntries > (segmentTable.length - segmentTableSize)) { |
| 678 | + // flush current page |
| 679 | + int checksum = makePacketHeader(0x00, header, null); |
| 680 | + checksum = calcCrc32(checksum, new byte[0], 0); |
| 681 | + header.putInt(HEADER_CHECKSUM_OFFSET, checksum); |
| 682 | + write(header); |
| 683 | + } |
| 684 | + |
| 685 | + // After ensuring space, if still not enough (edge case), throw |
| 686 | + if (requiredEntries > (segmentTable.length - segmentTableSize)) { |
| 687 | + throw new IOException("Unable to reserve segment table entries for metadata chunk"); |
| 688 | + } |
| 689 | + |
| 690 | + // Fill the segment table entries for this chunk. For intermediate chunks |
| 691 | + // that are an exact multiple of OGG_SEGMENT_SIZE we must NOT append a |
| 692 | + // trailing zero entry (that would incorrectly signal packet end). |
| 693 | + final int remainingToAssign = chunkSize; |
| 694 | + for (int seg = remainingToAssign; seg > 0; seg -= OGG_SEGMENT_SIZE) { |
| 695 | + segmentTable[segmentTableSize++] = (byte) Math.min(seg, OGG_SEGMENT_SIZE); |
| 696 | + } |
| 697 | + |
| 698 | + if (chunkIsMultiple && isFinalChunk) { |
| 699 | + // Only append the zero terminator for a final chunk that has an exact |
| 700 | + // multiple of OGG_SEGMENT_SIZE bytes. |
| 701 | + segmentTable[segmentTableSize++] = 0x00; |
| 702 | + } |
| 703 | + |
| 704 | + // For continuation pages (after the first), mark the page as continued. |
| 705 | + if (!first) { |
| 706 | + packetFlag = FLAG_CONTINUED; |
| 707 | + } |
| 708 | + |
| 709 | + final byte[] chunk = Arrays.copyOfRange(data, offset, offset + chunkSize); |
| 710 | + |
| 711 | + // Now create header (which will consume and clear the segment table) and write |
| 712 | + // header + chunk data. makePacketHeader will compute checksum including chunk |
| 713 | + // when an immediatePage is provided. |
| 714 | + makePacketHeader(0x00, header, chunk); |
| 715 | + write(header); |
| 716 | + output.write(chunk); |
| 717 | + |
| 718 | + offset += chunkSize; |
| 719 | + first = false; |
| 720 | + } |
| 721 | + } |
| 722 | + |
629 | 723 | private void populateCrc32Table() { |
630 | 724 | for (int i = 0; i < 0x100; i++) { |
631 | 725 | int crc = i << 24; |
|
0 commit comments