11package org .schabi .newpipe .streams ;
22
3+ import android .util .Log ;
4+
35import androidx .annotation .NonNull ;
46import androidx .annotation .Nullable ;
57
8+ import org .schabi .newpipe .BuildConfig ;
9+ import org .schabi .newpipe .extractor .stream .StreamInfo ;
610import org .schabi .newpipe .streams .WebMReader .Cluster ;
711import org .schabi .newpipe .streams .WebMReader .Segment ;
812import org .schabi .newpipe .streams .WebMReader .SimpleBlock ;
1418import java .nio .ByteBuffer ;
1519import java .nio .ByteOrder ;
1620import java .time .OffsetDateTime ;
21+ import java .time .format .DateTimeFormatter ;
22+ import java .util .stream .Collectors ;
1723
1824/**
1925 * @author kapodamy
@@ -53,8 +59,10 @@ public class OggFromWebMWriter implements Closeable {
5359 private long segmentTableNextTimestamp = TIME_SCALE_NS ;
5460
5561 private final int [] crc32Table = new int [256 ];
62+ private final StreamInfo streamInfo ;
5663
57- public OggFromWebMWriter (@ NonNull final SharpStream source , @ NonNull final SharpStream target ) {
64+ public OggFromWebMWriter (@ NonNull final SharpStream source , @ NonNull final SharpStream target ,
65+ @ Nullable final StreamInfo streamInfo ) {
5866 if (!source .canRead () || !source .canRewind ()) {
5967 throw new IllegalArgumentException ("source stream must be readable and allows seeking" );
6068 }
@@ -64,6 +72,7 @@ public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final Sharp
6472
6573 this .source = source ;
6674 this .output = target ;
75+ this .streamInfo = streamInfo ;
6776
6877 this .streamId = (int ) System .currentTimeMillis ();
6978
@@ -272,25 +281,29 @@ private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffe
272281
273282 @ Nullable
274283 private byte [] makeMetadata () {
284+ Log .d ("OggFromWebMWriter" , "Downloading media with codec ID " + webmTrack .codecId );
285+
275286 if ("A_OPUS" .equals (webmTrack .codecId )) {
276- final var commentFormat = "COMMENT=Downloaded using NewPipe on %s" ;
277- final var commentStr = String .format (commentFormat , OffsetDateTime .now ().toString ());
278- final var comment = commentStr .getBytes ();
279- final var head = ByteBuffer .allocate (20 + comment .length );
280- head .order (ByteOrder .LITTLE_ENDIAN );
281- head .put (new byte []{
282- // Byte order is LE, i.e. LSB first
283- 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
284- 0x00 , 0x00 , 0x00 , 0x00 , // vendor string of length 0
285- 0x01 , 0x00 , 0x00 , 0x00 , // additional tags count
286-
287- // + 4 bytes for the comment string length
288- // + N bytes for the comment string itself
289- });
290- head .putInt (comment .length );
291- head .put (comment );
292-
293- return head .array ();
287+ var metadata = "" ;
288+ metadata += String .format ("COMMENT=Downloaded using NewPipe %s on %s\n " ,
289+ BuildConfig .VERSION_NAME ,
290+ OffsetDateTime .now ().toString ());
291+ if (streamInfo != null ) {
292+ metadata += String .format ("COMMENT=URL: %s\n " , streamInfo .getUrl ());
293+ metadata += String .format ("GENRE=%s\n " , streamInfo .getCategory ());
294+ metadata += String .format ("ARTIST=%s\n " , streamInfo .getUploaderName ());
295+ metadata += String .format ("TITLE=%s\n " , streamInfo .getName ());
296+ metadata += String .format ("DATE=%s\n " ,
297+ streamInfo
298+ .getUploadDate ()
299+ .getLocalDateTime ()
300+ .format (DateTimeFormatter .ISO_DATE ));
301+ }
302+
303+ Log .d ("OggFromWebMWriter" , "Creating metadata header with this data:" );
304+ Log .d ("OggFromWebMWriter" , metadata );
305+
306+ return makeOpusTagsHeader (metadata );
294307 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
295308 return new byte []{
296309 0x03 , // ¿¿¿???
@@ -304,6 +317,64 @@ private byte[] makeMetadata() {
304317 return null ;
305318 }
306319
320+ /**
321+ * This creates a single metadata tag for use in opus metadata headers. It contains the four
322+ * byte string length field and includes the string as-is. This cannot be used independently,
323+ * but must follow a proper "OpusTags" header.
324+ *
325+ * @param keyValue A key-value pair in the format "KEY=some value"
326+ * @return The binary data of the encoded metadata tag
327+ */
328+ private static byte [] makeOpusMetadataTag (final String keyValue ) {
329+ // Ensure the key is uppercase
330+ final var delimiterIndex = keyValue .indexOf ('=' );
331+ final var key = keyValue .substring (0 , delimiterIndex ).toUpperCase ();
332+ final var value = keyValue .substring (delimiterIndex + 1 );
333+ final var reconstructedKeyValue = key + "=" + value ;
334+
335+ final var bytes = reconstructedKeyValue .getBytes ();
336+ final var buf = ByteBuffer .allocate (4 + bytes .length );
337+ buf .order (ByteOrder .LITTLE_ENDIAN );
338+ buf .putInt (bytes .length );
339+ buf .put (bytes );
340+ return buf .array ();
341+ }
342+
343+ /**
344+ * This returns a complete "OpusTags" header, created from the provided tags string.
345+ * <p>
346+ * You probably want to use makeOpusMetadata(), which uses this function to create
347+ * a header with sensible metadata filled in.
348+ *
349+ * @param keyValueLines A multiline string with each line containing a key-value pair
350+ * in the format "KEY=some value". This may also be a blank string.
351+ * @return The binary header
352+ */
353+ private static byte [] makeOpusTagsHeader (@ NonNull final String keyValueLines ) {
354+ final var tags = keyValueLines
355+ .lines ()
356+ .map (String ::trim )
357+ .filter (s -> !s .isBlank ())
358+ .map (OggFromWebMWriter ::makeOpusMetadataTag )
359+ .collect (Collectors .toUnmodifiableList ());
360+
361+ final var tagsBytes = tags .stream ().collect (Collectors .summingInt (arr -> arr .length ));
362+
363+ // Fixed header fields + dynamic fields
364+ final var byteCount = 16 + tagsBytes ;
365+
366+ final var head = ByteBuffer .allocate (byteCount );
367+ head .order (ByteOrder .LITTLE_ENDIAN );
368+ head .put (new byte []{
369+ 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
370+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
371+ });
372+ head .putInt (tags .size ()); // 4 bytes for tag count
373+ tags .forEach (head ::put ); // dynamic amount of tag bytes
374+
375+ return head .array ();
376+ }
377+
307378 private void write (final ByteBuffer buffer ) throws IOException {
308379 output .write (buffer .array (), 0 , buffer .position ());
309380 buffer .position (0 );
0 commit comments