22
33import static org .schabi .newpipe .MainActivity .DEBUG ;
44
5+ import android .graphics .Bitmap ;
56import android .util .Log ;
67import android .util .Pair ;
78
1516import org .schabi .newpipe .streams .WebMReader .WebMTrack ;
1617import org .schabi .newpipe .streams .io .SharpStream ;
1718
19+ import java .io .ByteArrayOutputStream ;
1820import java .io .Closeable ;
1921import java .io .IOException ;
2022import java .nio .ByteBuffer ;
2123import java .nio .ByteOrder ;
24+ import java .nio .charset .StandardCharsets ;
2225import java .time .format .DateTimeFormatter ;
2326import java .util .ArrayList ;
27+ import java .util .Base64 ;
2428import java .util .List ;
2529import java .util .stream .Collectors ;
2630
@@ -63,9 +67,19 @@ public class OggFromWebMWriter implements Closeable {
6367
6468 private final int [] crc32Table = new int [256 ];
6569 private final StreamInfo streamInfo ;
70+ private final Bitmap thumbnail ;
6671
67- public OggFromWebMWriter (@ NonNull final SharpStream source , @ NonNull final SharpStream target ,
68- @ Nullable final StreamInfo streamInfo ) {
72+ /**
73+ * Constructor of OggFromWebMWriter.
74+ * @param source
75+ * @param target
76+ * @param streamInfo the stream info
77+ * @param thumbnail the thumbnail bitmap used as cover art
78+ */
79+ public OggFromWebMWriter (@ NonNull final SharpStream source ,
80+ @ NonNull final SharpStream target ,
81+ @ Nullable final StreamInfo streamInfo ,
82+ @ Nullable final Bitmap thumbnail ) {
6983 if (!source .canRead () || !source .canRewind ()) {
7084 throw new IllegalArgumentException ("source stream must be readable and allows seeking" );
7185 }
@@ -76,6 +90,7 @@ public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final Sharp
7690 this .source = source ;
7791 this .output = target ;
7892 this .streamInfo = streamInfo ;
93+ this .thumbnail = thumbnail ;
7994
8095 this .streamId = (int ) System .currentTimeMillis ();
8196
@@ -299,6 +314,14 @@ private byte[] makeMetadata() {
299314 .getUploadDate ()
300315 .getLocalDateTime ()
301316 .format (DateTimeFormatter .ISO_DATE )));
317+ if (thumbnail != null ) {
318+
319+ metadata .add (makeOpusPictureTag (thumbnail ));
320+ // Alternative approach, but less standard:
321+ // metadata.add(Pair.create("COVERART", Base64.getEncoder()
322+ // .encodeToString(thumb)));
323+ // metadata.add(Pair.create("COVERARTMIME", "image/jpeg"));
324+ }
302325 }
303326
304327 if (DEBUG ) {
@@ -309,7 +332,7 @@ private byte[] makeMetadata() {
309332 return makeOpusTagsHeader (metadata );
310333 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
311334 return new byte []{
312- 0x03 , // ¿¿¿ ???
335+ 0x03 , // ???
313336 0x76 , 0x6f , 0x72 , 0x62 , 0x69 , 0x73 , // "vorbis" binary string
314337 0x00 , 0x00 , 0x00 , 0x00 , // writing application string size (not present)
315338 0x00 , 0x00 , 0x00 , 0x00 // additional tags count (zero means no tags)
@@ -339,6 +362,47 @@ private static byte[] makeOpusMetadataTag(final Pair<String, String> pair) {
339362 return buf .array ();
340363 }
341364
365+ private static Pair <String , String > makeOpusPictureTag (final Bitmap bitmap ) {
366+ // FLAC picture block format (big-endian):
367+ // uint32 picture_type
368+ // uint32 mime_length, mime_string
369+ // uint32 desc_length, desc_string
370+ // uint32 width
371+ // uint32 height
372+ // uint32 color_depth
373+ // uint32 colors_indexed
374+ // uint32 data_length, data_bytes
375+
376+ final ByteArrayOutputStream baos = new ByteArrayOutputStream ();
377+ bitmap .compress (Bitmap .CompressFormat .JPEG , 100 , baos );
378+ bitmap .recycle ();
379+
380+ final byte [] imageData = baos .toByteArray ();
381+ final byte [] mimeBytes = "image/jpeg" .getBytes (StandardCharsets .UTF_8 );
382+ final byte [] descBytes = new byte [0 ]; // optional description
383+ // fixed ints + mime + desc
384+ final int headerSize = 4 * 8 + mimeBytes .length + descBytes .length ;
385+ final ByteBuffer buf = ByteBuffer .allocate (headerSize + imageData .length );
386+ buf .putInt (3 ); // picture type: 3 = Cover (front)
387+ buf .putInt (mimeBytes .length );
388+ buf .put (mimeBytes );
389+ buf .putInt (descBytes .length );
390+ // no description
391+ if (descBytes .length > 0 ) {
392+ buf .put (descBytes );
393+ }
394+ buf .putInt (bitmap .getWidth ()); // width (unknown)
395+ buf .putInt (bitmap .getHeight ()); // height (unknown)
396+ buf .putInt (0 ); // color depth
397+ buf .putInt (0 ); // colors indexed
398+ buf .putInt (imageData .length );
399+ buf .put (imageData );
400+ final String b64 = Base64 .getEncoder ().encodeToString (buf .array ());
401+ // Many apps also accept COVERART / COVERARTMIME,
402+ // but METADATA_BLOCK_PICTURE is the standard approach
403+ return Pair .create ("METADATA_BLOCK_PICTURE" , b64 );
404+ }
405+
342406 /**
343407 * This returns a complete "OpusTags" header, created from the provided metadata tags.
344408 * <p>
0 commit comments