diff --git a/photos/video/IMG_0841.MOV b/photos/video/IMG_0841.MOV new file mode 100644 index 0000000..63ff917 Binary files /dev/null and b/photos/video/IMG_0841.MOV differ diff --git a/photos/video/IMG_0842.MOV b/photos/video/IMG_0842.MOV new file mode 100644 index 0000000..f4498f6 Binary files /dev/null and b/photos/video/IMG_0842.MOV differ diff --git a/photos/video/IMG_0843.MOV b/photos/video/IMG_0843.MOV new file mode 100644 index 0000000..a08e8b5 Binary files /dev/null and b/photos/video/IMG_0843.MOV differ diff --git a/photos/video/IMG_0844.MOV b/photos/video/IMG_0844.MOV new file mode 100644 index 0000000..87fd2fb Binary files /dev/null and b/photos/video/IMG_0844.MOV differ diff --git a/pom.xml b/pom.xml index ce5e676..61dc2f3 100644 --- a/pom.xml +++ b/pom.xml @@ -174,22 +174,22 @@ org.slf4j slf4j-api - 1.5.10 + 1.6.6 org.slf4j slf4j-log4j12 - 1.5.10 + 1.6.6 org.slf4j jul-to-slf4j - 1.5.10 + 1.6.6 org.slf4j jcl-over-slf4j - 1.5.10 + 1.6.6 commons-io @@ -198,6 +198,23 @@ jar compile + + org.codehaus.jackson + jackson-mapper-asl + 1.9.11 + + + org.eclipse.jetty + jetty-server + 7.6.8.v20121106 + test + + + org.eclipse.jetty + jetty-webapp + 7.6.8.v20121106 + test + diff --git a/src/main/java/org/forkalsrud/album/video/FlvMetadata.java b/src/main/java/org/forkalsrud/album/video/FlvMetadata.java index ea4438e..8f1aa93 100644 --- a/src/main/java/org/forkalsrud/album/video/FlvMetadata.java +++ b/src/main/java/org/forkalsrud/album/video/FlvMetadata.java @@ -9,9 +9,12 @@ import java.util.LinkedList; import java.util.List; import org.forkalsrud.album.exif.Dimension; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FlvMetadata { + + private static Logger log = LoggerFactory.getLogger(FlvMetadata.class); private abstract class Attr { @@ -98,7 +101,7 @@ public class FlvMetadata { public void read(InputStream in) throws FlvFormatException, IOException { int type = in.read(); if (type != 0) { - throw new FlvFormatException("Not a boolean: " + type); + throw new FlvFormatException("Not a number: " + type); } set(readDouble(in)); } @@ -167,7 +170,7 @@ public class FlvMetadata { throw new FlvFormatException("Not an object: " + type); } if (!in.markSupported()) { - throw new FlvFormatException("Need redahead"); + throw new FlvFormatException("Need readahead"); } List filePositions = null; List times = null; @@ -203,6 +206,14 @@ public class FlvMetadata { private StringAttr creator = new StringAttr("creator"); private StringAttr metadataCreator = new StringAttr("metadatacreator"); + private StringAttr majorBrand = new StringAttr("major_brand"); + private StringAttr minorVersion = new StringAttr("minor_version"); + private StringAttr compatibleBrands = new StringAttr("compatible_brands"); + private StringAttr creationTime = new StringAttr("creation_time"); + private StringAttr encoder = new StringAttr("encoder"); + private StringAttr encoderEng = new StringAttr( "encoder-eng"); + private StringAttr date = new StringAttr("date"); + private StringAttr dateEng = new StringAttr("date-eng"); private BooleanAttr hasKeyframes = new BooleanAttr("hasKeyframes"); private BooleanAttr hasVideo = new BooleanAttr("hasVideo"); @@ -247,6 +258,15 @@ public class FlvMetadata { attrs.add(creator); attrs.add(metadataCreator); + attrs.add(majorBrand); + attrs.add(minorVersion); + attrs.add(compatibleBrands); + attrs.add(creationTime); + attrs.add(date); + attrs.add(dateEng); + attrs.add(encoder); + attrs.add(encoderEng); + attrs.add(hasKeyframes); attrs.add(hasVideo); attrs.add(hasAudio); @@ -511,10 +531,10 @@ public class FlvMetadata { throw new FlvFormatException("Not an ECMA array: " + type); } int arrayLen = readEcmaArrayHeader(in); - for (int i = 0; i < arrayLen; i++) { + while (!isLookingAtEnd(in)) { readProperty(in); } - readEcmaArrayFooter(in); + readEcmaArrayFoter(in); } catch (FlvFormatException e) { LoggerFactory.getLogger(getClass()).error("invalid", e); } @@ -583,7 +603,7 @@ public class FlvMetadata { return len; } - protected void readEcmaArrayFooter(InputStream in) throws FlvFormatException, IOException { + protected void readEcmaArrayFoter(InputStream in) throws FlvFormatException, IOException { readObjectEnd(in); } @@ -607,10 +627,26 @@ public class FlvMetadata { String name = readString(in); Attr attr = findAttrByName(name); - if (attr == null) { - throw new FlvFormatException("Unknown attribute: " + name); - } - attr.read(in); + if (attr != null) { + attr.read(in); + } else { + log.warn("Unknown metadata property: " + name); + readUnknownProperty(in, name); + } + } + + protected void readUnknownProperty(InputStream in, String name) throws IOException, FlvFormatException { + + in.mark(1); + int type = in.read(); + in.reset(); + switch (type) { + case 2: + new StringAttr(name).read(in); + break; + default: + break; + } } private Attr findAttrByName(String name) { diff --git a/src/main/java/org/forkalsrud/album/video/MovieCoder.java b/src/main/java/org/forkalsrud/album/video/MovieCoder.java index 63576b4..6e66aa5 100644 --- a/src/main/java/org/forkalsrud/album/video/MovieCoder.java +++ b/src/main/java/org/forkalsrud/album/video/MovieCoder.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; +import org.codehaus.jackson.map.ObjectMapper; import org.forkalsrud.album.db.Chunk; import org.forkalsrud.album.db.MovieDatabase; import org.forkalsrud.album.exif.Dimension; @@ -27,6 +28,7 @@ public class MovieCoder { private String ffmpegExecutable; private String mplayerExecutable; + private String exiftoolExecutable; private PictureScaler pictureScaler; private MovieDatabase movieDb; private HashMap currentEncodings = new HashMap(); @@ -39,15 +41,16 @@ public class MovieCoder { public void init() throws Exception { ExecUtil util = new ExecUtil(); - this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg"); + if (this.ffmpegExecutable == null) { + this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg"); + } this.mplayerExecutable = util.findExecutableInShellPath("mplayer"); + this.exiftoolExecutable = util.findExecutableInShellPath("exiftool"); } - - /** * * @param f the movie file @@ -57,35 +60,159 @@ public class MovieCoder { */ public Map generateVideoProperties(File f) throws IOException, InterruptedException { + /* + * [{ + "SourceFile": "/home/erik/local/IMG_0837.mov", + "ExifToolVersion": 9.12, + "FileName": "IMG_0837.mov", + "Directory": "/home/erik/local", + "FileSize": "1438 kB", + "FileModifyDate": "2013:01:19 18:36:53-08:00", + "FileAccessDate": "2013:01:19 18:36:57-08:00", + "FileInodeChangeDate": "2013:01:19 18:36:53-08:00", + "FilePermissions": "rw-rw-r--", + "FileType": "MOV", + "MIMEType": "video/quicktime", + "MajorBrand": "Apple QuickTime (.MOV/QT)", + "MinorVersion": "0.0.0", + "CompatibleBrands": ["qt "], + "MovieDataSize": 1467141, + "MovieHeaderVersion": 0, + "ModifyDate": "2012:12:30 06:44:31", + "TimeScale": 600, + "Duration": "9.07 s", + "PreferredRate": 1, + "PreferredVolume": "100.00%", + "PreviewTime": "0 s", + "PreviewDuration": "0 s", + "PosterTime": "0 s", + "SelectionTime": "0 s", + "SelectionDuration": "0 s", + "CurrentTime": "0 s", + "NextTrackID": 3, + "TrackHeaderVersion": 0, + "TrackCreateDate": "2012:12:30 06:44:26", + "TrackModifyDate": "2012:12:30 06:44:31", + "TrackID": 1, + "TrackDuration": "9.05 s", + "TrackLayer": 0, + "TrackVolume": "100.00%", + "Balance": 0, + "AudioChannels": 1, + "AudioBitsPerSample": 16, + "AudioSampleRate": 44100, + "AudioFormat": "chan", + "MatrixStructure": "0 1 0 -1 0 0 540 0 1", + "ImageWidth": 960, + "ImageHeight": 540, + "CleanApertureDimensions": "960x540", + "ProductionApertureDimensions": "960x540", + "EncodedPixelsDimensions": "960x540", + "MediaHeaderVersion": 0, + "MediaCreateDate": "2012:12:30 06:44:26", + "MediaModifyDate": "2012:12:30 06:44:31", + "MediaTimeScale": 600, + "MediaDuration": "9.10 s", + "MediaLanguageCode": "und", + "GraphicsMode": "ditherCopy", + "OpColor": "32768 32768 32768", + "HandlerClass": "Data Handler", + "HandlerVendorID": "Apple", + "HandlerDescription": "Core Media Data Handler", + "CompressorID": "avc1", + "SourceImageWidth": 960, + "SourceImageHeight": 540, + "XResolution": 72, + "YResolution": 72, + "CompressorName": "H.264", + "BitDepth": 24, + "VideoFrameRate": 30, + "CameraIdentifier": "Back", + "FrameReadoutTime": "28512 microseconds", + "Make": "Apple", + "SoftwareVersion": "6.0.1", + "CreateDate": "2012:12:29 16:30:21-08:00", + "Model": "iPhone 4S", + "HandlerType": "Metadata Tags", + "Make-und-US": "Apple", + "CreationDate-und-US": "2012:12:29 16:30:21-08:00", + "Software-und-US": "6.0.1", + "Model-und-US": "iPhone 4S", + "AvgBitrate": "1.29 Mbps", + "ImageSize": "960x540", + "Rotation": 90 +}] + */ Map props = new HashMap(); ProcessBuilder pb = new ProcessBuilder().command( - mplayerExecutable, "-vo", "null", "-ao", "null", "-frames", "0", "-identify", f.getAbsolutePath()); + this.exiftoolExecutable, "-j", f.getAbsolutePath()); pb.redirectErrorStream(false); Process p = pb.start(); p.getOutputStream().close(); - List lines = IOUtils.readLines(p.getInputStream()); + + ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally + @SuppressWarnings("unchecked") + List> userDataList = mapper.readValue(p.getInputStream(), List.class); p.waitFor(); - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss"); - String width = "", height = "", length = ""; - for (String line : lines) { - int pos = line.indexOf('='); - if (pos >= 0) { - String name = line.substring(0, pos); - String value = line.substring(pos + 1); - if (name.equals("ID_VIDEO_WIDTH")) width = value; - if (name.equals("ID_VIDEO_HEIGHT")) height = value; - if (name.equals("ID_LENGTH")) length = value; - } - } + props.put("type", "movie"); - props.put("orientation", "1"); - props.put("dimensions", new Dimension(width, height).toString()); + + Map userData = userDataList.get(0); + + System.out.println(userData); + { + // The orientation is about flipping and rotating. Here is what an 'F' looks like + // on pictures with each orientation. + // + // 1 2 3 4 5 6 7 8 + // + // 888888 888888 88 88 8888888888 88 88 8888888888 + // 88 88 88 88 88 88 88 88 88 88 88 88 + // 8888 8888 8888 8888 88 8888888888 8888888888 88 + // 88 88 88 88 + // 88 88 888888 888888 + // + // The first four are obtained with only flipping X and/or Y + // The last four are obtained by rotating 90 degrees and then flipping X and/or Y. + // + String orientation; + Object rotationObj = userData.get("Rotation"); + if (rotationObj == null) { + orientation = "1"; + } else { + String rotation = rotationObj.toString(); + if ("0".equals(rotation)) { + orientation = "1"; + } else if ("90".equals(rotation)) { + orientation = "6"; + } else if ("180".equals(rotation)) { + orientation = "3"; + } else if ("270".equals(rotation)) { + orientation = "8"; + } else { + log.warn("unknown rotation: " + rotation + " for file " + f); + orientation = "1"; + } + } + props.put("orientation", orientation); + } + + props.put("dimensions", userData.get("ImageSize").toString()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss"); props.put("captureDate", sdf.format(new Date(f.lastModified()))); + props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode())); - props.put("length", length); + for (String prop : new String[] { "Duration", "MediaDuration", "PlayDuration" }) { + Object o = userData.get(prop); + if (o != null) { + props.put("length", o.toString().split(" ")[0]); + break; + } + } return props; } + public File createTempDirectory() throws IOException { final File temp = File.createTempFile("temp", Long.toString(System.nanoTime())); @@ -160,6 +287,7 @@ public class MovieCoder { private FlvFilter filter; private String dbKey; private long fileTimestamp; + private int orientation; public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) { this.file = file; @@ -171,6 +299,7 @@ public class MovieCoder { FlvMetadata extraMeta = new FlvMetadata(); extraMeta.setDuration(thumbnail.getDuration()); this.filter = new FlvFilter(this, extraMeta); + this.orientation = thumbnail.getOrientation(); } /* @@ -190,28 +319,49 @@ public class MovieCoder { @Override public void run() { + // -vf transpose=1 (rotate 90) + // -vf transpose=2 (rotate 270) + // -vf vflip,hflip (rotate 180) + + // 1 2 3 4 5 6 7 8 + // + // 888888 888888 88 88 8888888888 88 88 8888888888 + // 88 88 88 88 88 88 88 88 88 88 88 88 + // 8888 8888 8888 8888 88 8888888888 8888888888 88 + // 88 88 88 88 + // 88 88 888888 888888 + String vf = new String[] { + null, // 1 + "hflip", // 2 + "vflip,hflip", // 3 + "vflip", // 4 + "transpose=1,hflip", // 5 + "transpose=1", // 6 + "transpose=2,hflip", // 7 + "transpose=2", // 8 + }[orientation - 1]; + try { -/* - ProcessBuilder pb = new ProcessBuilder().command( - ffmpegExecutable, "-i", file.getAbsolutePath(), - "-aspect", (thumbnail.getSize().getWidth() + ":" + thumbnail.getSize().getHeight()), - "-s", (targetSize.getWidth() + "x" + targetSize.getHeight()), - "-b", "1000000", - "-crf", "25", -// "-vcodec", "libx264", "-vpre", "knut_low", -// "-acodec", "libfaac", "-aq", "100", - "-f", "mpeg", - "-"); -*/ - ProcessBuilder pb = new ProcessBuilder().command( - ffmpegExecutable, "-i", file.getAbsolutePath(), -// "-aspect", (thumbnail.getSize().getWidth() + ":" + thumbnail.getSize().getHeight()), - "-s", (targetSize.getWidth() + "x" + targetSize.getHeight()), - "-crf", "30", - "-acodec", "libmp3lame", "-ar", "22050", "-vcodec", "libx264", - "-g", "150", "-vpre", "medium", - "-f", "flv", - "-"); + + ArrayList command = new ArrayList(); + command.add(ffmpegExecutable); + command.add("-i"); + command.add(file.getAbsolutePath()); + command.add("-s"); + command.add(targetSize.getWidth() + "x" + targetSize.getHeight()); + command.add("-crf"); + command.add("30"); + command.add("-acodec"); command.add("libmp3lame"); + command.add("-ar"); command.add("22050"); + command.add("-vcodec"); command.add("libx264"); + command.add("-g"); command.add("150"); + if (vf != null) { + command.add("-vf"); + command.add(vf); + } + command.add("-f"); command.add("flv"); + command.add("-"); + ProcessBuilder pb = new ProcessBuilder(command); log.info(pb.command().toString()); pb.redirectErrorStream(false); @@ -470,4 +620,15 @@ public class MovieCoder { notify(); } } + + + public void setFfmpegPath(String property) { + + if (property != null) { + File program = new File(property); + if (program.canExecute()) { + this.ffmpegExecutable = property; + } + } + } } diff --git a/src/main/java/org/forkalsrud/album/web/AlbumServlet.java b/src/main/java/org/forkalsrud/album/web/AlbumServlet.java index fd542fb..635b66b 100644 --- a/src/main/java/org/forkalsrud/album/web/AlbumServlet.java +++ b/src/main/java/org/forkalsrud/album/web/AlbumServlet.java @@ -142,7 +142,17 @@ public class AlbumServlet basePrefix = "/" + base.getName(); String dbDirName = props.getProperty("dbdir"); - File dbDir = dbDirName != null ? new File(dbDirName) : new File(System.getProperty("java.io.tmpdir"), "album"); + File dbDir; + if (dbDirName != null) { + dbDir = new File(dbDirName); + } else { + dbDir = new File(System.getProperty("java.io.tmpdir"), "album"); + if (dbDir.isDirectory() && dbDir.canWrite()) { + for (File f : dbDir.listFiles()) { + f.delete(); + } + } + } dbDir.mkdirs(); EnvironmentConfig environmentConfig = new EnvironmentConfig(); @@ -158,6 +168,7 @@ public class AlbumServlet pictureScaler = new PictureScaler(); movieCoder = new MovieCoder(pictureScaler, movieDb); + movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path")); try { movieCoder.init(); } catch (Exception e) { @@ -320,7 +331,7 @@ public class AlbumServlet thumbDb.store(key, cimg); log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries"); } catch (Exception e) { - e.fillInStackTrace(); + //e.fillInStackTrace(); throw new RuntimeException("sadness", e); } } diff --git a/src/test/java/org/forkalsrud/album/Runner.java b/src/test/java/org/forkalsrud/album/Runner.java new file mode 100644 index 0000000..06eca31 --- /dev/null +++ b/src/test/java/org/forkalsrud/album/Runner.java @@ -0,0 +1,27 @@ +package org.forkalsrud.album; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.webapp.WebAppContext; + +public class Runner { + + /** + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception { + Server server = new Server(8080); + + WebAppContext context = new WebAppContext(); + context.setDescriptor("src/main/webapp/WEB-INF/web.xml"); + context.setResourceBase("src/main/webapp"); + context.setContextPath("/"); + context.setParentLoaderPriority(true); + + server.setHandler(context); + + server.start(); + server.join(); + } + +}