Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
}

DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
currentInfo, psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));

Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show();
Expand Down
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;
Expand All @@ -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
Expand Down Expand Up @@ -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");
}
Expand All @@ -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();

Expand Down Expand Up @@ -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()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The genre should only be set if getCategory() is not empty

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
});

return makeOpusTagsHeader(metadata);
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
return new byte[]{
0x03, // ¿¿¿???
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ boolean test(SharpStream... sources) throws IOException {

@Override
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out, streamInfo);
demuxer.parseSource();
demuxer.selectTrack(0);
demuxer.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import androidx.annotation.NonNull;

import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.streams.io.SharpStream;

import java.io.File;
Expand All @@ -30,7 +31,8 @@ public abstract class Postprocessing implements Serializable {
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";

public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args,
StreamInfo streamInfo) {
Postprocessing instance;

switch (algorithmName) {
Expand All @@ -56,6 +58,7 @@ public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[
}

instance.args = args;
instance.streamInfo = streamInfo;
return instance;
}

Expand All @@ -75,8 +78,8 @@ public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[
*/
private final String name;


private String[] args;
protected StreamInfo streamInfo;

private transient DownloadMission mission;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
Expand Down Expand Up @@ -74,12 +75,12 @@ public class DownloadManagerService extends Service {
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
private static final String EXTRA_STREAM_INFO = "DownloadManagerService.extra.streamInfo";

private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
Expand Down Expand Up @@ -353,28 +354,28 @@ public void updateForegroundState(boolean state) {
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
* @param threads the number of threads maximal used to download chunks of the file.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @param source source url of the resource
* @param streamInfo stream metadata that may be written into the downloaded file.
* @param psArgs the arguments for the post-processing algorithm.
* @param nearLength the approximated final length of the file
* @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download
*/
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
char kind, int threads, String source, String psName,
char kind, int threads, StreamInfo streamInfo, String psName,
String[] psArgs, long nearLength,
ArrayList<MissionRecoveryInfo> recoveryInfo) {
final Intent intent = new Intent(context, DownloadManagerService.class)
.setAction(Intent.ACTION_RUN)
.putExtra(EXTRA_URLS, urls)
.putExtra(EXTRA_KIND, kind)
.putExtra(EXTRA_THREADS, threads)
.putExtra(EXTRA_SOURCE, source)
.putExtra(EXTRA_POSTPROCESSING_NAME, psName)
.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs)
.putExtra(EXTRA_NEAR_LENGTH, nearLength)
.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
.putExtra(EXTRA_PARENT_PATH, storage.getParentUri())
.putExtra(EXTRA_PATH, storage.getUri())
.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
.putExtra(EXTRA_STORAGE_TAG, storage.getTag())
.putExtra(EXTRA_STREAM_INFO, streamInfo);

context.startService(intent);
}
Expand All @@ -387,9 +388,9 @@ private void startMission(Intent intent) {
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
StreamInfo streamInfo = (StreamInfo)intent.getSerializableExtra(EXTRA_STREAM_INFO);
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
MissionRecoveryInfo.class);
Objects.requireNonNull(recovery);
Expand All @@ -405,11 +406,11 @@ private void startMission(Intent intent) {
if (psName == null)
ps = null;
else
ps = Postprocessing.getAlgorithm(psName, psArgs);
ps = Postprocessing.getAlgorithm(psName, psArgs, streamInfo);

final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
mission.threadCount = threads;
mission.source = source;
mission.source = streamInfo.getUrl();
mission.nearLength = nearLength;
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);

Expand Down