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}