99import androidx .annotation .NonNull ;
1010import androidx .annotation .Nullable ;
1111
12+ import org .schabi .newpipe .extractor .stream .SongMetadata ;
1213import org .schabi .newpipe .extractor .stream .StreamInfo ;
1314import org .schabi .newpipe .streams .WebMReader .Cluster ;
1415import org .schabi .newpipe .streams .WebMReader .Segment ;
1516import org .schabi .newpipe .streams .WebMReader .SimpleBlock ;
1617import org .schabi .newpipe .streams .WebMReader .WebMTrack ;
1718import org .schabi .newpipe .streams .io .SharpStream ;
19+ import org .schabi .newpipe .util .StreamInfoMetadataHelper ;
1820
1921import java .io .ByteArrayOutputStream ;
2022import java .io .Closeable ;
4042 * </p>
4143 * <ul>
4244 * <li>FLAC: <a href="https://www.rfc-editor.org/rfc/rfc9639">RFC 9639</a></li>
45+ * <li>
46+ * Vorbis: <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html">Vorbis I</a>.
47+ * <br>
48+ * Vorbis uses FLAC picture blocks for embedding cover art in the metadata.
49+ * </li>
4350 * <li>Opus: All specs can be found at <a href="https://opus-codec.org/docs/">
4451 * https://opus-codec.org/docs/</a>.
4552 * <a href="https://datatracker.ietf.org/doc/html/rfc7845.html">RFC7845</a>
4653 * defines the Ogg encapsulation for Opus streams, i.e.the container format and metadata.
54+ * <br>
55+ * Opus uses multiple Vorbis I features, e.g. the comment header format for metadata.
4756 * </li>
48- * <li>Vorbis: <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html">Vorbis I</a></li>
4957 * </ul>
5058 *
5159 * @author kapodamy
@@ -349,8 +357,8 @@ private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffe
349357 * @see <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2">
350358 * Vorbis I 4.2. Header decode and decode setup</a> and
351359 * <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-820005">
352- * Vorbis 5. comment field and header specification</a>
353- * for VORBIS metadata header format
360+ * Vorbis I 5. comment field and header specification</a>
361+ * for VORBIS metadata header format. Vorbis I 5. lists all the possible metadata tags.
354362 *
355363 * @return the metadata header as a byte array, or null if the codec is not supported
356364 * for metadata generation
@@ -361,38 +369,58 @@ private byte[] makeCommentHeader() {
361369 Log .d (TAG , "Downloading media with codec ID " + webmTrack .codecId );
362370 }
363371
364- if ("A_OPUS" .equals (webmTrack .codecId )) {
365- final var metadata = new ArrayList <Pair <String , String >>();
366- if (streamInfo != null ) {
367- metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
368- metadata .add (Pair .create ("GENRE" , streamInfo .getCategory ()));
369- metadata .add (Pair .create ("ARTIST" , streamInfo .getUploaderName ()));
370- metadata .add (Pair .create ("TITLE" , streamInfo .getName ()));
371- metadata .add (Pair .create ("DATE" , streamInfo
372- .getUploadDate ()
373- .getLocalDateTime ()
374- .format (DateTimeFormatter .ISO_DATE )));
375- if (thumbnail != null ) {
376- metadata .add (makeFlacPictureTag (thumbnail ));
377- }
372+ final var metadata = new ArrayList <Pair <String , String >>();
373+ if (streamInfo != null ) {
374+ final SongMetadata songMetadata = streamInfo .getSongMetadata ();
375+ final StreamInfoMetadataHelper metadHelper = new StreamInfoMetadataHelper (streamInfo );
376+ // metadata that can be present in the stream info and the song metadata.
377+ // Use the song metadata if available, otherwise fallback to stream info.
378+ metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
379+ metadata .add (Pair .create ("GENRE" , metadHelper .getGenre ()));
380+ metadata .add (Pair .create ("ARTIST" , metadHelper .getArtist ()));
381+ metadata .add (Pair .create ("TITLE" , metadHelper .getTitle ()));
382+ metadata .add (Pair .create ("DATE" , metadHelper .getReleaseDate ()
383+ .getLocalDateTime ()
384+ .format (DateTimeFormatter .ISO_DATE )));
385+ // Additional metadata that is only present in the song metadata
386+ if (songMetadata != null ) {
387+ metadata .add (Pair .create ("ALBUM" , songMetadata .album ));
388+ if (songMetadata .track != SongMetadata .TRACK_UNKNOWN ) {
389+ // TRACKNUMBER is suggested in Vorbis spec,
390+ // but TRACK is more commonly used in practice
391+ metadata .add (Pair .create ("TRACKNUMBER" , String .valueOf (songMetadata .track )));
392+ metadata .add (Pair .create ("TRACK" , String .valueOf (songMetadata .track )));
393+ }
394+ metadata .add (Pair .create ("PERFORMER" , String .join (", " , songMetadata .performer )));
395+ metadata .add (Pair .create ("ORGANIZATION" , songMetadata .label ));
396+ metadata .add (Pair .create ("COPYRIGHT" , songMetadata .copyright ));
378397 }
379-
380- if (DEBUG ) {
381- Log .d (TAG , "Creating metadata header with this data:" );
382- metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
398+ // Add thumbnail as cover art at the end because it is the largest metadata entry
399+ if (thumbnail != null ) {
400+ metadata .add (makeFlacPictureTag (thumbnail ));
383401 }
402+ }
403+
404+ if (DEBUG ) {
405+ Log .d (TAG , "Creating metadata header with this data:" );
406+ metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
407+ }
384408
385- return makeOpusTagsHeader (metadata );
409+ if ("A_OPUS" .equals (webmTrack .codecId )) {
410+ // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
411+ final byte [] identificationHeader = new byte []{
412+ 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
413+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
414+ };
415+ return makeCommentHeader (metadata , identificationHeader );
386416 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
387417 // See https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
388- // for the Vorbis comment header format
389- // TODO: add Vorbis metadata: same as Opus, but with the Vorbis comment header format
390- return new byte []{
418+ final byte [] identificationHeader = new byte []{
391419 0x03 , // packet type for Vorbis comment header
392420 0x76 , 0x6f , 0x72 , 0x62 , 0x69 , 0x73 , // "vorbis" binary string
393- 0x00 , 0x00 , 0x00 , 0x00 , // writing application string size (not present)
394- 0x00 , 0x00 , 0x00 , 0x00 // additional tags count (zero means no tags)
421+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
395422 };
423+ return makeCommentHeader (metadata , identificationHeader );
396424 }
397425
398426 // not implemented for the desired codec
@@ -402,12 +430,12 @@ private byte[] makeCommentHeader() {
402430 /**
403431 * This creates a single metadata tag for use in opus metadata headers. It contains the four
404432 * byte string length field and includes the string as-is. This cannot be used independently,
405- * but must follow a proper "OpusTags" header.
433+ * but must follow a proper Comment header.
406434 *
407435 * @param pair A key-value pair in the format "KEY=some value"
408436 * @return The binary data of the encoded metadata tag
409437 */
410- private static byte [] makeOpusMetadataTag (final Pair <String , String > pair ) {
438+ private static byte [] makeVorbisMetadataTag (final Pair <String , String > pair ) {
411439 final var keyValue = pair .first .toUpperCase () + "=" + pair .second .trim ();
412440
413441 final var bytes = keyValue .getBytes ();
@@ -483,24 +511,21 @@ private static Pair<String, String> makeFlacPictureTag(final Bitmap bitmap) {
483511 }
484512
485513 /**
486- * This returns a complete "OpusTags" header, created from the provided metadata tags.
487- * <p>
488- * You probably want to use makeOpusMetadata(), which uses this function to create
489- * a header with sensible metadata filled in.
490- *
491- * @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
492- * RFC7845 5.2</a>
514+ * This returns a complete Comment header, created from the provided metadata tags.
493515 *
494516 * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping
495517 * from one key to multiple values.
518+ * @param identificationHeader the identification header for the codec,
519+ * which is required to be prefixed to the comment header.
496520 * @return The binary header
497521 */
498- private static byte [] makeOpusTagsHeader (final List <Pair <String , String >> keyValueLines ) {
522+ private static byte [] makeCommentHeader (final List <Pair <String , String >> keyValueLines ,
523+ final byte [] identificationHeader ) {
499524 final var tags = keyValueLines
500525 .stream ()
501- .filter (p -> !p .second .isBlank ())
502- .map (OggFromWebMWriter ::makeOpusMetadataTag )
503- .collect ( Collectors . toUnmodifiableList () );
526+ .filter (p -> p . second != null && !p .second .isBlank ())
527+ .map (OggFromWebMWriter ::makeVorbisMetadataTag )
528+ .toList ( );
504529
505530 final var tagsBytes = tags .stream ().collect (Collectors .summingInt (arr -> arr .length ));
506531
@@ -509,11 +534,7 @@ private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyVal
509534
510535 final var head = ByteBuffer .allocate (byteCount );
511536 head .order (ByteOrder .LITTLE_ENDIAN );
512- // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
513- head .put (new byte []{
514- 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
515- 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
516- });
537+ head .put (identificationHeader );
517538 head .putInt (tags .size ()); // 4 bytes for tag count
518539 tags .forEach (head ::put ); // dynamic amount of tag bytes
519540
0 commit comments