1111import org .schabi .newpipe .streams .Mp4DashReader .TrackKind ;
1212import org .schabi .newpipe .streams .Mp4DashReader .TrunEntry ;
1313import org .schabi .newpipe .streams .io .SharpStream ;
14- import org .schabi .newpipe .util .StreamInfoMetadataHelper ;
1514
16- import java .io .ByteArrayOutputStream ;
1715import java .io .IOException ;
1816import java .nio .ByteBuffer ;
19- import java .nio .charset .StandardCharsets ;
2017import 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 */
3332public 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