Add support for cover art / thumbnail for ogg downloads
This commit is contained in:
parent
e005333ada
commit
e0a1011cd6
@ -2,6 +2,7 @@ package org.schabi.newpipe.streams;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
@ -15,12 +16,15 @@ import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -63,9 +67,19 @@ public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
private final int[] crc32Table = new int[256];
|
||||
private final StreamInfo streamInfo;
|
||||
private final Bitmap thumbnail;
|
||||
|
||||
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target,
|
||||
@Nullable final StreamInfo streamInfo) {
|
||||
/**
|
||||
* Constructor of OggFromWebMWriter.
|
||||
* @param source
|
||||
* @param target
|
||||
* @param streamInfo the stream info
|
||||
* @param thumbnail the thumbnail bitmap used as cover art
|
||||
*/
|
||||
public OggFromWebMWriter(@NonNull final SharpStream source,
|
||||
@NonNull final SharpStream target,
|
||||
@Nullable final StreamInfo streamInfo,
|
||||
@Nullable final Bitmap thumbnail) {
|
||||
if (!source.canRead() || !source.canRewind()) {
|
||||
throw new IllegalArgumentException("source stream must be readable and allows seeking");
|
||||
}
|
||||
@ -76,6 +90,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
this.source = source;
|
||||
this.output = target;
|
||||
this.streamInfo = streamInfo;
|
||||
this.thumbnail = thumbnail;
|
||||
|
||||
this.streamId = (int) System.currentTimeMillis();
|
||||
|
||||
@ -299,6 +314,9 @@ public class OggFromWebMWriter implements Closeable {
|
||||
.getUploadDate()
|
||||
.getLocalDateTime()
|
||||
.format(DateTimeFormatter.ISO_DATE)));
|
||||
if (thumbnail != null) {
|
||||
metadata.add(makeOpusPictureTag(thumbnail));
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
@ -309,7 +327,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
return makeOpusTagsHeader(metadata);
|
||||
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
|
||||
return new byte[]{
|
||||
0x03, // ¿¿¿???
|
||||
0x03, // ???
|
||||
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
|
||||
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
|
||||
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
|
||||
@ -339,6 +357,56 @@ public class OggFromWebMWriter implements Closeable {
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@code METADATA_BLOCK_PICTURE} tag to the Opus metadata,
|
||||
* containing the provided bitmap as cover art.
|
||||
*
|
||||
* <p>
|
||||
* One could also use the COVERART tag instead, but it is not as widely supported
|
||||
* as METADATA_BLOCK_PICTURE.
|
||||
* </p>
|
||||
*
|
||||
* @param bitmap The bitmap to use as cover art
|
||||
* @return The key-value pair representing the tag
|
||||
*/
|
||||
private static Pair<String, String> makeOpusPictureTag(final Bitmap bitmap) {
|
||||
// FLAC picture block format (big-endian):
|
||||
// uint32 picture_type
|
||||
// uint32 mime_length, mime_string
|
||||
// uint32 desc_length, desc_string
|
||||
// uint32 width
|
||||
// uint32 height
|
||||
// uint32 color_depth
|
||||
// uint32 colors_indexed
|
||||
// uint32 data_length, data_bytes
|
||||
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
|
||||
|
||||
final byte[] imageData = baos.toByteArray();
|
||||
final byte[] mimeBytes = "image/jpeg".getBytes(StandardCharsets.UTF_8);
|
||||
final byte[] descBytes = new byte[0]; // optional description
|
||||
// fixed ints + mime + desc
|
||||
final int headerSize = 4 * 8 + mimeBytes.length + descBytes.length;
|
||||
final ByteBuffer buf = ByteBuffer.allocate(headerSize + imageData.length);
|
||||
buf.putInt(3); // picture type: 3 = Cover (front)
|
||||
buf.putInt(mimeBytes.length);
|
||||
buf.put(mimeBytes);
|
||||
buf.putInt(descBytes.length);
|
||||
// no description
|
||||
if (descBytes.length > 0) {
|
||||
buf.put(descBytes);
|
||||
}
|
||||
buf.putInt(bitmap.getWidth()); // width (unknown)
|
||||
buf.putInt(bitmap.getHeight()); // height (unknown)
|
||||
buf.putInt(0); // color depth
|
||||
buf.putInt(0); // colors indexed
|
||||
buf.putInt(imageData.length);
|
||||
buf.put(imageData);
|
||||
final String b64 = Base64.getEncoder().encodeToString(buf.array());
|
||||
return Pair.create("METADATA_BLOCK_PICTURE", b64);
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns a complete "OpusTags" header, created from the provided metadata tags.
|
||||
* <p>
|
||||
@ -447,7 +515,8 @@ public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
private boolean addPacketSegment(final int size) {
|
||||
if (size > 65025) {
|
||||
throw new UnsupportedOperationException("page size cannot be larger than 65025");
|
||||
throw new UnsupportedOperationException(
|
||||
String.format("page size is %s but cannot be larger than 65025", size));
|
||||
}
|
||||
|
||||
int available = (segmentTable.length - segmentTableSize) * 255;
|
||||
|
||||
@ -186,7 +186,7 @@ object ImageStrategy {
|
||||
fun dbUrlToImageList(url: String?): List<Image> {
|
||||
return when (url) {
|
||||
null -> listOf()
|
||||
else -> listOf(Image(url, -1, -1, ResolutionLevel.UNKNOWN))
|
||||
else -> listOf(Image(url, Image.HEIGHT_UNKNOWN, Image.WIDTH_UNKNOWN, ResolutionLevel.UNKNOWN))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Handler;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.OsConstants;
|
||||
@ -8,6 +10,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
|
||||
import java.io.File;
|
||||
@ -21,11 +24,18 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.util.Utility;
|
||||
@ -58,6 +68,10 @@ public class DownloadMission extends Mission {
|
||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||
static final int ERROR_HTTP_FORBIDDEN = 403;
|
||||
|
||||
private StreamInfo streamInfo;
|
||||
protected volatile Bitmap thumbnail;
|
||||
protected volatile boolean thumbnailFetched = false;
|
||||
|
||||
/**
|
||||
* The urls of the file to download
|
||||
*/
|
||||
@ -153,7 +167,8 @@ public class DownloadMission extends Mission {
|
||||
public transient Thread[] threads = new Thread[0];
|
||||
public transient Thread init = null;
|
||||
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind,
|
||||
Postprocessing psInstance, StreamInfo streamInfo, Context context) {
|
||||
if (Objects.requireNonNull(urls).length < 1)
|
||||
throw new IllegalArgumentException("urls array is empty");
|
||||
this.urls = urls;
|
||||
@ -163,6 +178,7 @@ public class DownloadMission extends Mission {
|
||||
this.maxRetry = 3;
|
||||
this.storage = storage;
|
||||
this.psAlgorithm = psInstance;
|
||||
this.streamInfo = streamInfo;
|
||||
|
||||
if (DEBUG && psInstance == null && urls.length > 1) {
|
||||
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
|
||||
@ -698,6 +714,7 @@ public class DownloadMission extends Mission {
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
psAlgorithm.setThumbnail(thumbnail);
|
||||
psAlgorithm.run(this);
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||
@ -829,6 +846,34 @@ public class DownloadMission extends Mission {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the thumbnail / cover art from a list of thumbnails.
|
||||
* The highest quality is selected.
|
||||
*
|
||||
* @param images the list of thumbnails
|
||||
*/
|
||||
public void fetchThumbnail(@NonNull final List<Image> images) {
|
||||
if (images.isEmpty()) {
|
||||
thumbnailFetched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Some containers have a limited size for embedded images / metadata.
|
||||
// To avoid problems, we download a medium quality image.
|
||||
// Alternative approaches are to either downscale a high res image or
|
||||
// to download the correct size depending on the chosen post-processing algorithm.
|
||||
final String thumbnailUrl = ImageStrategy.choosePreferredImage(
|
||||
images, PreferredImageQuality.MEDIUM);
|
||||
// TODO: get context from somewhere else
|
||||
thumbnail = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), thumbnailUrl);
|
||||
thumbnailFetched = true;
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "fetchThumbnail: failed to load thumbnail", e);
|
||||
thumbnailFetched = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static class HttpError extends Exception {
|
||||
final int statusCode;
|
||||
|
||||
@ -34,7 +34,8 @@ class OggFromWebmDemuxer extends Postprocessing {
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
|
||||
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out, streamInfo);
|
||||
OggFromWebMWriter demuxer = new OggFromWebMWriter(
|
||||
sources[0], out, streamInfo, thumbnail);
|
||||
demuxer.parseSource();
|
||||
demuxer.selectTrack(0);
|
||||
demuxer.build();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -80,6 +81,7 @@ public abstract class Postprocessing implements Serializable {
|
||||
|
||||
private String[] args;
|
||||
protected StreamInfo streamInfo;
|
||||
protected Bitmap thumbnail;
|
||||
|
||||
private transient DownloadMission mission;
|
||||
|
||||
@ -107,6 +109,10 @@ public abstract class Postprocessing implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void setThumbnail(Bitmap thumbnail) {
|
||||
this.thumbnail = thumbnail;
|
||||
}
|
||||
|
||||
|
||||
public void run(DownloadMission target) throws IOException {
|
||||
this.mission = target;
|
||||
|
||||
@ -40,6 +40,7 @@ import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
@ -408,7 +409,8 @@ public class DownloadManagerService extends Service {
|
||||
else
|
||||
ps = Postprocessing.getAlgorithm(psName, psArgs, streamInfo);
|
||||
|
||||
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
|
||||
final DownloadMission mission = new DownloadMission(
|
||||
urls, storage, kind, ps, streamInfo, getApplicationContext());
|
||||
mission.threadCount = threads;
|
||||
mission.source = streamInfo.getUrl();
|
||||
mission.nearLength = nearLength;
|
||||
@ -417,7 +419,18 @@ public class DownloadManagerService extends Service {
|
||||
if (ps != null)
|
||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||
|
||||
handleConnectivityState(true);// first check the actual network status
|
||||
if (streamInfo != null) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
mission.fetchThumbnail(streamInfo.getThumbnails());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "failed to fetch thumbnail for mission: "
|
||||
+ mission.storage.getName(), e);
|
||||
}
|
||||
}, "ThumbnailFetcher").start();
|
||||
}
|
||||
|
||||
handleConnectivityState(true); // first check the actual network status
|
||||
|
||||
mManager.startMission(mission);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user