-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Feat: opus metadata encoding #12974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: opus metadata encoding #12974
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| package org.schabi.newpipe.streams; | ||
|
|
||
| import android.util.Log; | ||
| import android.util.Pair; | ||
|
|
||
| import androidx.annotation.NonNull; | ||
| import androidx.annotation.Nullable; | ||
|
|
||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||
| import org.schabi.newpipe.streams.WebMReader.Cluster; | ||
| import org.schabi.newpipe.streams.WebMReader.Segment; | ||
| import org.schabi.newpipe.streams.WebMReader.SimpleBlock; | ||
|
|
@@ -13,6 +17,10 @@ | |
| import java.io.IOException; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.ByteOrder; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * @author kapodamy | ||
|
|
@@ -52,8 +60,10 @@ public class OggFromWebMWriter implements Closeable { | |
| private long segmentTableNextTimestamp = TIME_SCALE_NS; | ||
|
|
||
| private final int[] crc32Table = new int[256]; | ||
| private final StreamInfo streamInfo; | ||
|
|
||
| public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { | ||
| public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target, | ||
| @Nullable final StreamInfo streamInfo) { | ||
| if (!source.canRead() || !source.canRewind()) { | ||
| throw new IllegalArgumentException("source stream must be readable and allows seeking"); | ||
| } | ||
|
|
@@ -63,6 +73,7 @@ public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final Sharp | |
|
|
||
| this.source = source; | ||
| this.output = target; | ||
| this.streamInfo = streamInfo; | ||
|
|
||
| this.streamId = (int) System.currentTimeMillis(); | ||
|
|
||
|
|
@@ -271,12 +282,27 @@ private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffe | |
|
|
||
| @Nullable | ||
| private byte[] makeMetadata() { | ||
| Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId); | ||
|
|
||
| if ("A_OPUS".equals(webmTrack.codecId)) { | ||
| return new byte[]{ | ||
| 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string | ||
| 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) | ||
| 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) | ||
| }; | ||
| final var metadata = new ArrayList<Pair<String, String>>(); | ||
| if (streamInfo != null) { | ||
| metadata.add(Pair.create("COMMENT", streamInfo.getUrl())); | ||
| metadata.add(Pair.create("GENRE", streamInfo.getCategory())); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The genre should only be set if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line 357 of OggFromWebMWriter.java filters out empty tags. Have you tested this and found that this filter does not work? I have not yet come across a video without a category, so I might be wrong. |
||
| metadata.add(Pair.create("ARTIST", streamInfo.getUploaderName())); | ||
| metadata.add(Pair.create("TITLE", streamInfo.getName())); | ||
| metadata.add(Pair.create("DATE", streamInfo | ||
| .getUploadDate() | ||
| .getLocalDateTime() | ||
| .format(DateTimeFormatter.ISO_DATE))); | ||
| } | ||
|
|
||
| Log.d("OggFromWebMWriter", "Creating metadata header with this data:"); | ||
| metadata.forEach(p -> { | ||
| Log.d("OggFromWebMWriter", p.first + "=" + p.second); | ||
| }); | ||
TobiGr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return makeOpusTagsHeader(metadata); | ||
| } else if ("A_VORBIS".equals(webmTrack.codecId)) { | ||
| return new byte[]{ | ||
| 0x03, // ¿¿¿??? | ||
|
|
@@ -290,6 +316,59 @@ private byte[] makeMetadata() { | |
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * This creates a single metadata tag for use in opus metadata headers. It contains the four | ||
| * byte string length field and includes the string as-is. This cannot be used independently, | ||
| * but must follow a proper "OpusTags" header. | ||
| * | ||
| * @param pair A key-value pair in the format "KEY=some value" | ||
| * @return The binary data of the encoded metadata tag | ||
| */ | ||
| private static byte[] makeOpusMetadataTag(final Pair<String, String> pair) { | ||
| final var keyValue = pair.first.toUpperCase() + "=" + pair.second.trim(); | ||
|
|
||
| final var bytes = keyValue.getBytes(); | ||
| final var buf = ByteBuffer.allocate(4 + bytes.length); | ||
| buf.order(ByteOrder.LITTLE_ENDIAN); | ||
| buf.putInt(bytes.length); | ||
| buf.put(bytes); | ||
| return buf.array(); | ||
| } | ||
|
|
||
| /** | ||
| * This returns a complete "OpusTags" header, created from the provided metadata tags. | ||
| * <p> | ||
| * You probably want to use makeOpusMetadata(), which uses this function to create | ||
| * a header with sensible metadata filled in. | ||
| * | ||
| * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping | ||
| * from one key to multiple values. | ||
| * @return The binary header | ||
| */ | ||
| private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyValueLines) { | ||
| final var tags = keyValueLines | ||
| .stream() | ||
| .filter(p -> !p.second.isBlank()) | ||
| .map(OggFromWebMWriter::makeOpusMetadataTag) | ||
| .collect(Collectors.toUnmodifiableList()); | ||
|
|
||
| final var tagsBytes = tags.stream().collect(Collectors.summingInt(arr -> arr.length)); | ||
|
|
||
| // Fixed header fields + dynamic fields | ||
| final var byteCount = 16 + tagsBytes; | ||
|
|
||
| final var head = ByteBuffer.allocate(byteCount); | ||
| head.order(ByteOrder.LITTLE_ENDIAN); | ||
| head.put(new byte[]{ | ||
| 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string | ||
| 0x00, 0x00, 0x00, 0x00, // vendor (aka. Encoder) string of length 0 | ||
| }); | ||
| head.putInt(tags.size()); // 4 bytes for tag count | ||
| tags.forEach(head::put); // dynamic amount of tag bytes | ||
|
|
||
| return head.array(); | ||
| } | ||
|
|
||
| private void write(final ByteBuffer buffer) throws IOException { | ||
| output.write(buffer.array(), 0, buffer.position()); | ||
| buffer.position(0); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.