2828import java .util .List ;
2929import java .util .stream .Collectors ;
3030
31+ import us .shandian .giga .postprocessing .ImageUtils ;
32+
3133/**
3234 * <p>
3335 * This class is used to convert a WebM stream containing Opus or Vorbis audio
5052 * @author tobigr
5153 */
5254public class OggFromWebMWriter implements Closeable {
55+ private static final String TAG = OggFromWebMWriter .class .getSimpleName ();
56+
57+ /**
58+ * No flags set.
59+ */
5360 private static final byte FLAG_UNSET = 0x00 ;
54- //private static final byte FLAG_CONTINUED = 0x01;
61+ /**
62+ * The packet is continued from previous the previous page.
63+ */
64+ private static final byte FLAG_CONTINUED = 0x01 ;
65+ /**
66+ * BOS (beginning of stream).
67+ */
5568 private static final byte FLAG_FIRST = 0x02 ;
69+ /**
70+ * EOS (end of stream).
71+ */
5672 private static final byte FLAG_LAST = 0x04 ;
5773
5874 private static final byte HEADER_CHECKSUM_OFFSET = 22 ;
5975 private static final byte HEADER_SIZE = 27 ;
6076
6177 private static final int TIME_SCALE_NS = 1000000000 ;
6278
79+ private static final int MAX_SEGMENT_SIZE = 255 ;
80+
81+ private static final int OPUS_MAX_PACKETS_SIZE = 61_140 ;
82+
83+ /**
84+ * <p>The maximum size of the compressed thumbnail image in bytes,
85+ * to be included in the Opus metadata.</p>
86+ *
87+ * This is a safe size to avoid creating metadata tags that are too large for the Ogg page,
88+ * since the metadata header and other tags can also take up space in the page.
89+ */
90+ private static final int MAX_THUMBNAIL_SIZE = OPUS_MAX_PACKETS_SIZE - 4500 ;
91+
92+
6393 private boolean done = false ;
6494 private boolean parsed = false ;
6595
@@ -323,12 +353,12 @@ private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffe
323353 * @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
324354 * RFC7845 5.2</a>
325355 *
326- * @return
356+ * @return The binary metadata header, or null if not implemented for the codec
327357 */
328358 @ Nullable
329359 private byte [] makeMetadata () {
330360 if (DEBUG ) {
331- Log .d ("OggFromWebMWriter" , "Downloading media with codec ID " + webmTrack .codecId );
361+ Log .d (TAG , "Downloading media with codec ID " + webmTrack .codecId );
332362 }
333363
334364 if ("A_OPUS" .equals (webmTrack .codecId )) {
@@ -343,18 +373,21 @@ private byte[] makeMetadata() {
343373 .getLocalDateTime ()
344374 .format (DateTimeFormatter .ISO_DATE )));
345375 if (thumbnail != null ) {
346- metadata .add (makeOpusPictureTag (thumbnail ));
376+ var pictureTag = makeOpusPictureTag (thumbnail , MAX_THUMBNAIL_SIZE );
377+ if (pictureTag != null ) {
378+ metadata .add (pictureTag );
379+ }
347380 }
348381 }
349382
350383 if (DEBUG ) {
351- Log .d ("OggFromWebMWriter" , "Creating metadata header with this data:" );
352- metadata .forEach (p -> Log .d ("OggFromWebMWriter" , p .first + "=" + p .second ));
384+ Log .d (TAG , "Creating metadata header with this data:" );
385+ metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
353386 }
354387
355388 return makeOpusTagsHeader (metadata );
356389 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
357- /**
390+ /*
358391 * See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
359392 * RFC7845 5.2</a>
360393 */
@@ -399,43 +432,68 @@ private static byte[] makeOpusMetadataTag(final Pair<String, String> pair) {
399432 * </p>
400433 *
401434 * @param bitmap The bitmap to use as cover art
435+ * @param maxSize The maximum size of the compressed image in bytes.
436+ * If the compressed image exceeds this size,
437+ * it will be further compressed until it fits.
438+ * This is necessary to avoid creating metadata tags
439+ * that are too large for the Ogg page.
402440 * @return The key-value pair representing the tag
441+ * or null if the image cannot be compressed to the maxSize
403442 */
404- private static Pair <String , String > makeOpusPictureTag (final Bitmap bitmap ) {
443+ @ Nullable
444+ private static Pair <String , String > makeOpusPictureTag (final Bitmap bitmap , final int maxSize ) {
405445 // FLAC picture block format (big-endian):
406446 // uint32 picture_type
407- // uint32 mime_length, mime_string
408- // uint32 desc_length, desc_string
447+ // uint32 mime_length,
448+ // mime_string
449+ // uint32 desc_length,
450+ // desc_string
409451 // uint32 width
410452 // uint32 height
411453 // uint32 color_depth
412454 // uint32 colors_indexed
413- // uint32 data_length, data_bytes
455+ // uint32 data_length,
456+ // data_bytes
414457
415458 final ByteArrayOutputStream baos = new ByteArrayOutputStream ();
416- bitmap .compress (Bitmap .CompressFormat .JPEG , 100 , baos );
459+
460+ final var compressedThumbnail = ImageUtils .INSTANCE .compressToSize (bitmap , maxSize );
461+ if (compressedThumbnail == null ) {
462+ if (DEBUG ) {
463+ Log .d (TAG , "failed to compress thumbnail to target size " + maxSize );
464+ }
465+ return null ;
466+ }
467+ compressedThumbnail .getBitmap ().compress (
468+ Bitmap .CompressFormat .JPEG , compressedThumbnail .getQuality (), baos );
417469
418470 final byte [] imageData = baos .toByteArray ();
419471 final byte [] mimeBytes = "image/jpeg" .getBytes (StandardCharsets .UTF_8 );
420472 final byte [] descBytes = new byte [0 ]; // optional description
421473 // fixed ints + mime + desc
422474 final int headerSize = 4 * 8 + mimeBytes .length + descBytes .length ;
423475 final ByteBuffer buf = ByteBuffer .allocate (headerSize + imageData .length );
424- buf .putInt (3 ); // picture type: 3 = Cover (front)
476+ // See https://id3.org/id3v2.3.0#Attached_picture for a full list of picture types
477+ // TODO: allow specifying other picture types, i.e. cover (front) for music albums;
478+ // but this info needs to be provided by the extractor first.
479+ buf .putInt (3 ); // picture type: 0 = Other
425480 buf .putInt (mimeBytes .length );
426481 buf .put (mimeBytes );
427482 buf .putInt (descBytes .length );
428- // no description
429483 if (descBytes .length > 0 ) {
484+ // currently no description available, might be added later.
430485 buf .put (descBytes );
431486 }
432- buf .putInt (bitmap . getWidth ()); // width (unknown)
433- buf .putInt (bitmap . getHeight ()); // height (unknown)
487+ buf .putInt (compressedThumbnail . getBitmap (). getWidth ());
488+ buf .putInt (compressedThumbnail . getBitmap (). getHeight ());
434489 buf .putInt (0 ); // color depth
435490 buf .putInt (0 ); // colors indexed
436491 buf .putInt (imageData .length );
437492 buf .put (imageData );
493+
438494 final String b64 = Base64 .getEncoder ().encodeToString (buf .array ());
495+ Log .d (TAG , "Compressed thumbnail size: " + imageData .length
496+ + " bytes, base64 metadata size: " + b64 .length () + " characters" );
439497 return Pair .create ("METADATA_BLOCK_PICTURE" , b64 );
440498 }
441499
@@ -457,7 +515,7 @@ private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyVal
457515 .stream ()
458516 .filter (p -> !p .second .isBlank ())
459517 .map (OggFromWebMWriter ::makeOpusMetadataTag )
460- .collect ( Collectors . toUnmodifiableList () );
518+ .toList ( );
461519
462520 final var tagsBytes = tags .stream ().collect (Collectors .summingInt (arr -> arr .length ));
463521
0 commit comments