|
1 | 1 | package org.schabi.newpipe.streams; |
2 | 2 |
|
| 3 | +import org.schabi.newpipe.extractor.stream.StreamInfo; |
3 | 4 | import org.schabi.newpipe.streams.Mp4DashReader.Hdlr; |
4 | 5 | import org.schabi.newpipe.streams.Mp4DashReader.Mdia; |
5 | 6 | import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk; |
|
11 | 12 |
|
12 | 13 | import java.io.IOException; |
13 | 14 | import java.nio.ByteBuffer; |
| 15 | +import java.nio.charset.StandardCharsets; |
14 | 16 | import java.util.ArrayList; |
15 | 17 |
|
16 | 18 | /** |
@@ -50,13 +52,17 @@ public class Mp4FromDashWriter { |
50 | 52 |
|
51 | 53 | private final ArrayList<Integer> compatibleBrands = new ArrayList<>(5); |
52 | 54 |
|
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 { |
54 | 59 | for (final SharpStream src : sources) { |
55 | 60 | if (!src.canRewind() && !src.canRead()) { |
56 | 61 | throw new IOException("All sources must be readable and allow rewind"); |
57 | 62 | } |
58 | 63 | } |
59 | 64 |
|
| 65 | + this.streamInfo = streamInfo; |
60 | 66 | sourceTracks = sources; |
61 | 67 | readers = new Mp4DashReader[sourceTracks.length]; |
62 | 68 | readersChunks = new Mp4DashChunk[readers.length]; |
@@ -712,10 +718,12 @@ private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo |
712 | 718 |
|
713 | 719 | makeMvhd(longestTrack); |
714 | 720 |
|
| 721 | + makeUdta(); |
| 722 | + |
715 | 723 | for (int i = 0; i < tracks.length; i++) { |
716 | 724 | 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); |
719 | 727 | } |
720 | 728 | makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); |
721 | 729 | } |
@@ -898,6 +906,111 @@ private byte[] makeSgpd() { |
898 | 906 | return buffer.array(); |
899 | 907 | } |
900 | 908 |
|
| 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 | + |
901 | 1014 | static class TablesInfo { |
902 | 1015 | int stts; |
903 | 1016 | int stsc; |
|
0 commit comments