Finally cracked the nut on the FLV metadata processing.

This commit is contained in:
Knut Forkalsrud 2011-07-31 13:40:27 -07:00
parent 86a09acc4c
commit e039870adc
6 changed files with 453 additions and 107 deletions

View file

@ -1,5 +1,6 @@
package org.forkalsrud.album.video; package org.forkalsrud.album.video;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -14,7 +15,7 @@ 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 interface FlvReceiver {
public void writeHeader(byte[]... data); public void writeHeader(byte[] data);
public void writeBody(byte[] data); public void writeBody(byte[] data);
} }
@ -50,8 +51,8 @@ public class FlvFilter extends OutputStream {
public FlvFilter(FlvReceiver receiver, FlvMetadata metadata) { public FlvFilter(FlvReceiver receiver, FlvMetadata metadata) {
this.receiver = receiver; this.receiver = receiver;
this.metadata = metadata; this.metadata = new FlvMetadata();
this.extraMetadata = null; this.extraMetadata = metadata;
} }
/** /**
@ -141,22 +142,21 @@ public class FlvFilter extends OutputStream {
// Then only audio and video // Then only audio and video
if (currentTagType == FLV_TAG_SCRIPTDATA) { if (currentTagType == FLV_TAG_SCRIPTDATA) {
if (!headerWritten) { if (!headerWritten) {
incomingMetadataLength = currentTagSize; incomingMetadataLength = currentTagSize - (FLV_SIZE_TAGHEADER + FLV_SIZE_TAGFOOTER);
/* ByteArrayInputStream scriptData = new ByteArrayInputStream(currentBox, FLV_SIZE_TAGHEADER, incomingMetadataLength);
TODO (knut 09 JUL 2011) generate (extra-)metadata here to enable user injection of metadata metadata.read(scriptData);
metadata.read(currentBox, FLV_SIZE_TAGHEADER, currentTagSize - FLV_SIZE_TAGFOOTER);
metadata.merge(extraMetadata); metadata.merge(extraMetadata);
generateHeader(); generateHeaderChunk();
*/ // receiver.writeHeader(fileHeader, currentBox); // copy the input directly
receiver.writeHeader(fileHeader, currentBox);
headerWritten = true; headerWritten = true;
metadata.clearKeyframes();
} else { } else {
log.warn("SCRIPTDATA out of order"); log.warn("SCRIPTDATA out of order");
writeBody(); writeBody();
} }
} else if (currentTagType == FLV_TAG_VIDEO || currentTagType == FLV_TAG_AUDIO) { } else if (currentTagType == FLV_TAG_VIDEO || currentTagType == FLV_TAG_AUDIO) {
if (!headerWritten) { if (!headerWritten) {
log.error("SCRIPTDATA out of order"); log.error("Missing SCRIPTDATA");
receiver.writeHeader(fileHeader); receiver.writeHeader(fileHeader);
headerWritten = true; headerWritten = true;
} }
@ -190,16 +190,29 @@ public class FlvFilter extends OutputStream {
} }
} }
public void generateHeader() throws IOException { /**
* Writes out the header chunk which consists of a file header and a script data tag.
* The tag body comes from the metadata object. The file header is a copy of what we read in at the start.
* @throws IOException
*/
public void generateHeaderChunk() throws IOException {
metadata.setFileSize(fileHeader.length + incomingMetadataLength + bodyLen); metadata.setFileSize(fileHeader.length + FLV_SIZE_TAGHEADER + incomingMetadataLength + FLV_SIZE_TAGFOOTER + bodyLen);
int metadataLength = metadata.calculateLength(); int metadataLength = metadata.calculateLength();
ByteArrayOutputStream out = new ByteArrayOutputStream(fileHeader.length + metadataLength); ByteArrayOutputStream out = new ByteArrayOutputStream(fileHeader.length + FLV_SIZE_TAGHEADER + metadataLength + FLV_SIZE_TAGFOOTER);
// file header
out.write(fileHeader); out.write(fileHeader);
metadata.setFileSize(fileHeader.length + metadataLength + bodyLen); metadata.setFileSize(fileHeader.length + FLV_SIZE_TAGHEADER + metadataLength + FLV_SIZE_TAGFOOTER + bodyLen);
metadata.setFileOffsetDelta(metadataLength - incomingMetadataLength); metadata.setFileOffsetDelta(metadataLength - incomingMetadataLength);
metadata.writeOnMetadata(out, metadataLength); // tag header
out.write(FLV_TAG_SCRIPTDATA); // Script data tag 1 byte
metadata.writeUint24(out, metadataLength); //data size 3 bytes
metadata.writeTimestamp(out, 0); // timestamp 4 bytes
metadata.writeUint24(out, 0); // Stream ID 3 bytes
// body
metadata.writeScriptDataObject(out);
// tag footer
metadata.writeUint32(out, FLV_SIZE_TAGHEADER + metadataLength + FLV_SIZE_TAGFOOTER); // size of tag
receiver.writeHeader(out.toByteArray()); receiver.writeHeader(out.toByteArray());
} }
@ -219,6 +232,12 @@ public class FlvFilter extends OutputStream {
return ((ch1 << 16) + (ch2 << 8) + (ch3 << 0)); return ((ch1 << 16) + (ch2 << 8) + (ch3 << 0));
} }
int decodeUint16(byte[] buf, int offset) {
int ch1 = buf[offset + 0] & 0xff;
int ch2 = buf[offset + 1] & 0xff;
return ((ch1 << 8) + (ch2 << 0));
}
int decodeUint8(byte[] buf, int offset) { int decodeUint8(byte[] buf, int offset) {
return buf[offset] & 0xff; return buf[offset] & 0xff;
} }

View file

@ -0,0 +1,21 @@
package org.forkalsrud.album.video;
public class FlvFormatException extends Exception {
public FlvFormatException() {
super();
}
public FlvFormatException(String message) {
super(message);
}
public FlvFormatException(Throwable cause) {
super(cause);
}
public FlvFormatException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -2,12 +2,14 @@ package org.forkalsrud.album.video;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; 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; import org.forkalsrud.album.exif.Dimension;
import org.slf4j.LoggerFactory;
public class FlvMetadata { public class FlvMetadata {
@ -27,15 +29,13 @@ public class FlvMetadata {
this.value = value; this.value = value;
} }
public void write(OutputStream out) throws IOException { public abstract void write(OutputStream out) throws IOException;
if (name != null) {
writeFlvString(out, name);
}
}
public boolean isPresent() { public boolean isPresent() {
return present; return present;
} }
public abstract void read(InputStream in) throws FlvFormatException, IOException;
} }
private class StringAttr extends Attr<String> { private class StringAttr extends Attr<String> {
@ -46,10 +46,18 @@ public class FlvMetadata {
@Override @Override
public void write(OutputStream out) throws IOException { public void write(OutputStream out) throws IOException {
super.write(out);
out.write(2); // DataString out.write(2); // DataString
writeFlvString(out, value); writeFlvString(out, value);
} }
@Override
public void read(InputStream in) throws FlvFormatException, IOException {
int type = in.read();
if (type != 2) {
throw new FlvFormatException("Not a string: " + type);
}
set(readString(in));
}
} }
private class BooleanAttr extends Attr<Boolean> { private class BooleanAttr extends Attr<Boolean> {
@ -60,10 +68,18 @@ public class FlvMetadata {
@Override @Override
public void write(OutputStream out) throws IOException { public void write(OutputStream out) throws IOException {
super.write(out);
out.write(1); // Bool out.write(1); // Bool
out.write(value ? 1 : 0); out.write(value ? 1 : 0);
} }
@Override
public void read(InputStream in) throws FlvFormatException, IOException {
int type = in.read();
if (type != 1) {
throw new FlvFormatException("Not a boolean: " + type);
}
set(readBoolean(in));
}
} }
private class DoubleAttr extends Attr<Double> { private class DoubleAttr extends Attr<Double> {
@ -74,10 +90,17 @@ public class FlvMetadata {
@Override @Override
public void write(OutputStream out) throws IOException { public void write(OutputStream out) throws IOException {
super.write(out);
out.write(0); // NUMBER out.write(0); // NUMBER
long x = Double.doubleToLongBits(value); writeDouble(out, value);
writeUint64(out, x); }
@Override
public void read(InputStream in) throws FlvFormatException, IOException {
int type = in.read();
if (type != 0) {
throw new FlvFormatException("Not a boolean: " + type);
}
set(readDouble(in));
} }
} }
@ -89,10 +112,17 @@ public class FlvMetadata {
@Override @Override
public void write(OutputStream out) throws IOException { public void write(OutputStream out) throws IOException {
super.write(out);
out.write(0); // NUMBER out.write(0); // NUMBER
long x = Double.doubleToLongBits(value + filePositionDelta); writeDouble(out, value + filePositionDelta);
writeUint64(out, x); }
@Override
public void read(InputStream in) throws FlvFormatException, IOException {
int type = in.read();
if (type != 0) {
throw new FlvFormatException("Not a boolean: " + type);
}
set((int)readDouble(in) - filePositionDelta);
} }
} }
@ -109,7 +139,6 @@ public class FlvMetadata {
FileOffsetAttr offset = new FileOffsetAttr(null); FileOffsetAttr offset = new FileOffsetAttr(null);
DoubleAttr timestamp = new DoubleAttr(null); DoubleAttr timestamp = new DoubleAttr(null);
super.write(out);
out.write(3); // Variable Array out.write(3); // Variable Array
@ -127,6 +156,43 @@ public class FlvMetadata {
writeFlvVariableArrayEnd(out); writeFlvVariableArrayEnd(out);
} }
@Override
public void read(InputStream in) throws FlvFormatException, IOException {
FileOffsetAttr offset = new FileOffsetAttr(null);
DoubleAttr timestamp = new DoubleAttr(null);
int type = in.read();
if (type != 3) {
throw new FlvFormatException("Not an object: " + type);
}
if (!in.markSupported()) {
throw new FlvFormatException("Need redahead");
}
List<Integer> filePositions = null;
List<Double> times = null;
while (!isLookingAtEnd(in)) {
String propName = readString(in);
if ("filepositions".equals(propName)) {
filePositions = readValueArray(in, offset);
} else if ("times".equals(propName)) {
times = readValueArray(in, timestamp);
}
}
readObjectEnd(in);
if (times != null && filePositions != null) {
if (times.size() != filePositions.size()) {
throw new FlvFormatException("positions and times need to match up " + times.size() + " v.s. " + filePositions.size());
}
keyframes.clear();
for (int i = 0; i < times.size(); i++) {
addVideoFrame(filePositions.get(i), Math.round(times.get(i).floatValue()), true);
}
} else {
System.out.println("Missing filepositions (" + filePositions + ") or times (" + times + ")");
}
}
@Override @Override
public boolean isPresent() { public boolean isPresent() {
return !value.isEmpty(); return !value.isEmpty();
@ -157,6 +223,7 @@ public class FlvMetadata {
private DoubleAttr audiosize = new DoubleAttr("audiosize"); private DoubleAttr audiosize = new DoubleAttr("audiosize");
private DoubleAttr audiodatarate = new DoubleAttr("audiodatarate"); private DoubleAttr audiodatarate = new DoubleAttr("audiodatarate");
private DoubleAttr audiodelay = new DoubleAttr("audiodelay");
private DoubleAttr audiocodecid = new DoubleAttr("audiocodecid"); private DoubleAttr audiocodecid = new DoubleAttr("audiocodecid");
private DoubleAttr audiosamplerate = new DoubleAttr("audiosamplerate"); private DoubleAttr audiosamplerate = new DoubleAttr("audiosamplerate");
@ -198,6 +265,7 @@ public class FlvMetadata {
attrs.add(audiosize); attrs.add(audiosize);
attrs.add(audiodatarate); attrs.add(audiodatarate);
attrs.add(audiodelay);
attrs.add(audiocodecid); attrs.add(audiocodecid);
attrs.add(audiosamplerate); attrs.add(audiosamplerate);
@ -224,6 +292,10 @@ public class FlvMetadata {
return i; return i;
} }
public void clearKeyframes() {
keyframes.clear();
}
public void addVideoFrame(int filePos, int timestamp, boolean isKeyframe) { public void addVideoFrame(int filePos, int timestamp, boolean isKeyframe) {
hasVideo.set(true); hasVideo.set(true);
@ -243,8 +315,9 @@ public class FlvMetadata {
} }
public int calculateLength() throws IOException { public int calculateLength() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeOnMetadata(baos, 0); writeScriptDataObject(baos);
return baos.size(); return baos.size();
} }
@ -253,47 +326,28 @@ public class FlvMetadata {
this.filePositionDelta = fileOffsetDelta; this.filePositionDelta = fileOffsetDelta;
} }
public void writeOnMetadata(OutputStream out, int size) throws IOException { /**
* @param out
out.write(18); // Script data tag * @throws IOException
writeUint24(out, size - 15); //data size */
writeUint32(out, 0); // timestamp public void writeScriptDataObject(OutputStream out) throws IOException {
// ScriptDataObject // ScriptDataObject
out.write(2); out.write(2); // string
writeFlvEcmaArray(out, "onMetaData", length()); writeFlvString(out, "onMetaData");
writeFlvEcmaArray(out, length());
for (Attr a : attrs) { for (Attr a : attrs) {
if (a.isPresent()) { if (a.isPresent()) {
if (a.name != null) {
writeFlvString(out, a.name);
}
a.write(out); a.write(out);
} }
} }
writeFlvVariableArrayEnd(out); writeFlvVariableArrayEnd(out);
} }
void writeStringAttr(OutputStream out, String name, String value) throws IOException { void writeDouble(OutputStream out, double value) throws IOException {
if (name != null) {
writeFlvString(out, name);
}
out.write(2); // DataString
writeFlvString(out, value);
}
void writeBoolAttr(OutputStream out, String name, boolean value) throws IOException {
if (name != null) {
writeFlvString(out, name);
}
out.write(1); // Bool
out.write(value ? 1 : 0);
}
void writeDoubleAttr(OutputStream out, String name, double value) throws IOException {
if (name != null) {
writeFlvString(out, name);
}
out.write(0); // NUMBER
long x = Double.doubleToLongBits(value); long x = Double.doubleToLongBits(value);
writeUint64(out, x); writeUint64(out, x);
} }
@ -305,19 +359,11 @@ public class FlvMetadata {
out.write(bytes); out.write(bytes);
} }
private void writeFlvEcmaArray(OutputStream out, String name, int len) throws IOException { private void writeFlvEcmaArray(OutputStream out, int len) throws IOException {
writeFlvString(out, name);
out.write(8); // ECMAArray out.write(8); // ECMAArray
writeUint32(out, len); writeUint32(out, len);
} }
/*
private void writeFlvVariableArrayStart(OutputStream out, String name) throws IOException {
writeFlvString(out, name);
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);
@ -364,6 +410,14 @@ public class FlvMetadata {
out.write((value >> 0) & 0xff); out.write((value >> 0) & 0xff);
} }
void writeTimestamp(OutputStream out, int value) throws IOException {
out.write((value >> 16) & 0xff);
out.write((value >> 8) & 0xff);
out.write((value >> 0) & 0xff);
out.write((value >> 24) & 0xff);
}
private static class Keyframe { private static class Keyframe {
private int fileOffset; private int fileOffset;
@ -393,6 +447,14 @@ public class FlvMetadata {
} }
/**
* Takes all the values from the parameter "theirs".
* Corresponding values in this are overwritten.
* Other values in this are kept.
* The typical purpose is to fill in some missing metadata.
*
* @param other
*/
public void merge(FlvMetadata other) { public void merge(FlvMetadata other) {
for (int i = 0; i < attrs.size(); i++) { for (int i = 0; i < attrs.size(); i++) {
@ -423,12 +485,147 @@ public class FlvMetadata {
} }
public void read(byte[] buf, int offset, int len) { public void read(InputStream in) throws IOException {
if (buf[offset] != 2) { //not string
return; try {
int type = in.read();
if (type != 2) {
throw new FlvFormatException("Not a string: " + type);
}
String name = readString(in);
if (!"onMetaData".equalsIgnoreCase(name)) {
throw new FlvFormatException("Not \"onMetaData\": " + name);
}
type = in.read();
if (type != 8) {
throw new FlvFormatException("Not an ECMA array: " + type);
}
int arrayLen = readEcmaArrayHeader(in);
for (int i = 0; i < arrayLen; i++) {
readProperty(in);
}
readEcmaArrayFooter(in);
} catch (FlvFormatException e) {
LoggerFactory.getLogger(getClass()).error("invalid", e);
} }
} }
protected String readString(InputStream in) throws IOException {
int len = readUint16(in);
byte[] s = new byte[len];
in.read(s);
return new String(s, "UTF-8");
}
protected boolean readBoolean(InputStream in) throws IOException {
int val = in.read();
return val != 0;
}
protected double readDouble(InputStream in) throws IOException {
long val = readUint64(in);
return Double.longBitsToDouble(val);
}
protected long readUint64(InputStream in) throws IOException {
long ch1 = in.read();
long ch2 = in.read();
long ch3 = in.read();
long ch4 = in.read();
long ch5 = in.read();
long ch6 = in.read();
long ch7 = in.read();
long ch8 = in.read();
return ((ch1 << 56) + (ch2 << 48) + (ch3 << 40) + (ch4 << 32) + (ch5 << 24) + (ch6 << 16) + (ch7 << 8) + (ch8 << 0));
}
protected int readUint32(InputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}
protected int readUint24(InputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
return ((ch1 << 16) + (ch2 << 8) + (ch3 << 0));
}
protected int readUint16(InputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
return ((ch1 << 8) + (ch2 << 0));
}
protected int readTimestamp(InputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
return ((ch4 << 24) + (ch1 << 16) + (ch2 << 8) + (ch3 << 0));
}
protected int readEcmaArrayHeader(InputStream in) throws IOException {
int len = readUint32(in);
return len;
}
protected void readEcmaArrayFooter(InputStream in) throws FlvFormatException, IOException {
readObjectEnd(in);
}
protected void readObjectEnd(InputStream in) throws FlvFormatException, IOException {
byte[] word = new byte[3];
in.read(word);
if (word[0] != 0 || word[1] != 0 || word[2] != 9) {
throw new FlvFormatException("Not at end: " + word[0] + ", " + word[1] + ", " + word[2]);
}
}
protected boolean isLookingAtEnd(InputStream in) throws IOException {
byte[] word = new byte[3];
in.mark(word.length);
in.read(word);
in.reset();
return word[0] == 0 && word[1] == 0 && word[2] == 9;
}
protected void readProperty(InputStream in) throws FlvFormatException, IOException {
String name = readString(in);
Attr attr = findAttrByName(name);
if (attr == null) {
throw new FlvFormatException("Unknown attribute: " + name);
}
attr.read(in);
}
private Attr findAttrByName(String name) {
for (Attr a : this.attrs) {
if (name.equals(a.name)) {
return a;
}
}
return null;
}
protected <T> List<T> readValueArray(InputStream in, Attr<T> elementAttr) throws FlvFormatException, IOException {
int type = in.read();
if (type != 10) {
throw new FlvFormatException("Not a value array: " + type);
}
int len = readUint32(in);
ArrayList<T> list = new ArrayList<T>(len);
for (int i = 0; i < len; i++) {
elementAttr.read(in);
list.add(elementAttr.value);
}
return list;
}
public void setVideoCodecId(int videocodecid) { public void setVideoCodecId(int videocodecid) {
this.videocodecid.set(Double.valueOf(videocodecid)); this.videocodecid.set(Double.valueOf(videocodecid));
@ -445,4 +642,45 @@ public class FlvMetadata {
} }
public int getWidth() {
return width.isPresent() ? width.value.intValue() : 0;
}
public int getHeight() {
return height.isPresent() ? height.value.intValue() : 0;
}
@Override
public boolean equals(Object o) {
if (o == null || !this.getClass().equals(o.getClass())) {
return false;
}
FlvMetadata other = (FlvMetadata)o;
for (int i = 0; i < attrs.size(); i++) {
Attr their = other.attrs.get(i);
Attr our = this.attrs.get(i);
if (our.isPresent() && their.isPresent()) {
if (!our.value.equals(their.value)) {
return false;
}
}
if (our.isPresent() != their.isPresent()) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int x = 0;
for (Attr a : attrs) {
x *= 13;
x += a.present ? a.value.hashCode() : 7;
}
return x;
}
} }

View file

@ -151,7 +151,6 @@ public class MovieCoder {
private final int chunkSize = 4 * 65536; private final int chunkSize = 4 * 65536;
private File file; private File file;
private Thumbnail thumbnail;
private Dimension targetSize; private Dimension targetSize;
private ArrayList<EncodingProcessListener> listeners = new ArrayList<EncodingProcessListener>(); private ArrayList<EncodingProcessListener> listeners = new ArrayList<EncodingProcessListener>();
private Chunk currentChunk = null; private Chunk currentChunk = null;
@ -163,15 +162,11 @@ public class MovieCoder {
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.targetSize = size; this.targetSize = size;
this.dbKey = key(file, targetSize); this.dbKey = key(file, targetSize);
FlvMetadata meta = new FlvMetadata(); FlvMetadata extraMeta = new FlvMetadata();
meta.setDuration(thumbnail.getDuration()); extraMeta.setDuration(thumbnail.getDuration());
meta.setVideoCodecId(2); this.filter = new FlvFilter(this, extraMeta);
meta.setAudioCodecId(2);
meta.setFramerate(15); // TODO (knut 10 JUL 2011) Need to make sure the ffmpeg args match
this.filter = new FlvFilter(this, meta);
} }
/* /*
@ -233,7 +228,10 @@ public class MovieCoder {
endLastChunk(); endLastChunk();
// Generate header again to get updated metadata // Generate header again to get updated metadata
// TODO (knut 09 JUL 2011) Figure out why the generated header doesn't work // TODO (knut 09 JUL 2011) Figure out why the generated header doesn't work
filter.generateHeader(); // TODO (knut 30 JUL 2011) Add some metadata about the file in the first chunk of the file
// size of first chunk, size of last chunk, number number of chunks between first-last, size of chunks between first-last
// This will be used to determine the right chunk number from a given byte offset in a range oritented HTTP request
filter.generateHeaderChunk();
} catch (Exception e) { } catch (Exception e) {
log.error("uh?", e); log.error("uh?", e);
movieDb.delete(dbKey); movieDb.delete(dbKey);
@ -246,17 +244,10 @@ public class MovieCoder {
} }
@Override @Override
public void writeHeader(byte[]... data) { public void writeHeader(byte[] data) {
int len = 0; int len = data.length;
for (byte[] ba : data) {
len += ba.length;
}
Chunk chunk0 = new Chunk(len, 0); Chunk chunk0 = new Chunk(len, 0);
int i = 0; chunk0.bits = data;
for (byte[] ba : data) {
System.arraycopy(ba, 0, chunk0.bits, i, ba.length);
i += ba.length;
}
movieDb.store(dbKey, 0, chunk0); movieDb.store(dbKey, 0, chunk0);
notifyListeners(0, false); notifyListeners(0, false);
} }

View file

@ -18,12 +18,8 @@ public class FlvFilterTest extends TestCase {
FlvFilter.FlvReceiver receiver = new FlvFilter.FlvReceiver() { FlvFilter.FlvReceiver receiver = new FlvFilter.FlvReceiver() {
@Override @Override
public void writeHeader(byte[]... data) { public void writeHeader(byte[] data) {
int len = 0; buf.append("HEADER[" + data.length + "]BODY[" + bodyLen + "]");
for (byte[] ba : data) {
len += ba.length;
}
buf.append("HEADER[" + len + "]BODY[" + bodyLen + "]");
} }
@Override @Override
@ -33,8 +29,8 @@ public class FlvFilterTest extends TestCase {
}; };
FlvFilter os = new FlvFilter(receiver, new FlvMetadata()); FlvFilter os = new FlvFilter(receiver, new FlvMetadata());
IOUtils.copy(is, os); IOUtils.copy(is, os);
os.generateHeader(); os.generateHeaderChunk();
assertEquals("HEADER[252]BODY[0]HEADER[579]BODY[911684]", buf.toString()); assertEquals("HEADER[320]BODY[0]HEADER[797]BODY[911684]", buf.toString());
} }
public void testFoo() { public void testFoo() {
@ -44,8 +40,4 @@ public class FlvFilterTest extends TestCase {
System.out.println(Long.toHexString(Double.doubleToLongBits(150d))); System.out.println(Long.toHexString(Double.doubleToLongBits(150d)));
} }
} }

View file

@ -1,11 +1,24 @@
package org.forkalsrud.album.video; package org.forkalsrud.album.video;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import org.forkalsrud.album.exif.Dimension; import org.forkalsrud.album.exif.Dimension;
import junit.framework.TestCase; import junit.framework.TestCase;
public class FlvMetadataTest extends TestCase { public class FlvMetadataTest extends TestCase {
private ByteArrayOutputStream out = new ByteArrayOutputStream();
private ByteArrayInputStream in;
FlvMetadata md0 = new FlvMetadata();
private void flip() {
in = new ByteArrayInputStream(out.toByteArray());
out.reset();
}
public void testMerge() throws Exception { public void testMerge() throws Exception {
FlvMetadata md1 = new FlvMetadata(); FlvMetadata md1 = new FlvMetadata();
@ -19,4 +32,76 @@ public class FlvMetadataTest extends TestCase {
assertEquals(Double.valueOf("6"), md2.get("height")); assertEquals(Double.valueOf("6"), md2.get("height"));
} }
public void testReadWriteDouble() throws Exception {
double value = 3.14;
md0.writeDouble(out, value);
flip();
assertEquals(value, md0.readDouble(in));
}
public void testReadWriteInt() throws Exception {
int value = 45;
// uint16
md0.writeUint16(out, value);
flip();
assertEquals(value, md0.readUint16(in));
// uint24
md0.writeUint24(out, value);
flip();
assertEquals(value, md0.readUint24(in));
// uint32
md0.writeUint32(out, value);
flip();
assertEquals(value, md0.readUint32(in));
// timestamp
md0.writeTimestamp(out, value);
flip();
assertEquals(value, md0.readTimestamp(in));
}
public void testReadWriteString() throws Exception {
String value = "awsome";
md0.writeFlvString(out, value);
flip();
assertEquals(value, md0.readString(in));
}
public void testWriteReadMetadata() throws Exception {
FlvMetadata md1 = new FlvMetadata();
md1.setDuration("3.14");
md1.setDimension(new Dimension(5, 6));
md1.writeScriptDataObject(out);
flip();
FlvMetadata md2 = new FlvMetadata();
md2.read(in);
assertEquals("3.14", md2.get("duration").toString());
assertEquals(5, md2.getWidth());
assertEquals(6, md2.getHeight());
}
public void testReadWriteMetadata() throws Exception {
InputStream is = getClass().getResourceAsStream("/VideoAd.flv");
is.read(new byte[24]); // offset to start of scriptdata
FlvMetadata md1 = new FlvMetadata();
md1.read(is);
md1.writeScriptDataObject(out);
flip();
FlvMetadata md2 = new FlvMetadata();
md2.read(in);
assertEquals(md1, md2);
}
} }