package com.googlecode.mp4parser.stuff; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.mp4parser.Box; import org.mp4parser.Container; import org.mp4parser.IsoFile; import org.mp4parser.boxes.UnknownBox; import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox; import org.mp4parser.boxes.apple.AppleItemListBox; import org.mp4parser.boxes.apple.AppleNameBox; import org.mp4parser.boxes.apple.Utf8AppleDataBox; import org.mp4parser.boxes.iso14496.part12.*; import org.mp4parser.boxes.microsoft.XtraBox; import org.mp4parser.tools.Path; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; /** * Added by marwatk 3/1/15 */ public class MetaDataTool { public static final boolean DEBUG = true; public static final String WM_RATING_TAG = "WM/SharedUserRating"; public static final int WM_RATING_VALS[] = {0, 1, 25, 50, 75, 99}; public static final String WM_TAGS_TAG = "WM/Category"; //http://stackoverflow.com/questions/3389348/parse-any-date-in-java private static final HashMap<String, String> DATE_FORMAT_REGEXPS = new HashMap<String, String>() { { put("^\\d{8}$", "yyyyMMdd"); put("^\\d{1,2}-\\d{1,2}-\\d{4}$", "dd-MM-yyyy"); put("^\\d{4}-\\d{1,2}-\\d{1,2}$", "yyyy-MM-dd"); put("^\\d{1,2}/\\d{1,2}/\\d{4}$", "MM/dd/yyyy"); put("^\\d{4}/\\d{1,2}/\\d{1,2}$", "yyyy/MM/dd"); put("^\\d{1,2}\\s[a-z]{3}\\s\\d{4}$", "dd MMM yyyy"); put("^\\d{1,2}\\s[a-z]{4,}\\s\\d{4}$", "dd MMMM yyyy"); put("^\\d{12}$", "yyyyMMddHHmm"); put("^\\d{8}\\s\\d{4}$", "yyyyMMdd HHmm"); put("^\\d{1,2}-\\d{1,2}-\\d{4}\\s\\d{1,2}:\\d{2}$", "dd-MM-yyyy HH:mm"); put("^\\d{4}-\\d{1,2}-\\d{1,2}\\s\\d{1,2}:\\d{2}$", "yyyy-MM-dd HH:mm"); put("^\\d{1,2}/\\d{1,2}/\\d{4}\\s\\d{1,2}:\\d{2}$", "MM/dd/yyyy HH:mm"); put("^\\d{4}/\\d{1,2}/\\d{1,2}\\s\\d{1,2}:\\d{2}$", "yyyy/MM/dd HH:mm"); put("^\\d{1,2}\\s[a-z]{3}\\s\\d{4}\\s\\d{1,2}:\\d{2}$", "dd MMM yyyy HH:mm"); put("^\\d{1,2}\\s[a-z]{4,}\\s\\d{4}\\s\\d{1,2}:\\d{2}$", "dd MMMM yyyy HH:mm"); put("^\\d{14}$", "yyyyMMddHHmmss"); put("^\\d{8}\\s\\d{6}$", "yyyyMMdd HHmmss"); put("^\\d{1,2}-\\d{1,2}-\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "dd-MM-yyyy HH:mm:ss"); put("^\\d{4}-\\d{1,2}-\\d{1,2}\\s\\d{1,2}:\\d{2}:\\d{2}$", "yyyy-MM-dd HH:mm:ss"); put("^\\d{1,2}/\\d{1,2}/\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "MM/dd/yyyy HH:mm:ss"); put("^\\d{4}/\\d{1,2}/\\d{1,2}\\s\\d{1,2}:\\d{2}:\\d{2}$", "yyyy/MM/dd HH:mm:ss"); put("^\\d{1,2}\\s[a-z]{3}\\s\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "dd MMM yyyy HH:mm:ss"); put("^\\d{1,2}\\s[a-z]{4,}\\s\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "dd MMMM yyyy HH:mm:ss"); } }; private long originalUserDataSize = 0; private XtraBox xtraBox; private UserDataBox userDataBox; private MetaBox metaBox; private IsoFile isoFile; public MetaDataTool(String path) throws IOException { //The source I copied this from created 2 new files, a temp file and a target file //I'm not sure this is necessary, but maybe when you make changes it's edited in-place? //Anyway, just to be safe I'm keeping it so no operations are done on original file File videoFile = new File(path); if (!videoFile.exists()) throw new FileNotFoundException("File " + path + " not exists"); if (!videoFile.canWrite()) throw new IllegalStateException("No write permissions to file " + path); File tempFile = File.createTempFile("ChangeMetaData", ""); FileUtils.copyFile(videoFile, tempFile); tempFile.deleteOnExit(); isoFile = new IsoFile(tempFile.getAbsolutePath()); userDataBox = Path.getPath(isoFile, "/moov/udta"); if (userDataBox != null) { originalUserDataSize = userDataBox.getSize(); } } public static void main(String[] args) { if (args.length != 7 && args.length != 1) { System.err.println("Usage: java -jar metaDatTool.jar <inputFile> <outputFile> <title> <createDate> <userRating> <; separated tags> <gps coordinates>"); System.err.println(" Use * for any value to keep the existing value, use an empty value to delete the current value"); System.err.println(" Example: java -jar metaDataTool.jar myFile.mp4 newFile.mp4 \"New Title\" \"*\" 5 \"myTag 1;myTag 2\" \"\""); System.err.println(" This would retitle it, leave the create date alone, set the rating to 5 stars, "); System.err.println(" replace any tags with 'myTag 1' and 'myTag 2' and delete the existing GPS coordinates"); System.err.println("Other usage: java -jar metaDataToo.jar <inputFile>"); System.err.println(" Prints a dump of all tags in the file"); System.exit(1); } if (args.length == 1) { MetaDataTool mdt; try { mdt = new MetaDataTool(args[0]); mdt.dumpBoxes(); System.exit(0); } catch (IOException e) { e.printStackTrace(); } } int i = 0; String inFile = args[i++]; String outFile = args[i++]; String title = args[i++]; String createDate = args[i++]; String userRating = args[i++]; String tags = args[i++]; String gpsCoords = args[i++]; try { System.out.println("================= BEFORE ==================="); MetaDataTool mdt = new MetaDataTool(inFile); mdt.dumpBoxes(); if (!"*".equals(title)) { mdt.setTitle(title); } if (!"*".equals(createDate)) { Date inputDate = parseDate(createDate); mdt.setMediaCreateDate(inputDate); mdt.setMediaModificationDate(inputDate); } if (!"*".equals(userRating)) { if ("".equals(userRating)) { mdt.removeWindowsMediaTag(WM_RATING_TAG); } else { mdt.setWindowsMediaRating(Integer.valueOf(userRating)); } } if (!"*".equals(tags)) { if ("".equals(tags)) { mdt.removeWindowsMediaTag(WM_TAGS_TAG); } else { String tagsAr[] = tags.split(";"); mdt.setWindowsMediaTags(tagsAr); } } if (!"*".equals(gpsCoords)) { mdt.setGpsCoordinates(gpsCoords); } mdt.writeMp4(outFile); if (DEBUG) { mdt = new MetaDataTool(outFile); System.out.println("================= AFTER ==================="); mdt.dumpBoxes(); } } catch (Exception e) { e.printStackTrace(); } } private static String getIndentation(int indent) { char c[] = new char[indent]; for (int i = 0; i < indent; i++) { c[i] = ' '; } return new String(c); } private static void dumpBoxes(Container container, int indent) { String meInd = getIndentation(indent); String subInd = getIndentation(indent + 2); System.out.println(meInd + container.getClass().getName()); for (Box box : container.getBoxes()) { if (box instanceof Container) { dumpBoxes((Container) box, indent + 2); } else { try { if (box instanceof UnknownBox) { System.out.println(subInd + box.getClass().getName() + "[" + box.getSize() + "/" + box.getType() + "]:" + box.toString()); } else if (box instanceof Utf8AppleDataBox) { System.out.println(subInd + box.getClass().getName() + ": " + box.getType() + ": " + box.toString() + ": " + ((Utf8AppleDataBox) box).getValue()); } else { System.out.println(subInd + box.getClass().getName() + ": " + box.getType() + "[" + box.getSize() + "]: " + box.toString()); } } catch (Exception e) { System.err.println("Error parsing " + box.getClass().getSimpleName() + " box: " + e); e.printStackTrace(System.err); } } } } public static boolean needsOffsetCorrection(IsoFile isoFile) { if (Path.getPaths(isoFile, "mdat").size() > 1) { throw new RuntimeException("There might be the weird case that a file has two mdats. One before" + " moov and one after moov. That would need special handling therefore I just throw an " + "exception here. "); } if (Path.getPaths(isoFile, "moof").size() > 0) { throw new RuntimeException("Fragmented MP4 files need correction, too. (But I would need to look where)"); } for (Box box : isoFile.getBoxes()) { if ("mdat".equals(box.getType())) { return false; } if ("moov".equals(box.getType())) { return true; } } throw new RuntimeException("Hmmm - shouldn't happen"); } private static void correctChunkOffsets(IsoFile tempIsoFile, long correction) { List<SampleTableBox> sampleTableBoxes = Path.getPaths(tempIsoFile, "/moov[0]/trak/mdia[0]/minf[0]/stbl[0]"); for (SampleTableBox sampleTableBox : sampleTableBoxes) { List<Box> stblChildren = new ArrayList<Box>(sampleTableBox.getBoxes()); ChunkOffsetBox chunkOffsetBox = Path.getPath(sampleTableBox, "stco"); if (chunkOffsetBox == null) { stblChildren.remove(Path.getPath(sampleTableBox, "co64")); } stblChildren.remove(chunkOffsetBox); assert chunkOffsetBox != null; long[] cOffsets = chunkOffsetBox.getChunkOffsets(); for (int i = 0; i < cOffsets.length; i++) { cOffsets[i] += correction; } StaticChunkOffsetBox cob = new StaticChunkOffsetBox(); cob.setChunkOffsets(cOffsets); stblChildren.add(cob); sampleTableBox.setBoxes(stblChildren); } } public static void deleteQuietly(File f) { try { f.delete(); } catch (Exception ioe) { //ignore } } public static void closeQuietly(IsoFile input) { try { if (input != null) { input.close(); } } catch (IOException ioe) { // ignore } } public static Box getBox(Container outer, String type) { List<Box> list = getBoxes(outer, new String[]{type}); return list.get(0); } public static List<Box> getBoxes(Container outer, String types[], List<Box> list) { for (Box box : outer.getBoxes()) { for (String type : types) { if (box.getType().equals(type)) { list.add(box); } } if (box instanceof Container) { getBoxes((Container) box, types, list); } } return list; } public static List<Box> getBoxes(Container outer, String types[]) { List<Box> list = new ArrayList<Box>(); return getBoxes(outer, types, list); } public static String determineDateFormat(String dateString) { for (String regexp : DATE_FORMAT_REGEXPS.keySet()) { if (dateString.toLowerCase().matches(regexp)) { return DATE_FORMAT_REGEXPS.get(regexp); } } return null; // Unknown format. } public static Date parseDate(String dateString) throws ParseException { String formatString = determineDateFormat(dateString); if (formatString == null) { return null; } SimpleDateFormat sdf = new SimpleDateFormat(formatString); return sdf.parse(dateString); } public void setWindowsMediaRating(int rating) { //0-5 if (rating < 0 || rating > 5) { throw new RuntimeException("Invalid rating, 0-5 only"); } if (rating == 0) { removeWindowsMediaTag(WM_RATING_TAG); } else { setWindowsMediaLong(WM_RATING_TAG, WM_RATING_VALS[rating]); } } public void setWindowsMediaTags(String tags[]) { if (tags == null || tags.length == 0) { removeWindowsMediaTag(WM_TAGS_TAG); } else { setWindowsMediaStrings(WM_TAGS_TAG, tags); } } private void setMediaDate(Date date, boolean create) { List<Box> headers = getBoxes(isoFile, new String[]{MovieHeaderBox.TYPE, MediaHeaderBox.TYPE, TrackHeaderBox.TYPE}); boolean set = false; for (Box header : headers) { if (header instanceof MediaHeaderBox) { set = true; if (create) { ((MediaHeaderBox) header).setCreationTime(date); } else { ((MediaHeaderBox) header).setModificationTime(date); } } else if (header instanceof MovieHeaderBox) { set = true; if (create) { ((MovieHeaderBox) header).setCreationTime(date); } else { ((MovieHeaderBox) header).setModificationTime(date); } } else if (header instanceof TrackHeaderBox) { set = true; if (create) { ((TrackHeaderBox) header).setCreationTime(date); } else { ((TrackHeaderBox) header).setModificationTime(date); } } } setWindowsMediaDate("WM/EncodingTime", date); if (!set) { throw new RuntimeException("Can't yet add MovieHeaderBox or MediaHeaderBox and none were preset to set create and/or modify date"); } } public void setGpsCoordinates(String iso6709String) { AppleGPSCoordinatesBox coordBox = (AppleGPSCoordinatesBox) getBox(isoFile, AppleGPSCoordinatesBox.TYPE); if (coordBox == null) { UserDataBox udb = getUserDataBox(); coordBox = new AppleGPSCoordinatesBox(); udb.addBox(coordBox); } coordBox.setValue(iso6709String); } public void setMediaCreateDate(Date date) { setMediaDate(date, true); } public void setMediaModificationDate(Date date) { setMediaDate(date, false); } public void setTitle(String title) { AppleNameBox titleBox = (AppleNameBox) getBox(isoFile, AppleNameBox.TYPE); if (titleBox == null) { AppleItemListBox itemList = getItemListBox(); titleBox = new AppleNameBox(); itemList.addBox(titleBox); } titleBox.setValue(title); } private AppleItemListBox getItemListBox() { AppleItemListBox itemList = (AppleItemListBox) getBox(isoFile, AppleItemListBox.TYPE); if (itemList == null) { MetaBox mb = getMetaBox(); itemList = new AppleItemListBox(); mb.addBox(itemList); } return itemList; } @SuppressWarnings("deprecation") public void setMediaModificationDate(String date) { setMediaModificationDate(new Date(Date.parse(date))); //Deprecated, but also the easiest way to do this quickly } @SuppressWarnings("deprecation") public void setMediaCreateDate(String date) { try { setMediaCreateDate(new Date(Date.parse(date))); //Deprecated, but also the easiest way to do this quickly } catch (IllegalArgumentException e) { throw new RuntimeException("Unable to parse date '" + date + "'", e); } } public void setWindowsMediaDate(String tagName, Date dateVal) { XtraBox xb = getXtraBox(); xb.setTagValue(tagName, dateVal); } public void setWindowsMediaLong(String tagName, long longVal) { XtraBox xb = getXtraBox(); xb.setTagValue(tagName, longVal); } public void setWindowsMediaStrings(String tagName, String values[]) { XtraBox xb = getXtraBox(); xb.setTagValues(tagName, values); } public void removeWindowsMediaTag(String tagName) { XtraBox xb = getXtraBox(); xb.removeTag(tagName); } private UserDataBox getUserDataBox() { if (userDataBox == null) { userDataBox = new UserDataBox(); isoFile.getMovieBox().addBox(userDataBox); } return userDataBox; } private MetaBox getMetaBox() { if (metaBox == null) { UserDataBox ud = getUserDataBox(); metaBox = (MetaBox) getBox(ud, MetaBox.TYPE); if (metaBox == null) { metaBox = new MetaBox(); ud.addBox(metaBox); } } return metaBox; } private XtraBox getXtraBox() { if (xtraBox == null) { UserDataBox ud = getUserDataBox(); //Create user data box if necessary xtraBox = (XtraBox) getBox(ud, XtraBox.TYPE); if (xtraBox == null) { xtraBox = new XtraBox(); ud.addBox(xtraBox); } } return xtraBox; } public void writeMp4(String filename) throws IOException { long finalUserDataSize = 0; if (userDataBox != null) { finalUserDataSize = userDataBox.getSize(); } if (needsOffsetCorrection(isoFile)) { correctChunkOffsets(isoFile, finalUserDataSize - originalUserDataSize); } FileOutputStream videoFileOutputStream = null; try { videoFileOutputStream = new FileOutputStream(filename); isoFile.getBox(videoFileOutputStream.getChannel()); } finally { closeQuietly(isoFile); IOUtils.closeQuietly(videoFileOutputStream); } } public void dumpBoxes() { dumpBoxes(isoFile, 0); } }