Add thumbnail / cover art to mp4 downloads

This commit is contained in:
tobigr 2026-01-03 21:15:14 +01:00
parent bacbe3f847
commit a91161525a
3 changed files with 75 additions and 3 deletions

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.streams;
import android.graphics.Bitmap;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
@ -10,13 +12,20 @@ import org.schabi.newpipe.streams.Mp4DashReader.TrackKind;
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* MP4 muxer that builds a standard MP4 file from DASH fragmented MP4 sources.
*
* @author kapodamy
*
* @implNote See <a href="https://atomicparsley.sourceforge.net/mpeg-4files.html">
* https://atomicparsley.sourceforge.net/mpeg-4files.html</a> for information on
* the MP4 file format and its specification.
*/
public class Mp4FromDashWriter {
private static final int EPOCH_OFFSET = 2082844800;
@ -53,8 +62,10 @@ public class Mp4FromDashWriter {
private final ArrayList<Integer> compatibleBrands = new ArrayList<>(5);
private final StreamInfo streamInfo;
private final Bitmap thumbnail;
public Mp4FromDashWriter(final StreamInfo streamInfo,
final Bitmap thumbnail,
final SharpStream... sources) throws IOException {
for (final SharpStream src : sources) {
if (!src.canRewind() && !src.canRead()) {
@ -63,6 +74,7 @@ public class Mp4FromDashWriter {
}
this.streamInfo = streamInfo;
this.thumbnail = thumbnail;
sourceTracks = sources;
readers = new Mp4DashReader[sourceTracks.length];
readersChunks = new Mp4DashChunk[readers.length];
@ -946,10 +958,23 @@ public class Mp4FromDashWriter {
writeMetaItem("©day", date);
}
if (thumbnail != null) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, baos);
final byte[] imgBytes = baos.toByteArray();
baos.close();
// 0x0000000E = PNG type indicator for 'data' box (0x0D = JPEG)
writeMetaCover(imgBytes, 0x0000000E);
}
// fix lengths
lengthFor(startIlst);
lengthFor(startMeta);
lengthFor(startUdta);
}
/**
@ -997,9 +1022,9 @@ public class Mp4FromDashWriter {
/**
* Create a minimal hdlr box for the meta container.
* The boxsize is fixed (33 bytes) as no name is provided.
* @return byte array with the hdlr box
*/
private byte[] makeMetaHdlr() {
final ByteBuffer buf = ByteBuffer.allocate(33);
buf.putInt(33);
buf.putInt(0x68646C72); // "hdlr"
@ -1011,6 +1036,52 @@ public class Mp4FromDashWriter {
return buf.array();
}
/**
* Helper to write cover image inside the 'udta' box.
*
* <pre>
* [size][key] [data_box]
* data_box = [size]["data"][type(4bytes)][locale(4bytes)=0][payload]
* </pre>
*
* @param imageData image byte data
* @param dataType type indicator: 0x0000000E = PNG, 0x0000000D = JPEG
* @throws IOException
*/
private void writeMetaCover(final byte[] imageData, final int dataType) throws IOException {
if (imageData == null || imageData.length == 0) {
return;
}
final byte[] keyBytes = "covr".getBytes(StandardCharsets.ISO_8859_1);
// data box: 4(size) + 4("data") + 4(type) + 4(locale) + payload
final int dataBoxSize = 16 + imageData.length;
final int itemBoxSize = 8 + dataBoxSize;
final ByteBuffer buf = ByteBuffer.allocate(itemBoxSize);
buf.putInt(itemBoxSize);
// key (4 chars)
if (keyBytes.length == 4) {
buf.put(keyBytes);
} else {
final byte[] kb = new byte[4];
System.arraycopy(keyBytes, 0, kb, 0, Math.min(keyBytes.length, 4));
buf.put(kb);
}
// data box
buf.putInt(dataBoxSize);
buf.putInt(0x64617461); // "data"
buf.putInt(dataType); // type indicator: 0x0000000E = PNG, 0x0000000D = JPEG
buf.putInt(0x00000000); // locale
buf.put(imageData);
auxWrite(buf.array());
}
static class TablesInfo {
int stts;
int stsc;

View File

@ -30,7 +30,8 @@ class M4aNoDash extends Postprocessing {
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(this.streamInfo, sources[0]);
Mp4FromDashWriter muxer = new Mp4FromDashWriter(
this.streamInfo, this.thumbnail, sources[0]);
muxer.setMainBrand(0x4D344120);// binary string "M4A "
muxer.parseSources();
muxer.selectTracks(0);

View File

@ -16,7 +16,7 @@ class Mp4FromDashMuxer extends Postprocessing {
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(this.streamInfo, sources);
Mp4FromDashWriter muxer = new Mp4FromDashWriter(this.streamInfo, this.thumbnail, sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);