More work on movie streaming
This commit is contained in:
parent
a516f02b14
commit
494c0e5930
10 changed files with 402 additions and 162 deletions
|
|
@ -10,7 +10,7 @@ public class Chunk {
|
||||||
|
|
||||||
public Chunk(int capacity, int metaCapacity) {
|
public Chunk(int capacity, int metaCapacity) {
|
||||||
this.bits = new byte[capacity];
|
this.bits = new byte[capacity];
|
||||||
this.bits = new byte[0];
|
this.meta = new byte[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +72,19 @@ public class MovieDatabase extends TupleBinding<Chunk> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void delete(String key) {
|
||||||
|
|
||||||
|
int seq = 0;
|
||||||
|
OperationStatus status = OperationStatus.SUCCESS;
|
||||||
|
Transaction txn = environment.beginTransaction(null, null);
|
||||||
|
while (OperationStatus.SUCCESS.equals(status)) {
|
||||||
|
DatabaseEntry binKey = key(key, seq);
|
||||||
|
status = db.delete(txn, binKey);
|
||||||
|
}
|
||||||
|
txn.commitSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private DatabaseEntry key(String key, int seq) {
|
private DatabaseEntry key(String key, int seq) {
|
||||||
DatabaseEntry returnValue = new DatabaseEntry();
|
DatabaseEntry returnValue = new DatabaseEntry();
|
||||||
returnValue.setData((key + "#" + seq).getBytes(UTF8));
|
returnValue.setData((key + "#" + seq).getBytes(UTF8));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.forkalsrud.album.video;
|
||||||
|
|
||||||
|
public interface EncodingProcessListener {
|
||||||
|
|
||||||
|
public abstract void chunkAvailable(int chunkNo);
|
||||||
|
|
||||||
|
public abstract void codingFinished(int lastCunkNo);
|
||||||
|
|
||||||
|
}
|
||||||
51
src/main/java/org/forkalsrud/album/video/ExecUtil.java
Normal file
51
src/main/java/org/forkalsrud/album/video/ExecUtil.java
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.forkalsrud.album.video;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
|
public class ExecUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public 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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package org.forkalsrud.album.video;
|
package org.forkalsrud.album.video;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -12,6 +13,11 @@ public class FlvFilter extends OutputStream {
|
||||||
|
|
||||||
private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FlvFilter.class);
|
private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FlvFilter.class);
|
||||||
|
|
||||||
|
public interface FlvReceiver {
|
||||||
|
public void writeHeader(byte[]... data);
|
||||||
|
public void writeBody(byte[] data);
|
||||||
|
}
|
||||||
|
|
||||||
private final static int FLV_SIZE_TAGHEADER = 11;
|
private final static int FLV_SIZE_TAGHEADER = 11;
|
||||||
private final static int FLV_SIZE_TAGFOOTER = 4;
|
private final static int FLV_SIZE_TAGFOOTER = 4;
|
||||||
private final static int FLV_SIZE_FILEHEADER = 13;
|
private final static int FLV_SIZE_FILEHEADER = 13;
|
||||||
|
|
@ -20,8 +26,8 @@ public class FlvFilter extends OutputStream {
|
||||||
private final static int FLV_TAG_VIDEO = 9;
|
private final static int FLV_TAG_VIDEO = 9;
|
||||||
private final static int FLV_TAG_SCRIPTDATA = 18;
|
private final static int FLV_TAG_SCRIPTDATA = 18;
|
||||||
|
|
||||||
private OutputStream headerDst;
|
private boolean headerWritten = false;
|
||||||
private OutputStream bodyDst;
|
private FlvReceiver receiver;
|
||||||
|
|
||||||
private byte[] fileHeader = new byte[FLV_SIZE_FILEHEADER];
|
private byte[] fileHeader = new byte[FLV_SIZE_FILEHEADER];
|
||||||
private int byteCounter = 0;
|
private int byteCounter = 0;
|
||||||
|
|
@ -38,8 +44,14 @@ public class FlvFilter extends OutputStream {
|
||||||
private int currentTagSize;
|
private int currentTagSize;
|
||||||
private byte[] currentBox;
|
private byte[] currentBox;
|
||||||
|
|
||||||
private FlvMetadata metadata = new FlvMetadata();
|
private FlvMetadata metadata;
|
||||||
|
private FlvMetadata extraMetadata;
|
||||||
|
|
||||||
|
public FlvFilter(FlvReceiver receiver, FlvMetadata metadata) {
|
||||||
|
this.receiver = receiver;
|
||||||
|
this.metadata = new FlvMetadata();
|
||||||
|
this.extraMetadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receive some bytes of FLV output
|
* Receive some bytes of FLV output
|
||||||
|
|
@ -87,6 +99,11 @@ public class FlvFilter extends OutputStream {
|
||||||
currentDataSize = decodeUint24(currentBoxHeader, 1);
|
currentDataSize = decodeUint24(currentBoxHeader, 1);
|
||||||
currentTagTimestamp = decodeTimestamp(currentBoxHeader, 4);
|
currentTagTimestamp = decodeTimestamp(currentBoxHeader, 4);
|
||||||
currentTagSize = currentDataSize + FLV_SIZE_TAGHEADER + FLV_SIZE_TAGFOOTER;
|
currentTagSize = currentDataSize + FLV_SIZE_TAGHEADER + FLV_SIZE_TAGFOOTER;
|
||||||
|
if (currentTagType == 0) {
|
||||||
|
log.info("Encountered tag type zero, exiting.");
|
||||||
|
currentBox = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentBox = new byte[currentTagSize];
|
currentBox = new byte[currentTagSize];
|
||||||
for (int i = 0; i < FLV_SIZE_TAGHEADER; i++) {
|
for (int i = 0; i < FLV_SIZE_TAGHEADER; i++) {
|
||||||
currentBox[i] = currentBoxHeader[i];
|
currentBox[i] = currentBoxHeader[i];
|
||||||
|
|
@ -117,32 +134,63 @@ public class FlvFilter extends OutputStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void processBox() {
|
void processBox() throws IOException {
|
||||||
|
|
||||||
switch (currentTagType) {
|
// We expect one SCRIPTDATA box immediately following the file header
|
||||||
case FLV_TAG_VIDEO:
|
// Then only audio and video
|
||||||
int flags = decodeUint8(currentBox, FLV_SIZE_TAGHEADER);
|
if (currentTagType == FLV_TAG_SCRIPTDATA) {
|
||||||
boolean isKeyFrame = ((flags >> 4) & 0xf) == 1;
|
if (!headerWritten) {
|
||||||
metadata.addVideoFrame(currentTagPos, currentTagTimestamp, isKeyFrame);
|
incomingMetadataLength = currentTagSize;
|
||||||
break;
|
/*
|
||||||
case FLV_TAG_SCRIPTDATA:
|
TODO (knut 09 JUL 2011) generate (extra-)metadata here to enable user injection of metadata
|
||||||
incomingMetadataLength = currentTagSize;
|
metadata.read(currentBox, FLV_SIZE_TAGHEADER, currentTagSize - FLV_SIZE_TAGFOOTER);
|
||||||
break;
|
metadata.merge(extraMetadata);
|
||||||
case FLV_TAG_AUDIO:
|
generateHeader();
|
||||||
metadata.addAudioFrame(currentTagPos, currentTagTimestamp);
|
*/
|
||||||
break;
|
receiver.writeHeader(fileHeader, currentBox);
|
||||||
default:
|
headerWritten = true;
|
||||||
|
} else {
|
||||||
|
log.warn("SCRIPTDATA out of order");
|
||||||
|
receiver.writeBody(currentBox);
|
||||||
|
}
|
||||||
|
} else if (currentTagType == FLV_TAG_VIDEO || currentTagType == FLV_TAG_AUDIO) {
|
||||||
|
if (!headerWritten) {
|
||||||
|
log.error("SCRIPTDATA out of order");
|
||||||
|
receiver.writeHeader(fileHeader);
|
||||||
|
headerWritten = true;
|
||||||
|
}
|
||||||
|
if (currentTagType == FLV_TAG_VIDEO) {
|
||||||
|
int flags = decodeUint8(currentBox, FLV_SIZE_TAGHEADER);
|
||||||
|
boolean isKeyFrame = ((flags >> 4) & 0xf) == 1;
|
||||||
|
metadata.addVideoFrame(currentTagPos, currentTagTimestamp, isKeyFrame);
|
||||||
|
} else { // currentTagType == FLV_TAG_AUDIO
|
||||||
|
metadata.addAudioFrame(currentTagPos, currentTagTimestamp);
|
||||||
|
}
|
||||||
|
receiver.writeBody(currentBox);
|
||||||
|
metadata.setFileSize(currentTagPos + currentTagSize);
|
||||||
|
} else {
|
||||||
log.error("Unknown box type: " + currentTagType);
|
log.error("Unknown box type: " + currentTagType);
|
||||||
break;
|
receiver.writeBody(currentBox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void generateHeader(OutputStream out) throws IOException {
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
if (currentBox != null) {
|
||||||
|
throw new IOException("Premature end of file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generateHeader() throws IOException {
|
||||||
|
|
||||||
|
int metadataLength = metadata.calculateLength();
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(fileHeader.length + metadataLength);
|
||||||
out.write(fileHeader);
|
out.write(fileHeader);
|
||||||
metadata.setFileOffsetDelta(metadata.calculateLength() - incomingMetadataLength);
|
metadata.setFileOffsetDelta(metadataLength - incomingMetadataLength);
|
||||||
metadata.writeOnMetadata(out);
|
metadata.writeOnMetadata(out);
|
||||||
|
|
||||||
|
receiver.writeHeader(out.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.forkalsrud.album.exif.Dimension;
|
||||||
|
|
||||||
public class FlvMetadata {
|
public class FlvMetadata {
|
||||||
|
|
||||||
private abstract class Attr<T> {
|
private abstract class Attr<T> {
|
||||||
|
|
@ -305,13 +307,13 @@ public class FlvMetadata {
|
||||||
out.write(8); // ECMAArray
|
out.write(8); // ECMAArray
|
||||||
writeUint32(out, len);
|
writeUint32(out, len);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
private void writeFlvVariableArrayStart(OutputStream out, String name) throws IOException {
|
private void writeFlvVariableArrayStart(OutputStream out, String name) throws IOException {
|
||||||
|
|
||||||
writeFlvString(out, name);
|
writeFlvString(out, name);
|
||||||
out.write(3); // Variable Array
|
out.write(3); // Variable Array
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
private void writeFlvVariableArrayEnd(OutputStream out) throws IOException {
|
private void writeFlvVariableArrayEnd(OutputStream out) throws IOException {
|
||||||
out.write(0);
|
out.write(0);
|
||||||
out.write(0);
|
out.write(0);
|
||||||
|
|
@ -369,4 +371,51 @@ public class FlvMetadata {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDuration(String durationStr) {
|
||||||
|
this.duration.set(Double.parseDouble(durationStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDimension(Dimension d) {
|
||||||
|
this.width.set(Double.valueOf(d.getWidth()));
|
||||||
|
this.height.set(Double.valueOf(d.getHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void merge(FlvMetadata other) {
|
||||||
|
|
||||||
|
for (int i = 0; i < attrs.size(); i++) {
|
||||||
|
Attr their = other.attrs.get(i);
|
||||||
|
if (their.isPresent()) {
|
||||||
|
Attr our = this.attrs.get(i);
|
||||||
|
our.set(their.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Object get(String prop) {
|
||||||
|
if (prop == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Attr a : attrs) {
|
||||||
|
if (prop.equals(a.name) && a.isPresent()) {
|
||||||
|
return a.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setFileSize(int size) {
|
||||||
|
this.filesize.set(Double.valueOf(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void read(byte[] buf, int offset, int len) {
|
||||||
|
if (buf[offset] != 2) { //not string
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.LineNumberReader;
|
import java.io.LineNumberReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -39,48 +37,13 @@ public class MovieCoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() throws Exception {
|
public void init() throws Exception {
|
||||||
this.ffmpegExecutable = findExecutableInShellPath("ffmpeg");
|
|
||||||
this.mplayerExecutable = findExecutableInShellPath("mplayer");
|
ExecUtil util = new ExecUtil();
|
||||||
|
this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg");
|
||||||
|
this.mplayerExecutable = util.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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -165,17 +128,28 @@ public class MovieCoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static interface EncodingProcessListener {
|
|
||||||
|
/**
|
||||||
public abstract void chunkAvailable(int chunkNo);
|
* @param file
|
||||||
|
* @param thumbnail
|
||||||
public abstract void codingFinished(int lastCunkNo);
|
* @param targetSize
|
||||||
|
* @param key
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private synchronized EncodingProcess submitEncodingJob(File file,
|
||||||
|
Thumbnail thumbnail, Dimension targetSize, String key) {
|
||||||
|
EncodingProcess ep;
|
||||||
|
ep = new EncodingProcess(file, thumbnail, targetSize);
|
||||||
|
currentEncodings.put(key, ep);
|
||||||
|
return ep;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncodingProcess implements Runnable {
|
|
||||||
|
|
||||||
final int chunkSize = 65536;
|
|
||||||
|
|
||||||
|
class EncodingProcess implements Runnable, FlvFilter.FlvReceiver {
|
||||||
|
|
||||||
|
final int chunkSize = 4 * 65536;
|
||||||
File file;
|
File file;
|
||||||
Thumbnail thumbnail;
|
Thumbnail thumbnail;
|
||||||
Dimension targetSize;
|
Dimension targetSize;
|
||||||
|
|
@ -184,39 +158,19 @@ public class MovieCoder {
|
||||||
int chunkPos;
|
int chunkPos;
|
||||||
int remainingCapacity;
|
int remainingCapacity;
|
||||||
int chunkNo = 0;
|
int chunkNo = 0;
|
||||||
|
FlvFilter filter;
|
||||||
|
String dbKey;
|
||||||
|
|
||||||
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) {
|
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.thumbnail = thumbnail;
|
this.thumbnail = thumbnail;
|
||||||
this.targetSize = size;
|
this.targetSize = size;
|
||||||
|
this.dbKey = key(file, targetSize);
|
||||||
|
FlvMetadata meta = new FlvMetadata();
|
||||||
|
meta.setDuration(thumbnail.getDuration());
|
||||||
|
this.filter = new FlvFilter(this, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private void startNewChunk() {
|
|
||||||
|
|
||||||
this.currentChunk = new Chunk(chunkSize, 0);
|
|
||||||
this.chunkPos = 0;
|
|
||||||
this.remainingCapacity = chunkSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private void endChunk() {
|
|
||||||
|
|
||||||
if (currentChunk != null && chunkPos > 0) {
|
|
||||||
|
|
||||||
movieDb.store(file.getPath() + ":" + targetSize.getWidth(), chunkNo, currentChunk);
|
|
||||||
for (EncodingProcessListener listener : listeners) {
|
|
||||||
listener.chunkAvailable(chunkNo);
|
|
||||||
}
|
|
||||||
chunkNo++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* We might want to do H.264 instead of the H.263 codec typically used in FLV files.
|
* We might want to do H.264 instead of the H.263 codec typically used in FLV files.
|
||||||
* http://rob.opendot.cl/index.php/useful-stuff/ffmpeg-x264-encoding-guide/
|
* http://rob.opendot.cl/index.php/useful-stuff/ffmpeg-x264-encoding-guide/
|
||||||
|
|
@ -231,7 +185,6 @@ public class MovieCoder {
|
||||||
* -acodec libfaac -aq 100 \
|
* -acodec libfaac -aq 100 \
|
||||||
* <outfile>.mp4
|
* <outfile>.mp4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
|
|
@ -252,7 +205,7 @@ public class MovieCoder {
|
||||||
ffmpegExecutable, "-i", file.getAbsolutePath(),
|
ffmpegExecutable, "-i", file.getAbsolutePath(),
|
||||||
// "-aspect", (thumbnail.getSize().getWidth() + ":" + thumbnail.getSize().getHeight()),
|
// "-aspect", (thumbnail.getSize().getWidth() + ":" + thumbnail.getSize().getHeight()),
|
||||||
"-s", (targetSize.getWidth() + "x" + targetSize.getHeight()),
|
"-s", (targetSize.getWidth() + "x" + targetSize.getHeight()),
|
||||||
"-b", "600k",
|
"-b", "150k",
|
||||||
"-acodec", "libmp3lame", "-ar", "22050", "-vcodec", "flv",
|
"-acodec", "libmp3lame", "-ar", "22050", "-vcodec", "flv",
|
||||||
"-g", "150", "-cmp", "2", "-subcmp", "2", "-mbd", "2",
|
"-g", "150", "-cmp", "2", "-subcmp", "2", "-mbd", "2",
|
||||||
"-flags", "+aic+cbp+mv0+mv4", "-trellis", "1",
|
"-flags", "+aic+cbp+mv0+mv4", "-trellis", "1",
|
||||||
|
|
@ -268,34 +221,103 @@ public class MovieCoder {
|
||||||
InputStream diagnostic = p.getErrorStream();
|
InputStream diagnostic = p.getErrorStream();
|
||||||
new Thread(new ErrorStreamPumper(diagnostic)).start();
|
new Thread(new ErrorStreamPumper(diagnostic)).start();
|
||||||
|
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
int len;
|
int len;
|
||||||
startNewChunk();
|
while ((len = movieStream.read(buffer)) > 0) {
|
||||||
while ((len = movieStream.read(currentChunk.bits, chunkPos, remainingCapacity)) > 0) {
|
filter.write(buffer, 0, len);
|
||||||
|
|
||||||
chunkPos += len;
|
|
||||||
remainingCapacity -= len;
|
|
||||||
|
|
||||||
if (remainingCapacity == 0) {
|
|
||||||
endChunk();
|
|
||||||
startNewChunk();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
endChunk();
|
filter.flush();
|
||||||
|
endLastChunk();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("uh?", e);
|
log.error("uh?", e);
|
||||||
|
movieDb.delete(dbKey);
|
||||||
} finally {
|
} finally {
|
||||||
synchronized (MovieCoder.this) {
|
synchronized (MovieCoder.this) {
|
||||||
currentEncodings.remove(key(file, targetSize));
|
currentEncodings.remove(key(file, targetSize));
|
||||||
}
|
}
|
||||||
for (EncodingProcessListener listener : listeners) {
|
notifyListeners(chunkNo, true);
|
||||||
listener.codingFinished(chunkNo - 1);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeHeader(byte[]... data) {
|
||||||
|
int len = 0;
|
||||||
|
for (byte[] ba : data) {
|
||||||
|
len += ba.length;
|
||||||
|
}
|
||||||
|
Chunk chunk0 = new Chunk(len, 0);
|
||||||
|
int i = 0;
|
||||||
|
for (byte[] ba : data) {
|
||||||
|
System.arraycopy(ba, 0, chunk0.bits, i, ba.length);
|
||||||
|
i += ba.length;
|
||||||
|
}
|
||||||
|
movieDb.store(dbKey, 0, chunk0);
|
||||||
|
notifyListeners(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners(int chunkNo, boolean end) {
|
||||||
|
for (EncodingProcessListener listener : listeners) {
|
||||||
|
if (end) {
|
||||||
|
listener.codingFinished(chunkNo);
|
||||||
|
} else {
|
||||||
|
listener.chunkAvailable(chunkNo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeBody(byte[] data) {
|
||||||
|
writeBody(data, 0, data.length);
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void addListener(VideoStreamer videoStreamer) {
|
private void writeBody(byte[] data, int offset, int len) {
|
||||||
|
|
||||||
|
int remainingInput = len;
|
||||||
|
int inputPos = offset;
|
||||||
|
while (remainingInput > 0) {
|
||||||
|
if (currentChunk == null) {
|
||||||
|
startNewChunk();
|
||||||
|
}
|
||||||
|
int copyLen = Math.min(remainingCapacity, remainingInput);
|
||||||
|
System.arraycopy(data, inputPos, currentChunk.bits, chunkPos, copyLen);
|
||||||
|
chunkPos += copyLen;
|
||||||
|
remainingCapacity -= copyLen;
|
||||||
|
remainingInput -= copyLen;
|
||||||
|
if (remainingCapacity == 0) {
|
||||||
|
endChunk();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startNewChunk() {
|
||||||
|
|
||||||
|
this.chunkNo++;
|
||||||
|
this.currentChunk = new Chunk(chunkSize, 0);
|
||||||
|
this.chunkPos = 0;
|
||||||
|
this.remainingCapacity = chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endChunk() {
|
||||||
|
|
||||||
|
movieDb.store(dbKey, chunkNo, currentChunk);
|
||||||
|
currentChunk = null;
|
||||||
|
notifyListeners(chunkNo, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endLastChunk() {
|
||||||
|
|
||||||
|
if (currentChunk == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// reallocate
|
||||||
|
Chunk last = new Chunk(chunkPos, 0);
|
||||||
|
System.arraycopy(currentChunk.bits, 0, last.bits, 0, chunkPos);
|
||||||
|
movieDb.store(key(file, targetSize), chunkNo, last);
|
||||||
|
currentChunk = null;
|
||||||
|
notifyListeners(chunkNo, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addListener(EncodingProcessListener videoStreamer) {
|
||||||
listeners.add(videoStreamer);
|
listeners.add(videoStreamer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +339,7 @@ public class MovieCoder {
|
||||||
LineNumberReader lnr = new LineNumberReader(new InputStreamReader(is));
|
LineNumberReader lnr = new LineNumberReader(new InputStreamReader(is));
|
||||||
String line;
|
String line;
|
||||||
while ((line = lnr.readLine()) != null) {
|
while ((line = lnr.readLine()) != null) {
|
||||||
log.warn(line);
|
log.info(line);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("stderr?", e);
|
log.error("stderr?", e);
|
||||||
|
|
@ -330,63 +352,69 @@ public class MovieCoder {
|
||||||
// range requests etcetera
|
// range requests etcetera
|
||||||
public void stream(File file, Thumbnail thumbnail, String size, OutputStream out) throws IOException, InterruptedException {
|
public void stream(File file, Thumbnail thumbnail, String size, OutputStream out) throws IOException, InterruptedException {
|
||||||
|
|
||||||
new VideoStreamer().stream(file, thumbnail, size, out);
|
grabStream(file, thumbnail, size).stream(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String key(File file, Dimension size) {
|
private static String key(File file, Dimension size) {
|
||||||
|
|
||||||
return file.getPath() + ":" + size.getWidth();
|
return file.getPath() + ":" + size.getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
synchronized VideoStreamer grabStream(File file, Thumbnail thumbnail, String size) {
|
||||||
|
|
||||||
|
Dimension targetSize = thumbnail.getSize().scale(size);
|
||||||
|
String key = key(file, targetSize);
|
||||||
|
|
||||||
|
// See if we have (at least the beginning of) the file in the DB
|
||||||
|
// and whether file is currently in the process of being encoded
|
||||||
|
EncodingProcess ep = currentEncodings.get(key);
|
||||||
|
Chunk chunk = movieDb.load(key, 0);
|
||||||
|
|
||||||
|
// If neither we need to start the encoding process
|
||||||
|
if (chunk == null && ep == null) {
|
||||||
|
ep = submitEncodingJob(file, thumbnail, targetSize, key);
|
||||||
|
new Thread(ep).start();
|
||||||
|
}
|
||||||
|
return new VideoStreamer(key, ep, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class VideoStreamer implements EncodingProcessListener {
|
class VideoStreamer implements EncodingProcessListener {
|
||||||
|
|
||||||
int chunkNo = 0;
|
private int chunkNo = 0;
|
||||||
int lastChunkNo = -1;
|
private int lastChunkNo = -1;
|
||||||
|
private EncodingProcess ep;
|
||||||
|
private Chunk chunk;
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0) {
|
||||||
|
this.key = key;
|
||||||
|
this.ep = ep;
|
||||||
|
this.chunk = chunk0;
|
||||||
|
if (ep != null) {
|
||||||
|
ep.addListener(this);
|
||||||
|
}
|
||||||
|
// Range requests can hook in here
|
||||||
|
// if we have chunk metadata in chunk0 we can use that to compute the first
|
||||||
|
// chunk we want and set this.chunkNo accordingly. Otherwise (not likely
|
||||||
|
// to happen in practice) we just have to wait for each chunk to become
|
||||||
|
// available and count the number of bytes as we skip them.
|
||||||
|
//
|
||||||
|
// The end of the range is also a matter of counting the bytes.
|
||||||
|
}
|
||||||
|
|
||||||
private boolean done() {
|
private boolean done() {
|
||||||
return lastChunkNo >= 0 && chunkNo > lastChunkNo;
|
return lastChunkNo >= 0 && chunkNo > lastChunkNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stream(File file, Thumbnail thumbnail, String size, OutputStream out) throws IOException, InterruptedException {
|
public void stream(OutputStream out) throws IOException, InterruptedException {
|
||||||
|
|
||||||
Dimension targetSize = thumbnail.getSize().scale(size);
|
|
||||||
String key = key(file, targetSize);
|
|
||||||
|
|
||||||
log.info("being asked to stream " + file + " size=" + size);
|
|
||||||
|
|
||||||
EncodingProcess ep;
|
|
||||||
Chunk chunk;
|
|
||||||
|
|
||||||
synchronized (MovieCoder.this) {
|
|
||||||
// See if we have (at least the beginning of) the file in the DB
|
|
||||||
chunk = movieDb.load(key, chunkNo);
|
|
||||||
|
|
||||||
// See if file is currently in the process of being encoded
|
|
||||||
ep = currentEncodings.get(key);
|
|
||||||
|
|
||||||
// If neither we need to start the encoding process
|
|
||||||
if (chunk == null && ep == null) {
|
|
||||||
ep = new EncodingProcess(file, thumbnail, targetSize);
|
|
||||||
currentEncodings.put(key, ep);
|
|
||||||
ep.addListener(this);
|
|
||||||
} else if (chunk == null && ep != null) {
|
|
||||||
// Encoding started but no chunk yet written
|
|
||||||
ep.addListener(this);
|
|
||||||
} else if (chunk != null && ep == null) {
|
|
||||||
// Encoding done, just stream from DB
|
|
||||||
// Here we do not add ourselves as a listener, so we also don't wait down below
|
|
||||||
// On the other hand we should have updated metadata about the chunks in chunk 0
|
|
||||||
// so we should use that to set lastChunkNo
|
|
||||||
} else {
|
|
||||||
// Encoding partially done, stream what we have and wait for the rest to become available
|
|
||||||
ep.addListener(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (!done()) {
|
while (!done()) {
|
||||||
while (chunk != null) {
|
while (chunk != null) {
|
||||||
out.write(chunk.bits);
|
out.write(chunk.bits);
|
||||||
chunk = movieDb.load(key, chunkNo++);
|
chunk = movieDb.load(key, ++chunkNo);
|
||||||
}
|
}
|
||||||
if (!done()) {
|
if (!done()) {
|
||||||
if (ep != null) {
|
if (ep != null) {
|
||||||
|
|
@ -404,6 +432,7 @@ public class MovieCoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void chunkAvailable(int chunkNo) {
|
public synchronized void chunkAvailable(int chunkNo) {
|
||||||
notify();
|
notify();
|
||||||
|
|
@ -412,6 +441,7 @@ public class MovieCoder {
|
||||||
@Override
|
@Override
|
||||||
public synchronized void codingFinished(int lastCunkNo) {
|
public synchronized void codingFinished(int lastCunkNo) {
|
||||||
this.lastChunkNo = lastCunkNo;
|
this.lastChunkNo = lastCunkNo;
|
||||||
|
ep = null;
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -346,9 +346,10 @@ public class AlbumServlet
|
||||||
res.setStatus(HttpServletResponse.SC_OK);
|
res.setStatus(HttpServletResponse.SC_OK);
|
||||||
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
|
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
|
||||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||||
res.setContentType("application/octet-stream");
|
res.setContentType("video/x-flv");
|
||||||
movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream());
|
movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream());
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
log.error("darn", ex);
|
||||||
throw new RuntimeException("sadness", ex);
|
throw new RuntimeException("sadness", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package org.forkalsrud.album.video;
|
package org.forkalsrud.album.video;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|
@ -10,14 +9,32 @@ import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
public class FlvFilterTest extends TestCase {
|
public class FlvFilterTest extends TestCase {
|
||||||
|
|
||||||
|
private int bodyLen = 0;
|
||||||
|
|
||||||
public void testWrite() throws IOException {
|
public void testWrite() throws IOException {
|
||||||
|
|
||||||
|
final StringBuilder buf = new StringBuilder();
|
||||||
InputStream is = getClass().getResourceAsStream("/VideoAd.flv");
|
InputStream is = getClass().getResourceAsStream("/VideoAd.flv");
|
||||||
FlvFilter os = new FlvFilter();
|
FlvFilter.FlvReceiver receiver = new FlvFilter.FlvReceiver() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeHeader(byte[]... data) {
|
||||||
|
int len = 0;
|
||||||
|
for (byte[] ba : data) {
|
||||||
|
len += ba.length;
|
||||||
|
}
|
||||||
|
buf.append("HEADER[" + len + "]BODY[" + bodyLen + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeBody(byte[] data) {
|
||||||
|
bodyLen += data.length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FlvFilter os = new FlvFilter(receiver, new FlvMetadata());
|
||||||
IOUtils.copy(is, os);
|
IOUtils.copy(is, os);
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
os.generateHeader();
|
||||||
os.generateHeader(baos);
|
assertEquals("HEADER[252]BODY[0]HEADER[579]BODY[911684]", buf.toString());
|
||||||
assertEquals(579, baos.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.forkalsrud.album.video;
|
||||||
|
|
||||||
|
import org.forkalsrud.album.exif.Dimension;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
public class FlvMetadataTest extends TestCase {
|
||||||
|
|
||||||
|
public void testMerge() throws Exception {
|
||||||
|
|
||||||
|
FlvMetadata md1 = new FlvMetadata();
|
||||||
|
md1.setDuration("3.14");
|
||||||
|
md1.setDimension(new Dimension(5, 6));
|
||||||
|
|
||||||
|
FlvMetadata md2 = new FlvMetadata();
|
||||||
|
md2.merge(md1);
|
||||||
|
assertEquals(Double.valueOf("3.14"), md2.get("duration"));
|
||||||
|
assertEquals(Double.valueOf("5"), md2.get("width"));
|
||||||
|
assertEquals(Double.valueOf("6"), md2.get("height"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue