11package org .schabi .newpipe .streams ;
22
3+ import static org .schabi .newpipe .MainActivity .DEBUG ;
4+
5+ import android .util .Log ;
6+ import android .util .Pair ;
7+
38import androidx .annotation .NonNull ;
49import androidx .annotation .Nullable ;
510
11+ import org .schabi .newpipe .extractor .stream .StreamInfo ;
612import org .schabi .newpipe .streams .WebMReader .Cluster ;
713import org .schabi .newpipe .streams .WebMReader .Segment ;
814import org .schabi .newpipe .streams .WebMReader .SimpleBlock ;
1319import java .io .IOException ;
1420import java .nio .ByteBuffer ;
1521import java .nio .ByteOrder ;
22+ import java .time .format .DateTimeFormatter ;
23+ import java .util .ArrayList ;
24+ import java .util .List ;
25+ import java .util .stream .Collectors ;
1626
1727/**
1828 * @author kapodamy
@@ -52,8 +62,10 @@ public class OggFromWebMWriter implements Closeable {
5262 private long segmentTableNextTimestamp = TIME_SCALE_NS ;
5363
5464 private final int [] crc32Table = new int [256 ];
65+ private final StreamInfo streamInfo ;
5566
56- public OggFromWebMWriter (@ NonNull final SharpStream source , @ NonNull final SharpStream target ) {
67+ public OggFromWebMWriter (@ NonNull final SharpStream source , @ NonNull final SharpStream target ,
68+ @ Nullable final StreamInfo streamInfo ) {
5769 if (!source .canRead () || !source .canRewind ()) {
5870 throw new IllegalArgumentException ("source stream must be readable and allows seeking" );
5971 }
@@ -63,6 +75,7 @@ public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final Sharp
6375
6476 this .source = source ;
6577 this .output = target ;
78+ this .streamInfo = streamInfo ;
6679
6780 this .streamId = (int ) System .currentTimeMillis ();
6881
@@ -271,12 +284,31 @@ private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffe
271284
272285 @ Nullable
273286 private byte [] makeMetadata () {
287+ if (DEBUG ) {
288+ Log .d ("OggFromWebMWriter" , "Downloading media with codec ID " + webmTrack .codecId );
289+ }
290+
274291 if ("A_OPUS" .equals (webmTrack .codecId )) {
275- return new byte []{
276- 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
277- 0x00 , 0x00 , 0x00 , 0x00 , // writing application string size (not present)
278- 0x00 , 0x00 , 0x00 , 0x00 // additional tags count (zero means no tags)
279- };
292+ final var metadata = new ArrayList <Pair <String , String >>();
293+ if (streamInfo != null ) {
294+ metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
295+ metadata .add (Pair .create ("GENRE" , streamInfo .getCategory ()));
296+ metadata .add (Pair .create ("ARTIST" , streamInfo .getUploaderName ()));
297+ metadata .add (Pair .create ("TITLE" , streamInfo .getName ()));
298+ metadata .add (Pair .create ("DATE" , streamInfo
299+ .getUploadDate ()
300+ .getLocalDateTime ()
301+ .format (DateTimeFormatter .ISO_DATE )));
302+ }
303+
304+ if (DEBUG ) {
305+ Log .d ("OggFromWebMWriter" , "Creating metadata header with this data:" );
306+ metadata .forEach (p -> {
307+ Log .d ("OggFromWebMWriter" , p .first + "=" + p .second );
308+ });
309+ }
310+
311+ return makeOpusTagsHeader (metadata );
280312 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
281313 return new byte []{
282314 0x03 , // ¿¿¿???
@@ -290,6 +322,59 @@ private byte[] makeMetadata() {
290322 return null ;
291323 }
292324
325+ /**
326+ * This creates a single metadata tag for use in opus metadata headers. It contains the four
327+ * byte string length field and includes the string as-is. This cannot be used independently,
328+ * but must follow a proper "OpusTags" header.
329+ *
330+ * @param pair A key-value pair in the format "KEY=some value"
331+ * @return The binary data of the encoded metadata tag
332+ */
333+ private static byte [] makeOpusMetadataTag (final Pair <String , String > pair ) {
334+ final var keyValue = pair .first .toUpperCase () + "=" + pair .second .trim ();
335+
336+ final var bytes = keyValue .getBytes ();
337+ final var buf = ByteBuffer .allocate (4 + bytes .length );
338+ buf .order (ByteOrder .LITTLE_ENDIAN );
339+ buf .putInt (bytes .length );
340+ buf .put (bytes );
341+ return buf .array ();
342+ }
343+
344+ /**
345+ * This returns a complete "OpusTags" header, created from the provided metadata tags.
346+ * <p>
347+ * You probably want to use makeOpusMetadata(), which uses this function to create
348+ * a header with sensible metadata filled in.
349+ *
350+ * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping
351+ * from one key to multiple values.
352+ * @return The binary header
353+ */
354+ private static byte [] makeOpusTagsHeader (final List <Pair <String , String >> keyValueLines ) {
355+ final var tags = keyValueLines
356+ .stream ()
357+ .filter (p -> !p .second .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+
293378 private void write (final ByteBuffer buffer ) throws IOException {
294379 output .write (buffer .array (), 0 , buffer .position ());
295380 buffer .position (0 );
0 commit comments