More work on video scaling. dynamic.html now successfully includes movies in the UI.

This commit is contained in:
Knut Forkalsrud 2011-03-26 21:41:54 -07:00
parent 9b584bd82e
commit 1e04937fab
12 changed files with 642 additions and 147 deletions

View file

@ -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>

View 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];
}
}

View 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);
}
}

View file

@ -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;
}
}

View file

@ -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());

View file

@ -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;
}
}

View file

@ -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);
}
}
@ -185,100 +180,6 @@ public class DirectoryMetadataGenerator {
}
/**
* 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 {
String dateStr = (String)exifDirectory.getObject(tagName);

View 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);
}
}
}

View file

@ -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;

View file

@ -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

View file

@ -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>

View file

@ -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',\