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>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>2.3.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>1.6</source>
|
<source>1.6</source>
|
||||||
<target>1.6</target>
|
<target>1.6</target>
|
||||||
|
|
@ -49,6 +50,7 @@
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-eclipse-plugin</artifactId>
|
<artifactId>maven-eclipse-plugin</artifactId>
|
||||||
|
<version>2.8</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<wtpversion>1.5</wtpversion>
|
<wtpversion>1.5</wtpversion>
|
||||||
<wtpContextName>photo</wtpContextName>
|
<wtpContextName>photo</wtpContextName>
|
||||||
|
|
@ -131,9 +133,6 @@
|
||||||
<artifactId>je</artifactId>
|
<artifactId>je</artifactId>
|
||||||
<version>4.0.103</version>
|
<version>4.0.103</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--
|
|
||||||
<dependency><groupId>com.caucho</groupId><artifactId>resin</artifactId><version>3.1.8</version></dependency>
|
|
||||||
-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework</groupId>
|
<groupId>org.springframework</groupId>
|
||||||
<artifactId>spring-webmvc</artifactId>
|
<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;
|
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.DirectoryDatabase;
|
||||||
import org.forkalsrud.album.db.DirectoryProps;
|
import org.forkalsrud.album.db.DirectoryProps;
|
||||||
|
import org.forkalsrud.album.video.MovieCoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author knut
|
* @author knut
|
||||||
|
|
@ -26,26 +27,32 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
||||||
final static String CACHE_FILE = "cache.properties";
|
final static String CACHE_FILE = "cache.properties";
|
||||||
final static String OVERRIDE_FILE = "album.properties";
|
final static String OVERRIDE_FILE = "album.properties";
|
||||||
|
|
||||||
DirectoryDatabase db;
|
public interface ServiceApi {
|
||||||
|
|
||||||
|
public DirectoryDatabase getDirectoryDatabase();
|
||||||
|
public DirectoryMetadataGenerator getMetadataGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceApi services;
|
||||||
DirectoryProps cache;
|
DirectoryProps cache;
|
||||||
boolean childrenLoaded = false;
|
boolean childrenLoaded = false;
|
||||||
Comparator<Entry> sort = null;
|
Comparator<Entry> sort = null;
|
||||||
Date earliest = null;
|
Date earliest = null;
|
||||||
|
|
||||||
public DirectoryEntry(DirectoryDatabase db, File file) {
|
public DirectoryEntry(ServiceApi api, File file) {
|
||||||
super(file);
|
super(file);
|
||||||
if (!file.isDirectory()) {
|
if (!file.isDirectory()) {
|
||||||
throw new RuntimeException("Use DirectoryEntry only for directories: " + file);
|
throw new RuntimeException("Use DirectoryEntry only for directories: " + file);
|
||||||
}
|
}
|
||||||
this.db = db;
|
this.services = api;
|
||||||
cache = db.load(file.getAbsolutePath());
|
cache = this.services.getDirectoryDatabase().load(file.getAbsolutePath());
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
cache = new DirectoryProps();
|
cache = new DirectoryProps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DirectoryEntry(DirectoryDatabase db, Entry parent, File file) {
|
public DirectoryEntry(ServiceApi api, Entry parent, File file) {
|
||||||
this(db, file);
|
this(api, file);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,8 +121,8 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
||||||
|
|
||||||
DirectoryProps generateCache() throws IOException, InterruptedException {
|
DirectoryProps generateCache() throws IOException, InterruptedException {
|
||||||
|
|
||||||
DirectoryProps props = new DirectoryMetadataGenerator(file).generate();
|
DirectoryProps props = services.getMetadataGenerator().generate(file);
|
||||||
db.store(file.getAbsolutePath(), props);
|
services.getDirectoryDatabase().store(file.getAbsolutePath(), props);
|
||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +178,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
||||||
String name = key.substring("dir.".length());
|
String name = key.substring("dir.".length());
|
||||||
boolean hidden = Boolean.parseBoolean(props.getProperty("dir." + name + ".hidden"));
|
boolean hidden = Boolean.parseBoolean(props.getProperty("dir." + name + ".hidden"));
|
||||||
if (!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);
|
children.add(dir);
|
||||||
if (name != null && name.equals(coverFileName)) {
|
if (name != null && name.equals(coverFileName)) {
|
||||||
setThumbnail(dir.getThumbnail());
|
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.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
|
|
@ -21,8 +17,8 @@ import javax.imageio.ImageReadParam;
|
||||||
import javax.imageio.ImageReader;
|
import javax.imageio.ImageReader;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.forkalsrud.album.db.DirectoryProps;
|
import org.forkalsrud.album.db.DirectoryProps;
|
||||||
|
import org.forkalsrud.album.video.MovieCoder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -39,19 +35,18 @@ public class DirectoryMetadataGenerator {
|
||||||
final static String CACHE_FILE = "cache.properties";
|
final static String CACHE_FILE = "cache.properties";
|
||||||
final static String OVERRIDE_FILE = "album.properties";
|
final static String OVERRIDE_FILE = "album.properties";
|
||||||
|
|
||||||
File dir;
|
MovieCoder movieCoder;
|
||||||
|
|
||||||
|
|
||||||
public DirectoryMetadataGenerator(File f) {
|
public DirectoryMetadataGenerator(MovieCoder movieCoder) {
|
||||||
this.dir = f;
|
this.movieCoder = movieCoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public DirectoryProps generate() throws IOException, InterruptedException {
|
public DirectoryProps generate(File dir) throws IOException, InterruptedException {
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
DirectoryProps props = new DirectoryProps();
|
DirectoryProps props = new DirectoryProps();
|
||||||
generateFileEntries(props);
|
generateFileEntries(dir, props);
|
||||||
long end = System.currentTimeMillis();
|
long end = System.currentTimeMillis();
|
||||||
props.setTimestamp(end);
|
props.setTimestamp(end);
|
||||||
log.info("Time to generate properties for " + dir.getPath() + ": " + (end - start)/1000d + " s");
|
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();
|
File[] files = dir.listFiles();
|
||||||
for (File f : files) {
|
for (File f : files) {
|
||||||
|
|
@ -86,7 +81,7 @@ public class DirectoryMetadataGenerator {
|
||||||
Map<String, String> p = generateThumbnailProperties(f);
|
Map<String, String> p = generateThumbnailProperties(f);
|
||||||
addPropsForFile(props, f, p);
|
addPropsForFile(props, f, p);
|
||||||
} else if (name.endsWith(".mov") || name.endsWith(".MOV") || name.endsWith(".mp4") || name.endsWith(".avi")) {
|
} 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);
|
addPropsForFile(props, f, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,100 +179,6 @@ public class DirectoryMetadataGenerator {
|
||||||
return props;
|
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 {
|
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.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
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.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
@ -27,13 +24,16 @@ import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.apache.log4j.PropertyConfigurator;
|
import org.apache.log4j.PropertyConfigurator;
|
||||||
import org.forkalsrud.album.db.DirectoryDatabase;
|
import org.forkalsrud.album.db.DirectoryDatabase;
|
||||||
|
import org.forkalsrud.album.db.MovieDatabase;
|
||||||
import org.forkalsrud.album.db.ThumbnailDatabase;
|
import org.forkalsrud.album.db.ThumbnailDatabase;
|
||||||
import org.forkalsrud.album.exif.DirectoryEntry;
|
import org.forkalsrud.album.exif.DirectoryEntry;
|
||||||
|
import org.forkalsrud.album.exif.DirectoryEntryFactory;
|
||||||
import org.forkalsrud.album.exif.Entry;
|
import org.forkalsrud.album.exif.Entry;
|
||||||
import org.forkalsrud.album.exif.FileEntry;
|
import org.forkalsrud.album.exif.FileEntry;
|
||||||
import org.forkalsrud.album.exif.SearchEngine;
|
import org.forkalsrud.album.exif.SearchEngine;
|
||||||
import org.forkalsrud.album.exif.SearchResults;
|
import org.forkalsrud.album.exif.SearchResults;
|
||||||
import org.forkalsrud.album.exif.Thumbnail;
|
import org.forkalsrud.album.exif.Thumbnail;
|
||||||
|
import org.forkalsrud.album.video.MovieCoder;
|
||||||
import org.springframework.web.util.HtmlUtils;
|
import org.springframework.web.util.HtmlUtils;
|
||||||
|
|
||||||
import com.sleepycat.je.Environment;
|
import com.sleepycat.je.Environment;
|
||||||
|
|
@ -101,8 +101,11 @@ public class AlbumServlet
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
ThumbnailDatabase thumbDb;
|
ThumbnailDatabase thumbDb;
|
||||||
DirectoryDatabase dirDb;
|
DirectoryDatabase dirDb;
|
||||||
|
MovieDatabase movieDb;
|
||||||
Timer timer;
|
Timer timer;
|
||||||
Entry cachedRootNode = null;
|
Entry cachedRootNode = null;
|
||||||
|
MovieCoder movieCoder;
|
||||||
|
DirectoryEntryFactory dirEntryFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init()
|
public void init()
|
||||||
|
|
@ -149,8 +152,20 @@ public class AlbumServlet
|
||||||
|
|
||||||
thumbDb = new ThumbnailDatabase(environment);
|
thumbDb = new ThumbnailDatabase(environment);
|
||||||
dirDb = new DirectoryDatabase(environment);
|
dirDb = new DirectoryDatabase(environment);
|
||||||
|
movieDb = new MovieDatabase(environment);
|
||||||
|
|
||||||
|
|
||||||
pictureScaler = new PictureScaler();
|
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();
|
lastCacheFlushTime = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +226,18 @@ public class AlbumServlet
|
||||||
return;
|
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")) {
|
if (pathInfo.endsWith(".edit")) {
|
||||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length());
|
pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length());
|
||||||
handleEdit(req, res, (FileEntry)resolveEntry(pathInfo));
|
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) {
|
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||||
try {
|
try {
|
||||||
Mapper mapper = new Mapper();
|
Mapper mapper = new Mapper();
|
||||||
|
|
@ -426,7 +482,7 @@ public class AlbumServlet
|
||||||
if (base.equals(file.getAbsoluteFile())) {
|
if (base.equals(file.getAbsoluteFile())) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (cachedRootNode == null) {
|
if (cachedRootNode == null) {
|
||||||
cachedRootNode = new DirectoryEntry(dirDb, null, file);
|
cachedRootNode = dirEntryFactory.getEntry(file, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cachedRootNode;
|
return cachedRootNode;
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,7 @@ public class PictureScaler {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
synchronized PictureRequest getPictureRequest(File file, Thumbnail thumbnail, String size) {
|
synchronized PictureRequest getPictureRequest(String key, File file, Thumbnail thumbnail, String size) {
|
||||||
String key = file.getPath() + ":" + size;
|
|
||||||
PictureRequest req = outstandingRequests.get(key);
|
PictureRequest req = outstandingRequests.get(key);
|
||||||
if (req == null) {
|
if (req == null) {
|
||||||
req = new PictureRequest(key, file, thumbnail, size);
|
req = new PictureRequest(key, file, thumbnail, size);
|
||||||
|
|
@ -117,8 +116,11 @@ public class PictureScaler {
|
||||||
|
|
||||||
|
|
||||||
public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) {
|
public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) {
|
||||||
|
String key = file.getPath() + ":" + size;
|
||||||
PictureRequest req = getPictureRequest(file, thumbnail, 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 {
|
try {
|
||||||
return req.waitForIt(30, TimeUnit.SECONDS);
|
return req.waitForIt(30, TimeUnit.SECONDS);
|
||||||
// } catch (TimeoutException toe) {
|
// } catch (TimeoutException toe) {
|
||||||
|
|
@ -148,17 +150,7 @@ public class PictureScaler {
|
||||||
CachedImage scalePictureReally(File file, Thumbnail thumbnail, String size) throws IOException {
|
CachedImage scalePictureReally(File file, Thumbnail thumbnail, String size) throws IOException {
|
||||||
|
|
||||||
Dimension orig = thumbnail.getSize();
|
Dimension orig = thumbnail.getSize();
|
||||||
Dimension outd;
|
Dimension outd = orig.scale(size);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* In order to make the quality as good as possible we follow the advice from
|
/* 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
|
* http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ $(document).ready(function() {
|
||||||
#end
|
#end
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<span class="name">$en.name</span><br/>
|
<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>
|
<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
|
#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>
|
<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);
|
$("#name").html(data.name);
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ $(function() {
|
||||||
var gridDiv = $("<div class=\"grid\">\n"
|
var gridDiv = $("<div class=\"grid\">\n"
|
||||||
+ " <span class=\"name\">" + entry.name + "</span><br/>\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 + "\">"
|
+ " <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"
|
+ "<p id=\"" + entry.name + "\" class=\"caption\"></p>\n"
|
||||||
+ "</div>\n");
|
+ "</div>\n");
|
||||||
gridDiv.appendTo('body');
|
gridDiv.appendTo('body');
|
||||||
|
|
@ -116,7 +116,7 @@ $(function() {
|
||||||
|
|
||||||
switch (entry.type) {
|
switch (entry.type) {
|
||||||
case "movie":
|
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',
|
'titlePosition' : 'inside',
|
||||||
'transitionIn' : 'elastic',
|
'transitionIn' : 'elastic',
|
||||||
'transitionOut' : 'elastic',
|
'transitionOut' : 'elastic',
|
||||||
|
|
@ -132,11 +132,7 @@ $(function() {
|
||||||
'allowfullscreen' : 'true',
|
'allowfullscreen' : 'true',
|
||||||
'wmode' : 'transparent',
|
'wmode' : 'transparent',
|
||||||
'flashvars':
|
'flashvars':
|
||||||
"config={ 'clip': { 'url': 'raw/" + escape(entry.name) + "', 'provider': 'h264streaming' },\
|
"config={ 'clip': { 'url': '/album" + escape(entry.path) + ".movie?size=640' },\
|
||||||
'plugins': {\
|
|
||||||
'h264streaming': {\
|
|
||||||
'url': 'assets/flowplayer/flowplayer.h264streaming-3.0.5.swf'\
|
|
||||||
},\
|
|
||||||
'controls': {\
|
'controls': {\
|
||||||
'url': 'assets/flowplayer/flowplayer.controls-3.0.3.swf',\
|
'url': 'assets/flowplayer/flowplayer.controls-3.0.3.swf',\
|
||||||
'backgroundColor': 'transparent', 'progressColor': 'transparent', 'bufferColor': 'transparent',\
|
'backgroundColor': 'transparent', 'progressColor': 'transparent', 'bufferColor': 'transparent',\
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue