Skip to content

Commit c571196

Browse files
committed
Add metadata fields to MP4 downloads
1 parent 1815eec commit c571196

File tree

3 files changed

+118
-5
lines changed

3 files changed

+118
-5
lines changed

app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.schabi.newpipe.streams;
22

3+
import org.schabi.newpipe.extractor.stream.StreamInfo;
34
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
45
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
56
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
@@ -11,6 +12,7 @@
1112

1213
import java.io.IOException;
1314
import java.nio.ByteBuffer;
15+
import java.nio.charset.StandardCharsets;
1416
import java.util.ArrayList;
1517

1618
/**
@@ -50,13 +52,17 @@ public class Mp4FromDashWriter {
5052

5153
private final ArrayList<Integer> compatibleBrands = new ArrayList<>(5);
5254

53-
public Mp4FromDashWriter(final SharpStream... sources) throws IOException {
55+
private final StreamInfo streamInfo;
56+
57+
public Mp4FromDashWriter(final StreamInfo streamInfo,
58+
final SharpStream... sources) throws IOException {
5459
for (final SharpStream src : sources) {
5560
if (!src.canRewind() && !src.canRead()) {
5661
throw new IOException("All sources must be readable and allow rewind");
5762
}
5863
}
5964

65+
this.streamInfo = streamInfo;
6066
sourceTracks = sources;
6167
readers = new Mp4DashReader[sourceTracks.length];
6268
readersChunks = new Mp4DashChunk[readers.length];
@@ -712,10 +718,12 @@ private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo
712718

713719
makeMvhd(longestTrack);
714720

721+
makeUdta();
722+
715723
for (int i = 0; i < tracks.length; i++) {
716724
if (tracks[i].trak.tkhd.matrix.length != 36) {
717-
throw
718-
new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
725+
throw new RuntimeException(
726+
"bad track matrix length (expected 36) in track n°" + i);
719727
}
720728
makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
721729
}
@@ -898,6 +906,111 @@ private byte[] makeSgpd() {
898906
return buffer.array();
899907
}
900908

909+
910+
/**
911+
* Create the 'udta' box with metadata fields.
912+
* @throws IOException
913+
*/
914+
private void makeUdta() throws IOException {
915+
if (streamInfo == null) {
916+
return;
917+
}
918+
919+
final String title = streamInfo.getName();
920+
final String artist = streamInfo.getUploaderName();
921+
final String date = streamInfo.getUploadDate().getLocalDateTime().toLocalDate().toString();
922+
923+
// udta
924+
final int startUdta = auxOffset();
925+
auxWrite(ByteBuffer.allocate(8).putInt(0).putInt(0x75647461).array()); // "udta"
926+
927+
// meta (full box: type + version/flags)
928+
final int startMeta = auxOffset();
929+
auxWrite(ByteBuffer.allocate(8).putInt(0).putInt(0x6D657461).array()); // "meta"
930+
auxWrite(ByteBuffer.allocate(4).putInt(0).array()); // version & flags = 0
931+
932+
// hdlr inside meta
933+
auxWrite(makeMetaHdlr());
934+
935+
// ilst container
936+
final int startIlst = auxOffset();
937+
auxWrite(ByteBuffer.allocate(8).putInt(0).putInt(0x696C7374).array()); // "ilst"
938+
939+
if (title != null && !title.isEmpty()) {
940+
writeMetaItem("©nam", title);
941+
}
942+
if (artist != null && !artist.isEmpty()) {
943+
writeMetaItem("©ART", artist);
944+
}
945+
if (date != null && !date.isEmpty()) {
946+
writeMetaItem("©day", date);
947+
}
948+
949+
// fix lengths
950+
lengthFor(startIlst);
951+
lengthFor(startMeta);
952+
lengthFor(startUdta);
953+
}
954+
955+
/**
956+
* Helper to write a metadata item inside the 'ilst' box.
957+
*
958+
* <pre>
959+
* [size][key] [data_box]
960+
* data_box = [size]["data"][type(4bytes)=1][locale(4bytes)=0][payload]
961+
* </pre>
962+
*
963+
* @param keyStr 4-char metadata key
964+
* @param value the metadata value
965+
* @throws IOException
966+
*/
967+
//
968+
private void writeMetaItem(final String keyStr, final String value) throws IOException {
969+
final byte[] valBytes = value.getBytes(StandardCharsets.UTF_8);
970+
final byte[] keyBytes = keyStr.getBytes(StandardCharsets.ISO_8859_1);
971+
972+
final int dataBoxSize = 16 + valBytes.length; // 4(size)+4("data")+4(type/locale)+payload
973+
final int itemBoxSize = 8 + dataBoxSize; // 4(size)+4(key)+dataBox
974+
975+
final ByteBuffer buf = ByteBuffer.allocate(itemBoxSize);
976+
buf.putInt(itemBoxSize);
977+
// key (4 bytes)
978+
if (keyBytes.length == 4) {
979+
buf.put(keyBytes);
980+
} else {
981+
// fallback: pad or truncate
982+
final byte[] kb = new byte[4];
983+
System.arraycopy(keyBytes, 0, kb, 0, Math.min(keyBytes.length, 4));
984+
buf.put(kb);
985+
}
986+
987+
// data box
988+
buf.putInt(dataBoxSize);
989+
buf.putInt(0x64617461); // "data"
990+
buf.putInt(0x00000001); // well-known type indicator (UTF-8)
991+
buf.putInt(0x00000000); // locale
992+
buf.put(valBytes);
993+
994+
auxWrite(buf.array());
995+
}
996+
997+
/**
998+
* Create a minimal hdlr box for the meta container.
999+
* The boxsize is fixed (33 bytes) as no name is provided.
1000+
*/
1001+
private byte[] makeMetaHdlr() {
1002+
1003+
final ByteBuffer buf = ByteBuffer.allocate(33);
1004+
buf.putInt(33);
1005+
buf.putInt(0x68646C72); // "hdlr"
1006+
buf.putInt(0x00000000); // pre-defined
1007+
buf.putInt(0x6D646972); // "mdir" handler_type (metadata directory)
1008+
buf.putInt(0x00000000); // subtype / reserved
1009+
buf.put(new byte[12]); // reserved
1010+
buf.put((byte) 0x00); // name (empty, null-terminated)
1011+
return buf.array();
1012+
}
1013+
9011014
static class TablesInfo {
9021015
int stts;
9031016
int stsc;

app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ boolean test(SharpStream... sources) throws IOException {
3030

3131
@Override
3232
int process(SharpStream out, SharpStream... sources) throws IOException {
33-
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources[0]);
33+
Mp4FromDashWriter muxer = new Mp4FromDashWriter(this.streamInfo, sources[0]);
3434
muxer.setMainBrand(0x4D344120);// binary string "M4A "
3535
muxer.parseSources();
3636
muxer.selectTracks(0);

app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class Mp4FromDashMuxer extends Postprocessing {
1616

1717
@Override
1818
int process(SharpStream out, SharpStream... sources) throws IOException {
19-
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources);
19+
Mp4FromDashWriter muxer = new Mp4FromDashWriter(this.streamInfo, sources);
2020
muxer.parseSources();
2121
muxer.selectTracks(0, 0);
2222
muxer.build(out);

0 commit comments

Comments
 (0)