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
@@ -352,8 +360,8 @@ private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffe
352360 * @see <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2">
353361 * Vorbis I 4.2. Header decode and decode setup</a> and
354362 * <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-820005">
355- * Vorbis 5. comment field and header specification</a>
356- * for VORBIS metadata header format
363+ * Vorbis I 5. comment field and header specification</a>
364+ * for VORBIS metadata header format. Vorbis I 5. lists all the possible metadata tags.
357365 *
358366 * @return the metadata header as a byte array, or null if the codec is not supported
359367 * for metadata generation
@@ -364,38 +372,58 @@ private byte[] makeCommentHeader() {
364372 Log .d (TAG , "Downloading media with codec ID " + webmTrack .codecId );
365373 }
366374
367- if ("A_OPUS" .equals (webmTrack .codecId )) {
368- final var metadata = new ArrayList <Pair <String , String >>();
369- if (streamInfo != null ) {
370- metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
371- metadata .add (Pair .create ("GENRE" , streamInfo .getCategory ()));
372- metadata .add (Pair .create ("ARTIST" , streamInfo .getUploaderName ()));
373- metadata .add (Pair .create ("TITLE" , streamInfo .getName ()));
374- metadata .add (Pair .create ("DATE" , streamInfo
375- .getUploadDate ()
376- .getLocalDateTime ()
377- .format (DateTimeFormatter .ISO_DATE )));
378- if (thumbnail != null ) {
379- metadata .add (makeFlacPictureTag (thumbnail ));
380- }
375+ final var metadata = new ArrayList <Pair <String , String >>();
376+ if (streamInfo != null ) {
377+ final SongMetadata songMetadata = streamInfo .getSongMetadata ();
378+ final StreamInfoMetadataHelper metadHelper = new StreamInfoMetadataHelper (streamInfo );
379+ // metadata that can be present in the stream info and the song metadata.
380+ // Use the song metadata if available, otherwise fallback to stream info.
381+ metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
382+ metadata .add (Pair .create ("GENRE" , metadHelper .getGenre ()));
383+ metadata .add (Pair .create ("ARTIST" , metadHelper .getArtist ()));
384+ metadata .add (Pair .create ("TITLE" , metadHelper .getTitle ()));
385+ metadata .add (Pair .create ("DATE" , metadHelper .getReleaseDate ()
386+ .getLocalDateTime ()
387+ .format (DateTimeFormatter .ISO_DATE )));
388+ // Additional metadata that is only present in the song metadata
389+ if (songMetadata != null ) {
390+ metadata .add (Pair .create ("ALBUM" , songMetadata .album ));
391+ if (songMetadata .track != SongMetadata .TRACK_UNKNOWN ) {
392+ // TRACKNUMBER is suggested in Vorbis spec,
393+ // but TRACK is more commonly used in practice
394+ metadata .add (Pair .create ("TRACKNUMBER" , String .valueOf (songMetadata .track )));
395+ metadata .add (Pair .create ("TRACK" , String .valueOf (songMetadata .track )));
396+ }
397+ metadata .add (Pair .create ("PERFORMER" , String .join (", " , songMetadata .performer )));
398+ metadata .add (Pair .create ("ORGANIZATION" , songMetadata .label ));
399+ metadata .add (Pair .create ("COPYRIGHT" , songMetadata .copyright ));
381400 }
382-
383- if (DEBUG ) {
384- Log .d (TAG , "Creating metadata header with this data:" );
385- metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
401+ // Add thumbnail as cover art at the end because it is the largest metadata entry
402+ if (thumbnail != null ) {
403+ metadata .add (makeFlacPictureTag (thumbnail ));
386404 }
405+ }
406+
407+ if (DEBUG ) {
408+ Log .d (TAG , "Creating metadata header with this data:" );
409+ metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
410+ }
387411
388- return makeOpusTagsHeader (metadata );
412+ if ("A_OPUS" .equals (webmTrack .codecId )) {
413+ // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
414+ final byte [] identificationHeader = new byte []{
415+ 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
416+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
417+ };
418+ return makeCommentHeader (metadata , identificationHeader );
389419 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
390420 // See https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
391- // for the Vorbis comment header format
392- // TODO: add Vorbis metadata: same as Opus, but with the Vorbis comment header format
393- return new byte []{
421+ final byte [] identificationHeader = new byte []{
394422 0x03 , // packet type for Vorbis comment header
395423 0x76 , 0x6f , 0x72 , 0x62 , 0x69 , 0x73 , // "vorbis" binary string
396- 0x00 , 0x00 , 0x00 , 0x00 , // writing application string size (not present)
397- 0x00 , 0x00 , 0x00 , 0x00 // additional tags count (zero means no tags)
424+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
398425 };
426+ return makeCommentHeader (metadata , identificationHeader );
399427 }
400428
401429 // not implemented for the desired codec
@@ -405,12 +433,12 @@ private byte[] makeCommentHeader() {
405433 /**
406434 * This creates a single metadata tag for use in opus metadata headers. It contains the four
407435 * byte string length field and includes the string as-is. This cannot be used independently,
408- * but must follow a proper "OpusTags" header.
436+ * but must follow a proper Comment header.
409437 *
410438 * @param pair A key-value pair in the format "KEY=some value"
411439 * @return The binary data of the encoded metadata tag
412440 */
413- private static byte [] makeOpusMetadataTag (final Pair <String , String > pair ) {
441+ private static byte [] makeVorbisMetadataTag (final Pair <String , String > pair ) {
414442 final var keyValue = pair .first .toUpperCase () + "=" + pair .second .trim ();
415443
416444 final var bytes = keyValue .getBytes ();
@@ -486,24 +514,21 @@ private static Pair<String, String> makeFlacPictureTag(final Bitmap bitmap) {
486514 }
487515
488516 /**
489- * This returns a complete "OpusTags" header, created from the provided metadata tags.
490- * <p>
491- * You probably want to use makeOpusMetadata(), which uses this function to create
492- * a header with sensible metadata filled in.
493- *
494- * @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
495- * RFC7845 5.2</a>
517+ * This returns a complete Comment header, created from the provided metadata tags.
496518 *
497519 * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping
498520 * from one key to multiple values.
521+ * @param identificationHeader the identification header for the codec,
522+ * which is required to be prefixed to the comment header.
499523 * @return The binary header
500524 */
501- private static byte [] makeOpusTagsHeader (final List <Pair <String , String >> keyValueLines ) {
525+ private static byte [] makeCommentHeader (final List <Pair <String , String >> keyValueLines ,
526+ final byte [] identificationHeader ) {
502527 final var tags = keyValueLines
503528 .stream ()
504- .filter (p -> !p .second .isBlank ())
505- .map (OggFromWebMWriter ::makeOpusMetadataTag )
506- .collect ( Collectors . toUnmodifiableList () );
529+ .filter (p -> p . second != null && !p .second .isBlank ())
530+ .map (OggFromWebMWriter ::makeVorbisMetadataTag )
531+ .toList ( );
507532
508533 final var tagsBytes = tags .stream ().collect (Collectors .summingInt (arr -> arr .length ));
509534
@@ -512,11 +537,7 @@ private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyVal
512537
513538 final var head = ByteBuffer .allocate (byteCount );
514539 head .order (ByteOrder .LITTLE_ENDIAN );
515- // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
516- head .put (new byte []{
517- 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
518- 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
519- });
540+ head .put (identificationHeader );
520541 head .putInt (tags .size ()); // 4 bytes for tag count
521542 tags .forEach (head ::put ); // dynamic amount of tag bytes
522543
0 commit comments