Skip to content

Commit 5fa8895

Browse files
committed
Add metadata to mp4 files that usually would not need postprocessing
Extract MP4 metadata (udta) generation into separate helper class
1 parent 4899651 commit 5fa8895

File tree

5 files changed

+734
-198
lines changed

5 files changed

+734
-198
lines changed

app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,8 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
10931093
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
10941094
nearLength = secondary.getSizeInBytes() + videoSize;
10951095
}
1096+
} else if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
1097+
psName = Postprocessing.ALGORITHM_MP4_METADATA;
10961098
}
10971099
break;
10981100
case R.id.subtitle_button:

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

Lines changed: 30 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,22 @@
1111
import org.schabi.newpipe.streams.Mp4DashReader.TrackKind;
1212
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
1313
import org.schabi.newpipe.streams.io.SharpStream;
14-
import org.schabi.newpipe.util.StreamInfoMetadataHelper;
1514

16-
import java.io.ByteArrayOutputStream;
1715
import java.io.IOException;
1816
import java.nio.ByteBuffer;
19-
import java.nio.charset.StandardCharsets;
2017
import java.util.ArrayList;
2118

19+
import us.shandian.giga.postprocessing.Mp4MetadataHelper;
20+
2221
/**
2322
* MP4 muxer that builds a standard MP4 file from DASH fragmented MP4 sources.
2423
*
25-
* <p>
26-
* See <a href="https://atomicparsley.sourceforge.net/mpeg-4files.html">
27-
* https://atomicparsley.sourceforge.net/mpeg-4files.html</a> for information on
24+
* @see <a href="https://atomicparsley.sourceforge.net/mpeg-4files.html">
25+
* https://atomicparsley.sourceforge.net/mpeg-4files.html</a> for a quick summary on
2826
* the MP4 file format and its specification.
29-
* </p>
30-
*
27+
* @see <a href="https://developer.apple.com/documentation/quicktime-file-format/">
28+
* Apple Quick Time Format Specification</a> which is the basis for MP4 file format
29+
* and contains detailed information about the structure of MP4 files.
3130
* @author kapodamy
3231
*/
3332
public class Mp4FromDashWriter {
@@ -64,8 +63,8 @@ public class Mp4FromDashWriter {
6463

6564
private final ArrayList<Integer> compatibleBrands = new ArrayList<>(5);
6665

67-
private final StreamInfo streamInfo;
68-
private final Bitmap thumbnail;
66+
67+
private final Mp4MetadataHelper metadataHelper;
6968

7069
public Mp4FromDashWriter(final StreamInfo streamInfo,
7170
final Bitmap thumbnail,
@@ -76,8 +75,26 @@ public Mp4FromDashWriter(final StreamInfo streamInfo,
7675
}
7776
}
7877

79-
this.streamInfo = streamInfo;
80-
this.thumbnail = thumbnail;
78+
this.metadataHelper = new Mp4MetadataHelper(
79+
this::auxOffset,
80+
buffer -> {
81+
try {
82+
auxWrite(buffer);
83+
} catch (final IOException e) {
84+
throw new RuntimeException(e);
85+
}
86+
},
87+
offset -> {
88+
try {
89+
return lengthFor(offset);
90+
} catch (final IOException e) {
91+
throw new RuntimeException(e);
92+
}
93+
},
94+
streamInfo,
95+
thumbnail
96+
);
97+
8198
sourceTracks = sources;
8299
readers = new Mp4DashReader[sourceTracks.length];
83100
readersChunks = new Mp4DashChunk[readers.length];
@@ -733,7 +750,7 @@ private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo
733750

734751
makeMvhd(longestTrack);
735752

736-
makeUdta();
753+
metadataHelper.makeUdta();
737754

738755
for (int i = 0; i < tracks.length; i++) {
739756
if (tracks[i].trak.tkhd.matrix.length != 36) {
@@ -929,191 +946,6 @@ private byte[] makeSgpd() {
929946
}
930947

931948

932-
/**
933-
* Create the 'udta' box with metadata fields.
934-
* {@code udta} is a user data box that can contain various types of metadata,
935-
* including title, artist, date, and cover art.
936-
* @see <a href="https://developer.apple.com/documentation/quicktime-file-format/
937-
* user_data_atoms">Apple Quick Time Format Specification for user data atoms</a>
938-
* @see <a href="https://wiki.multimedia.cx/index.php?title=FFmpeg_Metadata
939-
* #QuickTime/MOV/MP4/M4A/et_al.">Multimedia Wiki FFmpeg Metadata</a>
940-
* @see <a href="https://atomicparsley.sourceforge.net/mpeg-4files.html">atomicparsley docs</a>
941-
* for a short and understandable reference about metadata keys and values
942-
* @throws IOException
943-
*/
944-
private void makeUdta() throws IOException {
945-
if (streamInfo == null) {
946-
return;
947-
}
948-
949-
// udta
950-
final int startUdta = auxOffset();
951-
auxWrite(ByteBuffer.allocate(8).putInt(0).putInt(0x75647461).array()); // "udta"
952-
953-
// meta (full box: type + version/flags)
954-
final int startMeta = auxOffset();
955-
auxWrite(ByteBuffer.allocate(8).putInt(0).putInt(0x6D657461).array()); // "meta"
956-
auxWrite(ByteBuffer.allocate(4).putInt(0).array()); // version & flags = 0
957-
958-
// hdlr inside meta
959-
auxWrite(makeMetaHdlr());
960-
961-
// ilst container
962-
final int startIlst = auxOffset();
963-
auxWrite(ByteBuffer.allocate(8).putInt(0).putInt(0x696C7374).array()); // "ilst"
964-
965-
// write metadata items
966-
967-
final var metaHelper = new StreamInfoMetadataHelper(streamInfo);
968-
final String title = metaHelper.getTitle();
969-
final String artist = metaHelper.getArtist();
970-
final String date = metaHelper.getReleaseDate().getLocalDateTime()
971-
.toLocalDate().toString();
972-
final String recordLabel = metaHelper.getRecordLabel();
973-
final String copyright = metaHelper.getCopyright();
974-
975-
if (title != null && !title.isEmpty()) {
976-
writeMetaItem("©nam", title);
977-
}
978-
if (artist != null && !artist.isEmpty()) {
979-
writeMetaItem("©ART", artist);
980-
}
981-
if (date != null && !date.isEmpty()) {
982-
// this means 'year' in mp4 metadata, who the hell thought that?
983-
writeMetaItem("©day", date);
984-
}
985-
if (recordLabel != null && !recordLabel.isEmpty()) {
986-
writeMetaItem("©lab", recordLabel);
987-
}
988-
if (copyright != null && !copyright.isEmpty()) {
989-
writeMetaItem("©cpy", copyright);
990-
}
991-
992-
if (thumbnail != null) {
993-
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
994-
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, baos);
995-
final byte[] imgBytes = baos.toByteArray();
996-
baos.close();
997-
// 0x0000000E = PNG type indicator for 'data' box (0x0D = JPEG)
998-
writeMetaCover(imgBytes, 0x0000000E);
999-
1000-
}
1001-
1002-
// fix lengths
1003-
lengthFor(startIlst);
1004-
lengthFor(startMeta);
1005-
lengthFor(startUdta);
1006-
1007-
}
1008-
1009-
/**
1010-
* Helper to write a metadata item inside the 'ilst' box.
1011-
*
1012-
* <pre>
1013-
* [size][key] [data_box]
1014-
* data_box = [size]["data"][type(4bytes)=1][locale(4bytes)=0][payload]
1015-
* </pre>
1016-
*
1017-
* @param keyStr 4-char metadata key
1018-
* @param value the metadata value
1019-
* @throws IOException
1020-
*/
1021-
//
1022-
private void writeMetaItem(final String keyStr, final String value) throws IOException {
1023-
final byte[] valBytes = value.getBytes(StandardCharsets.UTF_8);
1024-
final byte[] keyBytes = keyStr.getBytes(StandardCharsets.ISO_8859_1);
1025-
1026-
final int dataBoxSize = 16 + valBytes.length; // 4(size)+4("data")+4(type/locale)+payload
1027-
final int itemBoxSize = 8 + dataBoxSize; // 4(size)+4(key)+dataBox
1028-
1029-
final ByteBuffer buf = ByteBuffer.allocate(itemBoxSize);
1030-
buf.putInt(itemBoxSize);
1031-
// key (4 bytes)
1032-
if (keyBytes.length == 4) {
1033-
buf.put(keyBytes);
1034-
} else {
1035-
// fallback: pad or truncate
1036-
final byte[] kb = new byte[4];
1037-
System.arraycopy(keyBytes, 0, kb, 0, Math.min(keyBytes.length, 4));
1038-
buf.put(kb);
1039-
}
1040-
1041-
// data box
1042-
buf.putInt(dataBoxSize);
1043-
buf.putInt(0x64617461); // "data"
1044-
buf.putInt(0x00000001); // well-known type indicator (UTF-8)
1045-
buf.putInt(0x00000000); // locale
1046-
buf.put(valBytes);
1047-
1048-
auxWrite(buf.array());
1049-
}
1050-
1051-
/**
1052-
* Create a minimal hdlr box for the meta container.
1053-
* The boxsize is fixed (33 bytes) as no name is provided.
1054-
* @return byte array with the hdlr box
1055-
*/
1056-
private byte[] makeMetaHdlr() {
1057-
final ByteBuffer buf = ByteBuffer.allocate(33);
1058-
buf.putInt(33);
1059-
buf.putInt(0x68646C72); // "hdlr"
1060-
buf.putInt(0x00000000); // pre-defined
1061-
buf.putInt(0x6D646972); // "mdir" handler_type (metadata directory)
1062-
buf.putInt(0x00000000); // subtype / reserved
1063-
buf.put(new byte[12]); // reserved
1064-
buf.put((byte) 0x00); // name (empty, null-terminated)
1065-
return buf.array();
1066-
}
1067-
1068-
/**
1069-
* Helper to add cover image inside the 'udta' box.
1070-
* <p>
1071-
* This method writes the 'covr' metadata item which contains the cover image.
1072-
* The cover image is displayed as thumbnail in many media players and file managers.
1073-
* </p>
1074-
* <pre>
1075-
* [size][key] [data_box]
1076-
* data_box = [size]["data"][type(4bytes)][locale(4bytes)=0][payload]
1077-
* </pre>
1078-
*
1079-
* @param imageData image byte data
1080-
* @param dataType type indicator: 0x0000000E = PNG, 0x0000000D = JPEG
1081-
* @throws IOException
1082-
*/
1083-
private void writeMetaCover(final byte[] imageData, final int dataType) throws IOException {
1084-
if (imageData == null || imageData.length == 0) {
1085-
return;
1086-
}
1087-
1088-
final byte[] keyBytes = "covr".getBytes(StandardCharsets.ISO_8859_1);
1089-
1090-
// data box: 4(size) + 4("data") + 4(type) + 4(locale) + payload
1091-
final int dataBoxSize = 16 + imageData.length;
1092-
final int itemBoxSize = 8 + dataBoxSize;
1093-
1094-
final ByteBuffer buf = ByteBuffer.allocate(itemBoxSize);
1095-
buf.putInt(itemBoxSize);
1096-
1097-
// key (4 chars)
1098-
if (keyBytes.length == 4) {
1099-
buf.put(keyBytes);
1100-
} else {
1101-
final byte[] kb = new byte[4];
1102-
System.arraycopy(keyBytes, 0, kb, 0, Math.min(keyBytes.length, 4));
1103-
buf.put(kb);
1104-
}
1105-
1106-
// data box
1107-
buf.putInt(dataBoxSize);
1108-
buf.putInt(0x64617461); // "data"
1109-
buf.putInt(dataType); // type indicator: 0x0000000E = PNG, 0x0000000D = JPEG
1110-
buf.putInt(0x00000000); // locale
1111-
buf.put(imageData);
1112-
1113-
auxWrite(buf.array());
1114-
}
1115-
1116-
1117949
static class TablesInfo {
1118950
int stts;
1119951
int stsc;

0 commit comments

Comments
 (0)