001/*
002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bdelbosc
018 */
019package org.nuxeo.importer.stream.consumer.watermarker;
020
021import java.io.ByteArrayOutputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.RandomAccessFile;
025import java.nio.ByteBuffer;
026import java.nio.channels.Channels;
027import java.nio.channels.FileChannel;
028import java.util.List;
029
030import org.mp4parser.Box;
031import org.mp4parser.Container;
032import org.mp4parser.IsoFile;
033import org.mp4parser.boxes.apple.AppleItemListBox;
034import org.mp4parser.boxes.apple.AppleNameBox;
035import org.mp4parser.boxes.iso14496.part12.ChunkOffsetBox;
036import org.mp4parser.boxes.iso14496.part12.FreeBox;
037import org.mp4parser.boxes.iso14496.part12.HandlerBox;
038import org.mp4parser.boxes.iso14496.part12.MetaBox;
039import org.mp4parser.boxes.iso14496.part12.MovieBox;
040import org.mp4parser.boxes.iso14496.part12.UserDataBox;
041import org.mp4parser.tools.Path;
042
043/**
044 * Adds watermark to mp4 file by modifying its title. This is for testing purpose only.
045 *
046 * @since 10.2
047 */
048public class Mp4Watermarker extends AbstractWatermarker {
049
050    protected static final String BOX_MOOV = "moov";
051
052    protected static final String BOX_MDAT = "mdat";
053
054    protected static final String PATH_UDTA = "udta";
055
056    protected static final String PATH_META = "meta";
057
058    protected static final String PATH_ILST = "ilst";
059
060    protected static final String PATH_NAM = "©nam";
061
062    protected static final String PATH_MVEX = "moov[0]/mvex[0]";
063
064    protected static final String PATH_CHUNKS = "trak/mdia[0]/minf[0]/stbl[0]/stco[0]";
065
066    protected static final String PATH_CHUNKS_BIS = "trak/mdia[0]/minf[0]/stbl[0]/st64[0]";
067
068    @Override
069    public java.nio.file.Path addWatermark(java.nio.file.Path inputFile, java.nio.file.Path outputDir,
070            String watermark) {
071        File videoFile = inputFile.toFile();
072        try (IsoFile isoFile = new IsoFile(inputFile.toString())) {
073
074            MovieBox moov = isoFile.getBoxes(MovieBox.class).get(0);
075            FreeBox freeBox = findFreeBox(moov);
076            long sizeBefore = moov.getSize();
077            long offset = 0;
078            for (Box box : isoFile.getBoxes()) {
079                if (BOX_MOOV.equals(box.getType())) {
080                    break;
081                }
082                offset += box.getSize();
083            }
084            boolean correctOffset = needsOffsetCorrection(isoFile);
085            // Create structure or just navigate to Apple List Box.
086            UserDataBox userDataBox;
087            if ((userDataBox = Path.getPath(moov, PATH_UDTA)) == null) {
088                userDataBox = new UserDataBox();
089                moov.addBox(userDataBox);
090            }
091            MetaBox metaBox;
092            if ((metaBox = Path.getPath(userDataBox, PATH_META)) == null) {
093                metaBox = new MetaBox();
094                HandlerBox hdlr = new HandlerBox();
095                hdlr.setHandlerType("mdir");
096                metaBox.addBox(hdlr);
097                userDataBox.addBox(metaBox);
098            }
099            AppleItemListBox ilst;
100            if ((ilst = Path.getPath(metaBox, PATH_ILST)) == null) {
101                ilst = new AppleItemListBox();
102                metaBox.addBox(ilst);
103            }
104            if (freeBox == null) {
105                freeBox = new FreeBox(128 * 1024);
106                metaBox.addBox(freeBox);
107            }
108            // Got Apple List Box
109            AppleNameBox nam;
110            if ((nam = Path.getPath(ilst, PATH_NAM)) == null) {
111                nam = new AppleNameBox();
112            }
113            nam.setDataCountry(0);
114            nam.setDataLanguage(0);
115            nam.setValue(watermark);
116            ilst.addBox(nam);
117
118            long sizeAfter = moov.getSize();
119            long diff = sizeAfter - sizeBefore;
120            // This is the difference of before/after
121            // can we compensate by resizing a Free Box we have found?
122            if (freeBox.getData().limit() > diff) {
123                // either shrink or grow!
124                freeBox.setData(ByteBuffer.allocate((int) (freeBox.getData().limit() - diff)));
125                sizeAfter = moov.getSize();
126                diff = sizeAfter - sizeBefore;
127            }
128            if (correctOffset && diff != 0) {
129                correctChunkOffsets(moov, diff);
130            }
131            BetterByteArrayOutputStream baos = new BetterByteArrayOutputStream();
132            moov.getBox(Channels.newChannel(baos));
133
134            File output = getOutputPath(inputFile, outputDir, watermark).toFile();
135            try (FileChannel read = new RandomAccessFile(videoFile, "r").getChannel();
136                    FileChannel write = new RandomAccessFile(output, "rw").getChannel()) {
137                if (diff == 0) {
138                    read.transferTo(0, read.size(), write);
139                    write.position(offset);
140                    write.write(ByteBuffer.wrap(baos.getBuffer(), 0, baos.size()));
141                } else {
142                    read.transferTo(0, offset, write);
143                    write.write(ByteBuffer.wrap(baos.getBuffer(), 0, baos.size()));
144                    read.transferTo(offset + baos.size(), read.size() - diff, write);
145                }
146                return output.toPath();
147            } finally {
148                baos.close();
149            }
150        } catch (IOException e) {
151            throw new IllegalArgumentException("shit happen", e);
152        }
153
154    }
155
156    protected boolean needsOffsetCorrection(IsoFile isoFile) {
157        if (Path.getPath(isoFile, PATH_MVEX) != null) {
158            // Fragmented files don't need a correction
159            return false;
160        } else {
161            // no correction needed if mdat is before moov as insert into moov want change the offsets of mdat
162            for (Box box : isoFile.getBoxes()) {
163                if (BOX_MOOV.equals(box.getType())) {
164                    return true;
165                }
166                if (BOX_MDAT.equals(box.getType())) {
167                    return false;
168                }
169            }
170            throw new RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense");
171        }
172    }
173
174    protected void correctChunkOffsets(MovieBox movieBox, long correction) {
175        List<ChunkOffsetBox> chunkOffsetBoxes = Path.getPaths((Box) movieBox, PATH_CHUNKS);
176        if (chunkOffsetBoxes.isEmpty()) {
177            chunkOffsetBoxes = Path.getPaths((Box) movieBox, PATH_CHUNKS_BIS);
178        }
179        for (ChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
180            long[] cOffsets = chunkOffsetBox.getChunkOffsets();
181            for (int i = 0; i < cOffsets.length; i++) {
182                cOffsets[i] += correction;
183            }
184        }
185    }
186
187    protected FreeBox findFreeBox(Container c) {
188        for (Box box : c.getBoxes()) {
189            if (box instanceof FreeBox) {
190                return (FreeBox) box;
191            }
192            if (box instanceof Container) {
193                FreeBox freeBox = findFreeBox((Container) box);
194                if (freeBox != null) {
195                    return freeBox;
196                }
197            }
198        }
199        return null;
200    }
201
202    protected static class BetterByteArrayOutputStream extends ByteArrayOutputStream {
203        byte[] getBuffer() {
204            return buf;
205        }
206    }
207}