More work on video scaling. dynamic.html now successfully includes movies in the UI.
This commit is contained in:
parent
9b584bd82e
commit
1e04937fab
12 changed files with 642 additions and 147 deletions
5
pom.xml
5
pom.xml
|
|
@ -36,6 +36,7 @@
|
|||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<configuration>
|
||||
<source>1.6</source>
|
||||
<target>1.6</target>
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-eclipse-plugin</artifactId>
|
||||
<version>2.8</version>
|
||||
<configuration>
|
||||
<wtpversion>1.5</wtpversion>
|
||||
<wtpContextName>photo</wtpContextName>
|
||||
|
|
@ -131,9 +133,6 @@
|
|||
<artifactId>je</artifactId>
|
||||
<version>4.0.103</version>
|
||||
</dependency>
|
||||
<!--
|
||||
<dependency><groupId>com.caucho</groupId><artifactId>resin</artifactId><version>3.1.8</version></dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
|
|
|
|||
14
src/main/java/org/forkalsrud/album/db/Chunk.java
Normal file
14
src/main/java/org/forkalsrud/album/db/Chunk.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.forkalsrud.album.db;
|
||||
|
||||
public class Chunk {
|
||||
|
||||
public byte[] bits;
|
||||
|
||||
public Chunk(int capacity) {
|
||||
this.bits = new byte[capacity];
|
||||
}
|
||||
|
||||
}
|
||||
106
src/main/java/org/forkalsrud/album/db/MovieDatabase.java
Normal file
106
src/main/java/org/forkalsrud/album/db/MovieDatabase.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.forkalsrud.album.db;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.forkalsrud.album.web.CachedImage;
|
||||
|
||||
import com.sleepycat.bind.tuple.TupleBinding;
|
||||
import com.sleepycat.bind.tuple.TupleInput;
|
||||
import com.sleepycat.bind.tuple.TupleOutput;
|
||||
import com.sleepycat.je.Database;
|
||||
import com.sleepycat.je.DatabaseConfig;
|
||||
import com.sleepycat.je.DatabaseEntry;
|
||||
import com.sleepycat.je.Environment;
|
||||
import com.sleepycat.je.OperationStatus;
|
||||
import com.sleepycat.je.Transaction;
|
||||
|
||||
/**
|
||||
* @author knut
|
||||
*
|
||||
*/
|
||||
public class MovieDatabase extends TupleBinding<Chunk> {
|
||||
|
||||
private static Charset UTF8 = Charset.forName("utf-8");
|
||||
|
||||
private Environment environment;
|
||||
private Database db;
|
||||
|
||||
public MovieDatabase(Environment environment) {
|
||||
|
||||
this.environment = environment;
|
||||
|
||||
String dbname = "movies";
|
||||
|
||||
DatabaseConfig databaseConfig = new DatabaseConfig();
|
||||
databaseConfig.setAllowCreate(true);
|
||||
databaseConfig.setTransactional(true);
|
||||
// perform other database configurations
|
||||
this.db = environment.openDatabase(null, dbname, databaseConfig);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
db.close();
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return db.count();
|
||||
}
|
||||
|
||||
public Chunk load(String key, int seq) {
|
||||
|
||||
DatabaseEntry data = new DatabaseEntry();
|
||||
Transaction txn = environment.beginTransaction(null, null);
|
||||
OperationStatus status = db.get(txn, key(key, seq), data, null);
|
||||
txn.commitNoSync();
|
||||
if (OperationStatus.SUCCESS.equals(status)) {
|
||||
return entryToObject(data);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void store(String key, int seq, Chunk chunk) {
|
||||
|
||||
DatabaseEntry data = new DatabaseEntry();
|
||||
objectToEntry(chunk, data);
|
||||
DatabaseEntry binKey = key(key, seq);
|
||||
Transaction txn = environment.beginTransaction(null, null);
|
||||
db.delete(txn, binKey);
|
||||
db.put(txn, binKey, data);
|
||||
txn.commitSync();
|
||||
}
|
||||
|
||||
|
||||
private DatabaseEntry key(String key, int seq) {
|
||||
DatabaseEntry returnValue = new DatabaseEntry();
|
||||
returnValue.setData((key + "#" + seq).getBytes(UTF8));
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Chunk entryToObject(TupleInput in) {
|
||||
|
||||
int version = in.readInt();
|
||||
if (version != 1) {
|
||||
throw new RuntimeException("I only understand version 1");
|
||||
}
|
||||
int lobLen = in.readInt();
|
||||
Chunk chunk = new Chunk(lobLen);
|
||||
in.read(chunk.bits, 0, lobLen);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void objectToEntry(Chunk chunk, TupleOutput out) {
|
||||
|
||||
out.writeInt(1); // version 1
|
||||
out.writeInt(chunk.bits.length);
|
||||
out.write(chunk.bits);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -80,4 +80,23 @@ public class Dimension {
|
|||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param size
|
||||
* @return
|
||||
*/
|
||||
public Dimension scale(String size) {
|
||||
Dimension outd;
|
||||
if (size.endsWith("h")) {
|
||||
int height = Integer.parseInt(size.substring(0, size.length() - 1));
|
||||
outd = scaleHeight(height);
|
||||
} else if (size.endsWith("w")) {
|
||||
int width = Integer.parseInt(size.substring(0, size.length() - 1));
|
||||
outd = scaleWidth(width);
|
||||
} else {
|
||||
int worh = Integer.parseInt(size);
|
||||
outd = scaled(worh);
|
||||
}
|
||||
return outd;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import java.util.List;
|
|||
|
||||
import org.forkalsrud.album.db.DirectoryDatabase;
|
||||
import org.forkalsrud.album.db.DirectoryProps;
|
||||
import org.forkalsrud.album.video.MovieCoder;
|
||||
|
||||
/**
|
||||
* @author knut
|
||||
|
|
@ -26,26 +27,32 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
|||
final static String CACHE_FILE = "cache.properties";
|
||||
final static String OVERRIDE_FILE = "album.properties";
|
||||
|
||||
DirectoryDatabase db;
|
||||
public interface ServiceApi {
|
||||
|
||||
public DirectoryDatabase getDirectoryDatabase();
|
||||
public DirectoryMetadataGenerator getMetadataGenerator();
|
||||
}
|
||||
|
||||
ServiceApi services;
|
||||
DirectoryProps cache;
|
||||
boolean childrenLoaded = false;
|
||||
Comparator<Entry> sort = null;
|
||||
Date earliest = null;
|
||||
|
||||
public DirectoryEntry(DirectoryDatabase db, File file) {
|
||||
public DirectoryEntry(ServiceApi api, File file) {
|
||||
super(file);
|
||||
if (!file.isDirectory()) {
|
||||
throw new RuntimeException("Use DirectoryEntry only for directories: " + file);
|
||||
}
|
||||
this.db = db;
|
||||
cache = db.load(file.getAbsolutePath());
|
||||
this.services = api;
|
||||
cache = this.services.getDirectoryDatabase().load(file.getAbsolutePath());
|
||||
if (cache == null) {
|
||||
cache = new DirectoryProps();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectoryEntry(DirectoryDatabase db, Entry parent, File file) {
|
||||
this(db, file);
|
||||
public DirectoryEntry(ServiceApi api, Entry parent, File file) {
|
||||
this(api, file);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
|
|
@ -114,8 +121,8 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
|||
|
||||
DirectoryProps generateCache() throws IOException, InterruptedException {
|
||||
|
||||
DirectoryProps props = new DirectoryMetadataGenerator(file).generate();
|
||||
db.store(file.getAbsolutePath(), props);
|
||||
DirectoryProps props = services.getMetadataGenerator().generate(file);
|
||||
services.getDirectoryDatabase().store(file.getAbsolutePath(), props);
|
||||
return props;
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +178,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
|||
String name = key.substring("dir.".length());
|
||||
boolean hidden = Boolean.parseBoolean(props.getProperty("dir." + name + ".hidden"));
|
||||
if (!hidden) {
|
||||
DirectoryEntry dir = new DirectoryEntry(db, this, new File(file, name));
|
||||
DirectoryEntry dir = new DirectoryEntry(services, this, new File(file, name));
|
||||
children.add(dir);
|
||||
if (name != null && name.equals(coverFileName)) {
|
||||
setThumbnail(dir.getThumbnail());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package org.forkalsrud.album.exif;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.forkalsrud.album.db.DirectoryDatabase;
|
||||
import org.forkalsrud.album.video.MovieCoder;
|
||||
|
||||
public class DirectoryEntryFactory implements DirectoryEntry.ServiceApi {
|
||||
|
||||
private DirectoryDatabase dirDb;
|
||||
private MovieCoder movieCoder;
|
||||
private DirectoryMetadataGenerator generator;
|
||||
|
||||
public DirectoryEntryFactory(DirectoryDatabase dirDb, MovieCoder movieCoder) {
|
||||
this.dirDb = dirDb;
|
||||
this.movieCoder = movieCoder;
|
||||
this.generator = new DirectoryMetadataGenerator(movieCoder);
|
||||
}
|
||||
|
||||
|
||||
public DirectoryEntry getEntry(File dir, DirectoryEntry parent) {
|
||||
return new DirectoryEntry(this, parent, dir);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DirectoryDatabase getDirectoryDatabase() {
|
||||
return dirDb;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DirectoryMetadataGenerator getMetadataGenerator() {
|
||||
return generator;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,16 +3,12 @@ package org.forkalsrud.album.exif;
|
|||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
|
|
@ -21,8 +17,8 @@ import javax.imageio.ImageReadParam;
|
|||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.forkalsrud.album.db.DirectoryProps;
|
||||
import org.forkalsrud.album.video.MovieCoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -39,19 +35,18 @@ public class DirectoryMetadataGenerator {
|
|||
final static String CACHE_FILE = "cache.properties";
|
||||
final static String OVERRIDE_FILE = "album.properties";
|
||||
|
||||
File dir;
|
||||
|
||||
MovieCoder movieCoder;
|
||||
|
||||
public DirectoryMetadataGenerator(File f) {
|
||||
this.dir = f;
|
||||
public DirectoryMetadataGenerator(MovieCoder movieCoder) {
|
||||
this.movieCoder = movieCoder;
|
||||
}
|
||||
|
||||
|
||||
public DirectoryProps generate() throws IOException, InterruptedException {
|
||||
public DirectoryProps generate(File dir) throws IOException, InterruptedException {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
DirectoryProps props = new DirectoryProps();
|
||||
generateFileEntries(props);
|
||||
generateFileEntries(dir, props);
|
||||
long end = System.currentTimeMillis();
|
||||
props.setTimestamp(end);
|
||||
log.info("Time to generate properties for " + dir.getPath() + ": " + (end - start)/1000d + " s");
|
||||
|
|
@ -59,7 +54,7 @@ public class DirectoryMetadataGenerator {
|
|||
}
|
||||
|
||||
|
||||
private void generateFileEntries(Properties props) throws IOException, InterruptedException {
|
||||
private void generateFileEntries(File dir, Properties props) throws IOException, InterruptedException {
|
||||
|
||||
File[] files = dir.listFiles();
|
||||
for (File f : files) {
|
||||
|
|
@ -86,7 +81,7 @@ public class DirectoryMetadataGenerator {
|
|||
Map<String, String> p = generateThumbnailProperties(f);
|
||||
addPropsForFile(props, f, p);
|
||||
} else if (name.endsWith(".mov") || name.endsWith(".MOV") || name.endsWith(".mp4") || name.endsWith(".avi")) {
|
||||
Map<String, String> p = generateVideoProperties(f);
|
||||
Map<String, String> p = movieCoder.generateVideoProperties(f);
|
||||
addPropsForFile(props, f, p);
|
||||
}
|
||||
}
|
||||
|
|
@ -184,100 +179,6 @@ public class DirectoryMetadataGenerator {
|
|||
return props;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The crazy Mac OSX does not even set the PATH to a reasonable value, so
|
||||
* we have to jump through hoops to guess where we may find the executables
|
||||
* for mplayer and friends.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private String findExecutableInShellPath(String name) throws IOException, InterruptedException {
|
||||
|
||||
String executableForName = name;
|
||||
ProcessBuilder pb = new ProcessBuilder(Arrays.asList(System.getenv("SHELL")));
|
||||
pb.redirectErrorStream(true); // send errors to stdout
|
||||
Process p = pb.start();
|
||||
PrintStream stdin = new PrintStream(p.getOutputStream());
|
||||
stdin.print("echo $PATH"); // This is still not entirely portable. Windows would like %PATH%
|
||||
stdin.close();
|
||||
InputStream stdout = p.getInputStream();
|
||||
String searchPath = IOUtils.toString(stdout);
|
||||
int returnStatus = p.waitFor();
|
||||
|
||||
String separator = System.getProperty("path.separator");
|
||||
if (searchPath != null && separator != null && !"".equals(separator)) {
|
||||
String[] elements = searchPath.split(separator);
|
||||
for (String path : elements) {
|
||||
|
||||
File executable = new File(path, name);
|
||||
if (executable.isFile()) {
|
||||
executableForName = executable.getAbsolutePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return executableForName;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Transcoding probably needs to go in at some point.
|
||||
* A suitable command line may be like the one below, as suggested by
|
||||
* http://rob.opendot.cl/index.php/useful-stuff/ffmpeg-x264-encoding-guide/
|
||||
* http://rob.opendot.cl/index.php/useful-stuff/encoding-a-flv-video-for-embedded-web-playback/
|
||||
*
|
||||
* Assuming video in is 1280x720 pixels resolution and we want to show 640x360
|
||||
* and we want an output bitrate about 1 Mbit/sec
|
||||
* we can do a one pass encoding like this:
|
||||
*
|
||||
* ffmpeg -i <infile> -aspect 1280:720 -s 640x360 -b 1000000 -crf 25 \
|
||||
* -vcodec libx264 -vpre knut_low \
|
||||
* -acodec libfaac -aq 100 \
|
||||
* <outfile>.mp4
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param f the movie file
|
||||
* @return a map with the following keys: orientation dimensions captureDate comment etag
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private Map<String, String> generateVideoProperties(File f) throws IOException, InterruptedException {
|
||||
|
||||
String mplayerExecutable = findExecutableInShellPath("mplayer");
|
||||
|
||||
Map<String, String> props = new HashMap<String, String>();
|
||||
ProcessBuilder pb = new ProcessBuilder().command(
|
||||
mplayerExecutable, "-vo", "null", "-ao", "null", "-frames", "0", "-identify", f.getAbsolutePath());
|
||||
pb.redirectErrorStream(false);
|
||||
Process p = pb.start();
|
||||
p.getOutputStream().close();
|
||||
List<String> lines = IOUtils.readLines(p.getInputStream());
|
||||
int returnStatus = p.waitFor();
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||
String width = "", height = "";
|
||||
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;
|
||||
}
|
||||
}
|
||||
props.put("type", "movie");
|
||||
props.put("orientation", "1");
|
||||
props.put("dimensions", new Dimension(width, height).toString());
|
||||
props.put("captureDate", sdf.format(new Date(f.lastModified())));
|
||||
props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode()));
|
||||
return props;
|
||||
}
|
||||
|
||||
private Date getExifDate(Directory exifDirectory, int tagName) throws MetadataException {
|
||||
|
||||
|
|
|
|||
369
src/main/java/org/forkalsrud/album/video/MovieCoder.java
Normal file
369
src/main/java/org/forkalsrud/album/video/MovieCoder.java
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
package org.forkalsrud.album.video;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.LineNumberReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.forkalsrud.album.db.MovieDatabase;
|
||||
import org.forkalsrud.album.db.Chunk;
|
||||
import org.forkalsrud.album.exif.Dimension;
|
||||
import org.forkalsrud.album.exif.Thumbnail;
|
||||
import org.forkalsrud.album.web.CachedImage;
|
||||
import org.forkalsrud.album.web.PictureScaler;
|
||||
|
||||
public class MovieCoder {
|
||||
|
||||
private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MovieCoder.class);
|
||||
|
||||
private String ffmpegExecutable;
|
||||
private String mplayerExecutable;
|
||||
private PictureScaler pictureScaler;
|
||||
private MovieDatabase movieDb;
|
||||
|
||||
public MovieCoder(PictureScaler pictureScaler, MovieDatabase movieDb) {
|
||||
this.pictureScaler = pictureScaler;
|
||||
this.movieDb = movieDb;
|
||||
}
|
||||
|
||||
public void init() throws Exception {
|
||||
this.ffmpegExecutable = findExecutableInShellPath("ffmpeg");
|
||||
this.mplayerExecutable = findExecutableInShellPath("mplayer");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The crazy Mac OSX does not even set the PATH to a reasonable value, so
|
||||
* we have to jump through hoops to guess where we may find the executables
|
||||
* for mplayer and friends.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private String findExecutableInShellPath(String name) throws IOException, InterruptedException {
|
||||
|
||||
String executableForName = name;
|
||||
ProcessBuilder pb = new ProcessBuilder(Arrays.asList(System.getenv("SHELL")));
|
||||
pb.redirectErrorStream(true); // send errors to stdout
|
||||
Process p = pb.start();
|
||||
PrintStream stdin = new PrintStream(p.getOutputStream());
|
||||
stdin.print("echo $PATH"); // This is still not entirely portable. Windows would like %PATH%
|
||||
stdin.close();
|
||||
InputStream stdout = p.getInputStream();
|
||||
String searchPath = IOUtils.toString(stdout);
|
||||
int returnStatus = p.waitFor();
|
||||
|
||||
String separator = System.getProperty("path.separator");
|
||||
if (searchPath != null && separator != null && !"".equals(separator)) {
|
||||
String[] elements = searchPath.split(separator);
|
||||
for (String path : elements) {
|
||||
|
||||
File executable = new File(path, name);
|
||||
if (executable.isFile()) {
|
||||
executableForName = executable.getAbsolutePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return executableForName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param f the movie file
|
||||
* @return a map with the following keys: orientation dimensions captureDate comment etag
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public Map<String, String> generateVideoProperties(File f) throws IOException, InterruptedException {
|
||||
|
||||
Map<String, String> props = new HashMap<String, String>();
|
||||
ProcessBuilder pb = new ProcessBuilder().command(
|
||||
mplayerExecutable, "-vo", "null", "-ao", "null", "-frames", "0", "-identify", f.getAbsolutePath());
|
||||
pb.redirectErrorStream(false);
|
||||
Process p = pb.start();
|
||||
p.getOutputStream().close();
|
||||
List<String> lines = IOUtils.readLines(p.getInputStream());
|
||||
int returnStatus = p.waitFor();
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||
String width = "", height = "";
|
||||
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;
|
||||
}
|
||||
}
|
||||
props.put("type", "movie");
|
||||
props.put("orientation", "1");
|
||||
props.put("dimensions", new Dimension(width, height).toString());
|
||||
props.put("captureDate", sdf.format(new Date(f.lastModified())));
|
||||
props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode()));
|
||||
return props;
|
||||
}
|
||||
|
||||
public File createTempDirectory() throws IOException {
|
||||
|
||||
final File temp = File.createTempFile("temp", Long.toString(System.nanoTime()));
|
||||
if (!temp.delete()) {
|
||||
throw new IOException("Could not delete temp file: " + temp.getAbsolutePath());
|
||||
}
|
||||
if (!temp.mkdir()) {
|
||||
throw new IOException("Could not create temp directory: " + temp.getAbsolutePath());
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
public void deleteTempDirectory(File dir) {
|
||||
for (File sub : dir.listFiles()) {
|
||||
if (sub.isDirectory()) {
|
||||
deleteTempDirectory(sub);
|
||||
}
|
||||
sub.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public CachedImage extractFrame(File file, int secondNo, Thumbnail thumbnail, String size) throws IOException, InterruptedException {
|
||||
|
||||
File tmpDir = createTempDirectory();
|
||||
try {
|
||||
File frame = new File(tmpDir, "00000001.jpg");
|
||||
ProcessBuilder pb = new ProcessBuilder().command(
|
||||
mplayerExecutable, "-vo", "jpeg:quality=95:outdir=" + tmpDir.getAbsolutePath(), "-ao", "null", "-frames", "1", file.getAbsolutePath());
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
p.getOutputStream().close();
|
||||
log.debug(IOUtils.toString(p.getInputStream()));
|
||||
int returnStatus = p.waitFor();
|
||||
|
||||
String key = file.getPath() + ":" + secondNo + ":" + size;
|
||||
CachedImage ci = pictureScaler.scalePicture(frame, thumbnail, size);
|
||||
return ci;
|
||||
} finally {
|
||||
deleteTempDirectory(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TailingOutputStream extends OutputStream {
|
||||
|
||||
int currentPos;
|
||||
int remainingBytes;
|
||||
OutputStream dst;
|
||||
|
||||
public TailingOutputStream(OutputStream dst, int startPos) {
|
||||
this.dst = dst;
|
||||
this.currentPos = startPos;
|
||||
this.remainingBytes = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) throws IOException {
|
||||
this.write(new byte[] { (byte) (b & 0xff) }, 0, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
dst.write(b, off, len);
|
||||
currentPos += len;
|
||||
remainingBytes -= len;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EncodingProcess implements Runnable {
|
||||
|
||||
final int chunkSize = 65536;
|
||||
int currentPos = 0;
|
||||
File file;
|
||||
Thumbnail thumbnail;
|
||||
Dimension targetSize;
|
||||
LinkedList<TailingOutputStream> consumers = new LinkedList<TailingOutputStream>();
|
||||
Chunk currentChunk = null;
|
||||
int chunkPos;
|
||||
int remainingCapacity;
|
||||
int chunkNo = 0;
|
||||
|
||||
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) {
|
||||
this.file = file;
|
||||
this.thumbnail = thumbnail;
|
||||
this.targetSize = size;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private void startNewChunk() {
|
||||
|
||||
this.currentChunk = new Chunk(chunkSize);
|
||||
this.chunkPos = 0;
|
||||
this.remainingCapacity = chunkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private void endChunk() {
|
||||
if (currentChunk != null && chunkPos > 0) {
|
||||
movieDb.store(file.getPath() + ":" + targetSize.getWidth(), chunkNo++, currentChunk);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public synchronized void setEncodedData(byte[] buf, int offset, int length) throws IOException {
|
||||
|
||||
for (TailingOutputStream consumer : consumers) {
|
||||
|
||||
long bytesToSkip = consumer.currentPos - this.currentPos;
|
||||
int i = offset;
|
||||
if (bytesToSkip > 0) {
|
||||
i += bytesToSkip;
|
||||
}
|
||||
int remaining = offset + length - i;
|
||||
int bytesToCopy = Math.min(remaining, consumer.remainingBytes);
|
||||
if (bytesToCopy > 0) {
|
||||
consumer.write(buf, i, bytesToCopy);
|
||||
} else {
|
||||
// consumer.done();
|
||||
}
|
||||
}
|
||||
this.currentPos += length;
|
||||
}
|
||||
|
||||
/*
|
||||
* Transcoding probably needs to go in at some point.
|
||||
* A suitable command line may be like the one below, as suggested by
|
||||
* http://rob.opendot.cl/index.php/useful-stuff/ffmpeg-x264-encoding-guide/
|
||||
* http://rob.opendot.cl/index.php/useful-stuff/encoding-a-flv-video-for-embedded-web-playback/
|
||||
*
|
||||
* Assuming video in is 1280x720 pixels resolution and we want to show 640x360
|
||||
* and we want an output bitrate about 1 Mbit/sec
|
||||
* we can do a one pass encoding like this:
|
||||
*
|
||||
* ffmpeg -i <infile> -aspect 1280:720 -s 640x360 -b 1000000 -crf 25 \
|
||||
* -vcodec libx264 -vpre knut_low \
|
||||
* -acodec libfaac -aq 100 \
|
||||
* <outfile>.mp4
|
||||
*/
|
||||
|
||||
public void run() {
|
||||
|
||||
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()),
|
||||
"-b", "600k",
|
||||
"-acodec", "libmp3lame", "-ar", "22050", "-vcodec", "flv",
|
||||
"-g", "150", "-cmp", "2", "-subcmp", "2", "-mbd", "2",
|
||||
"-flags", "+aic+cbp+mv0+mv4", "-trellis", "1",
|
||||
"-f", "flv",
|
||||
"-");
|
||||
|
||||
pb.redirectErrorStream(false);
|
||||
Process p = pb.start();
|
||||
p.getOutputStream().close();
|
||||
InputStream movieStream = p.getInputStream();
|
||||
InputStream diagnostic = p.getErrorStream();
|
||||
new Thread(new ErrorStreamPumper(diagnostic)).start();
|
||||
|
||||
int len;
|
||||
startNewChunk();
|
||||
while ((len = movieStream.read(currentChunk.bits, chunkPos, remainingCapacity)) > 0) {
|
||||
|
||||
setEncodedData(currentChunk.bits, chunkPos, len);
|
||||
chunkPos += len;
|
||||
remainingCapacity -= len;
|
||||
|
||||
if (remainingCapacity == 0) {
|
||||
endChunk();
|
||||
startNewChunk();
|
||||
}
|
||||
}
|
||||
endChunk();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
public void streamTo(OutputStream out) {
|
||||
consumers.add(new TailingOutputStream(out, 0));
|
||||
run();
|
||||
consumers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorStreamPumper implements Runnable {
|
||||
|
||||
InputStream is;
|
||||
public ErrorStreamPumper(InputStream is) {
|
||||
this.is = is;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
LineNumberReader lnr = new LineNumberReader(new InputStreamReader(is));
|
||||
String line;
|
||||
while ((line = lnr.readLine()) != null) {
|
||||
System.err.println(line);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HashMap<String, EncodingProcess> encodingsInProgress;
|
||||
|
||||
|
||||
|
||||
public void stream(File file, Thumbnail thumbnail, String size, OutputStream out) throws IOException, InterruptedException {
|
||||
|
||||
Dimension targetSize = thumbnail.getSize().scale(size);
|
||||
// new EncodingProcess(file, thumbnail, targetSize).streamTo(out);
|
||||
String key = file.getPath() + ":" + targetSize.getWidth();
|
||||
int chunkNo = 0;
|
||||
boolean done = false;
|
||||
while (!done) {
|
||||
Chunk chunk = movieDb.load(key, chunkNo++);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
out.write(chunk.bits);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,9 +6,6 @@ import java.io.FileOutputStream;
|
|||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
|
|
@ -27,13 +24,16 @@ import javax.servlet.http.HttpServletResponse;
|
|||
|
||||
import org.apache.log4j.PropertyConfigurator;
|
||||
import org.forkalsrud.album.db.DirectoryDatabase;
|
||||
import org.forkalsrud.album.db.MovieDatabase;
|
||||
import org.forkalsrud.album.db.ThumbnailDatabase;
|
||||
import org.forkalsrud.album.exif.DirectoryEntry;
|
||||
import org.forkalsrud.album.exif.DirectoryEntryFactory;
|
||||
import org.forkalsrud.album.exif.Entry;
|
||||
import org.forkalsrud.album.exif.FileEntry;
|
||||
import org.forkalsrud.album.exif.SearchEngine;
|
||||
import org.forkalsrud.album.exif.SearchResults;
|
||||
import org.forkalsrud.album.exif.Thumbnail;
|
||||
import org.forkalsrud.album.video.MovieCoder;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import com.sleepycat.je.Environment;
|
||||
|
|
@ -101,8 +101,11 @@ public class AlbumServlet
|
|||
private Environment environment;
|
||||
ThumbnailDatabase thumbDb;
|
||||
DirectoryDatabase dirDb;
|
||||
MovieDatabase movieDb;
|
||||
Timer timer;
|
||||
Entry cachedRootNode = null;
|
||||
MovieCoder movieCoder;
|
||||
DirectoryEntryFactory dirEntryFactory;
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
|
|
@ -149,8 +152,20 @@ public class AlbumServlet
|
|||
|
||||
thumbDb = new ThumbnailDatabase(environment);
|
||||
dirDb = new DirectoryDatabase(environment);
|
||||
movieDb = new MovieDatabase(environment);
|
||||
|
||||
|
||||
pictureScaler = new PictureScaler();
|
||||
|
||||
movieCoder = new MovieCoder(pictureScaler, movieDb);
|
||||
try {
|
||||
movieCoder.init();
|
||||
} catch (Exception e) {
|
||||
throw new ServletException("unable to locate movie helpers (mplayer and ffmpeg)", e);
|
||||
}
|
||||
|
||||
dirEntryFactory = new DirectoryEntryFactory(dirDb, movieCoder);
|
||||
|
||||
lastCacheFlushTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +226,18 @@ public class AlbumServlet
|
|||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".frame")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".frame".length());
|
||||
handleMovieFrame(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".movie")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".movie".length());
|
||||
handleMovie(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".edit")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length());
|
||||
handleEdit(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
|
|
@ -268,6 +295,35 @@ public class AlbumServlet
|
|||
}
|
||||
}
|
||||
|
||||
void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) {
|
||||
try {
|
||||
String size = req.getParameter("size");
|
||||
CachedImage cimg = movieCoder.extractFrame(entry.getPath(), 3, entry.getThumbnail(), size != null ? size : "250");
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
res.setContentType(cimg.mimeType);
|
||||
res.setContentLength(cimg.bits.length);
|
||||
res.getOutputStream().write(cimg.bits);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("sadness", ex);
|
||||
}
|
||||
}
|
||||
|
||||
void handleMovie(HttpServletRequest req, HttpServletResponse res, FileEntry entry) {
|
||||
try {
|
||||
String size = req.getParameter("size");
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
res.setContentType("application/octet-stream");
|
||||
movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream());
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("sadness", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||
try {
|
||||
Mapper mapper = new Mapper();
|
||||
|
|
@ -426,7 +482,7 @@ public class AlbumServlet
|
|||
if (base.equals(file.getAbsoluteFile())) {
|
||||
synchronized (this) {
|
||||
if (cachedRootNode == null) {
|
||||
cachedRootNode = new DirectoryEntry(dirDb, null, file);
|
||||
cachedRootNode = dirEntryFactory.getEntry(file, null);
|
||||
}
|
||||
}
|
||||
return cachedRootNode;
|
||||
|
|
|
|||
|
|
@ -47,8 +47,7 @@ public class PictureScaler {
|
|||
}
|
||||
|
||||
|
||||
synchronized PictureRequest getPictureRequest(File file, Thumbnail thumbnail, String size) {
|
||||
String key = file.getPath() + ":" + size;
|
||||
synchronized PictureRequest getPictureRequest(String key, File file, Thumbnail thumbnail, String size) {
|
||||
PictureRequest req = outstandingRequests.get(key);
|
||||
if (req == null) {
|
||||
req = new PictureRequest(key, file, thumbnail, size);
|
||||
|
|
@ -117,8 +116,11 @@ public class PictureScaler {
|
|||
|
||||
|
||||
public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) {
|
||||
|
||||
PictureRequest req = getPictureRequest(file, thumbnail, size);
|
||||
String key = file.getPath() + ":" + size;
|
||||
return scalePicture(key, file, thumbnail, size);
|
||||
}
|
||||
public CachedImage scalePicture(String key, File file, Thumbnail thumbnail, String size) {
|
||||
PictureRequest req = getPictureRequest(key, file, thumbnail, size);
|
||||
try {
|
||||
return req.waitForIt(30, TimeUnit.SECONDS);
|
||||
// } catch (TimeoutException toe) {
|
||||
|
|
@ -148,17 +150,7 @@ public class PictureScaler {
|
|||
CachedImage scalePictureReally(File file, Thumbnail thumbnail, String size) throws IOException {
|
||||
|
||||
Dimension orig = thumbnail.getSize();
|
||||
Dimension outd;
|
||||
if (size.endsWith("h")) {
|
||||
int height = Integer.parseInt(size.substring(0, size.length() - 1));
|
||||
outd = orig.scaleHeight(height);
|
||||
} else if (size.endsWith("w")) {
|
||||
int width = Integer.parseInt(size.substring(0, size.length() - 1));
|
||||
outd = orig.scaleWidth(width);
|
||||
} else {
|
||||
int worh = Integer.parseInt(size);
|
||||
outd = orig.scaled(worh);
|
||||
}
|
||||
Dimension outd = orig.scale(size);
|
||||
|
||||
/* In order to make the quality as good as possible we follow the advice from
|
||||
* http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ $(document).ready(function() {
|
|||
#end
|
||||
<div class="grid">
|
||||
<span class="name">$en.name</span><br/>
|
||||
#if($en.isFile())
|
||||
#if($en.type == "image")
|
||||
<div class="imgborder"><a class="ss" href="${base}${thpath}?size=${full}" rel="album" title="${en.name}"><img class="picture" src="${base}${thpath}?size=${thmb}" border="0" width="${dim.width}" height="${dim.height}"/></a></div>
|
||||
#else
|
||||
<div class="imgborder"><a class="dir" href="#navlink($en)" title="$!en.caption"><img class="picture" src="${base}${thpath}?size=${thmb}" border="0" width="${dim.width}" height="${dim.height}"/></a></div>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ $(function() {
|
|||
}
|
||||
|
||||
|
||||
$.getJSON('album/Photos.json', function(data, textStatus) {
|
||||
$.getJSON('album/photos.json', function(data, textStatus) {
|
||||
|
||||
$("#name").html(data.name);
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ $(function() {
|
|||
var gridDiv = $("<div class=\"grid\">\n"
|
||||
+ " <span class=\"name\">" + entry.name + "</span><br/>\n"
|
||||
+ " <div class=\"imgborder\"><a class=\"ss\" id=\"ent" + idx + "\" href=\"album" + entry.path + "?size=800\" title=\"" + entry.name + "\">"
|
||||
+ "<img class=\"picture\" src=\"album" + entry.path + "?size=250\" border=\"0\" width=\"" + dim.w + "\" height=\"" + dim.h + "\"/></a></div>\n"
|
||||
+ "<img class=\"picture\" src=\"album" + (entry.type == "movie" ? entry.path + ".frame" : entry.path) + "?size=250\" border=\"0\" width=\"" + dim.w + "\" height=\"" + dim.h + "\"/></a></div>\n"
|
||||
+ "<p id=\"" + entry.name + "\" class=\"caption\"></p>\n"
|
||||
+ "</div>\n");
|
||||
gridDiv.appendTo('body');
|
||||
|
|
@ -116,7 +116,7 @@ $(function() {
|
|||
|
||||
switch (entry.type) {
|
||||
case "movie":
|
||||
$("#ent" + idx).attr("rel", "album").attr("href", "raw/" + entry.name).fancybox({
|
||||
$("#ent" + idx).attr("rel", "album").attr("href", "/album" + entry.path + ".movie?size=640").fancybox({
|
||||
'titlePosition' : 'inside',
|
||||
'transitionIn' : 'elastic',
|
||||
'transitionOut' : 'elastic',
|
||||
|
|
@ -132,11 +132,7 @@ $(function() {
|
|||
'allowfullscreen' : 'true',
|
||||
'wmode' : 'transparent',
|
||||
'flashvars':
|
||||
"config={ 'clip': { 'url': 'raw/" + escape(entry.name) + "', 'provider': 'h264streaming' },\
|
||||
'plugins': {\
|
||||
'h264streaming': {\
|
||||
'url': 'assets/flowplayer/flowplayer.h264streaming-3.0.5.swf'\
|
||||
},\
|
||||
"config={ 'clip': { 'url': '/album" + escape(entry.path) + ".movie?size=640' },\
|
||||
'controls': {\
|
||||
'url': 'assets/flowplayer/flowplayer.controls-3.0.3.swf',\
|
||||
'backgroundColor': 'transparent', 'progressColor': 'transparent', 'bufferColor': 'transparent',\
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue