More FLV metadata logic

This commit is contained in:
Knut Forkalsrud 2011-07-04 02:09:57 -07:00
parent df04071177
commit 43fef6e92f
3 changed files with 409 additions and 6 deletions

View file

@ -1,8 +1,11 @@
package org.forkalsrud.album.video;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.LinkedList;
/**
* Separates the FLV header boxes from the body boxes.
@ -25,6 +28,8 @@ public class FlvFilter extends OutputStream {
private byte[] fileHeader = new byte[FLV_SIZE_FILEHEADER];
private int byteCounter = 0;
private int incomingMetadataLength = 0;
private byte[] currentBoxHeader = new byte[FLV_SIZE_TAGHEADER];
private int currentBoxWriteIdx = 0;
@ -36,6 +41,9 @@ public class FlvFilter extends OutputStream {
private int currentTagSize;
private byte[] currentBox;
private FlvMetadata metadata = new FlvMetadata();
/**
* Receive some bytes of FLV output
* @param b
@ -118,14 +126,13 @@ public class FlvFilter extends OutputStream {
case FLV_TAG_VIDEO:
int flags = decodeUint8(currentBox, FLV_SIZE_TAGHEADER);
boolean isKeyFrame = ((flags >> 4) & 0xf) == 1;
if (isKeyFrame) {
log.info("Keyframe ts " + currentTagTimestamp + " @ " + currentTagPos + " (" + (currentTagPos + currentTagSize) + ")");
}
metadata.addVideoFrame(currentTagPos, currentTagTimestamp, isKeyFrame);
break;
case FLV_TAG_SCRIPTDATA:
log.info("SCRIPTDATA @ " + currentTagPos + " - len: " + currentTagSize);
incomingMetadataLength = currentTagSize;
break;
case FLV_TAG_AUDIO:
metadata.addAudioFrame(currentTagPos, currentTagTimestamp);
break;
default:
log.error("Unknown box type: " + currentTagType);
@ -134,6 +141,14 @@ public class FlvFilter extends OutputStream {
}
public void generateHeader(OutputStream out) throws IOException {
out.write(fileHeader);
metadata.setFileOffsetDelta(metadata.calculateLength() - incomingMetadataLength);
metadata.writeOnMetadata(out);
}
int decodeUint32(byte[] buf, int offset) {
int ch1 = buf[offset + 0] & 0xff;
int ch2 = buf[offset + 1] & 0xff;
@ -162,9 +177,24 @@ public class FlvFilter extends OutputStream {
}
void encodeUint32(byte[] buf, int offset, int value) {
buf[offset + 0] = (byte)((value >> 24) & 0xff);
buf[offset + 1] = (byte)((value >> 16) & 0xff);
buf[offset + 2] = (byte)((value >> 8) & 0xff);
buf[offset + 3] = (byte)((value >> 0) & 0xff);
}
void encodeUint16(byte[] buf, int offset, int value) {
buf[offset + 0] = (byte)((value >> 8) & 0xff);
buf[offset + 1] = (byte)((value >> 0) & 0xff);
}
@Override
public void write(int b) throws IOException {
write(new byte[] { (byte)b });
}
}

View file

@ -0,0 +1,370 @@
package org.forkalsrud.album.video;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class FlvMetadata {
private abstract class Attr<T> {
protected boolean present = false;
protected String name;
protected T value;
public Attr(String name) {
super();
this.name = name;
}
public void set(T value) {
this.present = true;
this.value = value;
}
public void write(OutputStream out) throws IOException {
if (name != null) {
writeFlvString(out, name);
}
}
public boolean isPresent() {
return present;
}
}
private class StringAttr extends Attr<String> {
public StringAttr(String name) {
super(name);
}
@Override
public void write(OutputStream out) throws IOException {
super.write(out);
out.write(2); // DataString
writeFlvString(out, value);
}
}
private class BooleanAttr extends Attr<Boolean> {
public BooleanAttr(String name) {
super(name);
}
@Override
public void write(OutputStream out) throws IOException {
super.write(out);
out.write(1); // Bool
out.write(value ? 1 : 0);
}
}
private class DoubleAttr extends Attr<Double> {
public DoubleAttr(String name) {
super(name);
}
@Override
public void write(OutputStream out) throws IOException {
super.write(out);
out.write(0); // NUMBER
long x = Double.doubleToLongBits(value);
writeUint64(out, x);
}
}
private class FileOffsetAttr extends Attr<Integer> {
public FileOffsetAttr(String name) {
super(name);
}
@Override
public void write(OutputStream out) throws IOException {
super.write(out);
out.write(0); // NUMBER
long x = Double.doubleToLongBits(value + filePositionDelta);
writeUint64(out, x);
}
}
private class KeyframesAttr extends Attr<List<Keyframe>> {
public KeyframesAttr(String name, List<Keyframe> value) {
super(name);
this.value = value;
}
@Override
public void write(OutputStream out) throws IOException {
FileOffsetAttr offset = new FileOffsetAttr(null);
DoubleAttr timestamp = new DoubleAttr(null);
super.write(out);
out.write(3); // Variable Array
writeFlvValueArray(out, "filepositions", value.size());
for (Keyframe f : value) {
offset.set(f.getFileOffset(0));
offset.write(out);
}
writeFlvValueArray(out, "times", value.size());
for (Keyframe f : value) {
timestamp.set(f.getTimestamp() / 1000.0);
timestamp.write(out);
}
writeFlvVariableArrayEnd(out);
}
@Override
public boolean isPresent() {
return !value.isEmpty();
}
}
private int filePositionDelta = 0;
private StringAttr creator = new StringAttr("creator");
private StringAttr metadataCreator = new StringAttr("metadatacreator");
private BooleanAttr hasKeyframes = new BooleanAttr("hasKeyframes");
private BooleanAttr hasVideo = new BooleanAttr("hasVideo");
private BooleanAttr hasAudio = new BooleanAttr("hasAudio");
private BooleanAttr hasMetadata = new BooleanAttr("hasMetadata");
private BooleanAttr lastFrameIsKeyframe = new BooleanAttr("canSeekToEnd");
private DoubleAttr duration = new DoubleAttr("duration");
private DoubleAttr datasize = new DoubleAttr("datasize");
private DoubleAttr videosize = new DoubleAttr("videosize");
private DoubleAttr framerate = new DoubleAttr("framerate");
private DoubleAttr videodatarate = new DoubleAttr("videodatarate");
private DoubleAttr videocodecid = new DoubleAttr("videocodecid");
private DoubleAttr width = new DoubleAttr("width");
private DoubleAttr height = new DoubleAttr("height");
private DoubleAttr audiosize = new DoubleAttr("audiosize");
private DoubleAttr audiodatarate = new DoubleAttr("audiodatarate");
private DoubleAttr audiocodecid = new DoubleAttr("audiocodecid");
private DoubleAttr audiosamplerate = new DoubleAttr("audiosamplerate");
private DoubleAttr audiosamplesize = new DoubleAttr("audiosamplesize");
private BooleanAttr stereo = new BooleanAttr("stereo");
private DoubleAttr filesize = new DoubleAttr("filesize");
private DoubleAttr lasttimestamp = new DoubleAttr("lasttimestamp");
private DoubleAttr lastkeyframetimestamp = new DoubleAttr("lastkeyframetimestamp");
private DoubleAttr lastkeyframelocation = new DoubleAttr("lastkeyframelocation");
private LinkedList<Keyframe> keyframes = new LinkedList<Keyframe>();
private ArrayList<Attr> attrs = new ArrayList<Attr>();
public FlvMetadata() {
metadataCreator.set("forkalsrud.org");
hasMetadata.set(true);
attrs.add(creator);
attrs.add(metadataCreator);
attrs.add(hasKeyframes);
attrs.add(hasVideo);
attrs.add(hasAudio);
attrs.add(hasMetadata);
attrs.add(lastFrameIsKeyframe);
attrs.add(duration);
attrs.add(datasize);
attrs.add(videosize);
attrs.add(framerate);
attrs.add(videodatarate);
attrs.add(videocodecid);
attrs.add(width);
attrs.add(height);
attrs.add(audiosize);
attrs.add(audiodatarate);
attrs.add(audiocodecid);
attrs.add(audiosamplerate);
attrs.add(audiosamplesize);
attrs.add(stereo);
attrs.add(filesize);
attrs.add(lasttimestamp);
attrs.add(lastkeyframetimestamp);
attrs.add(lastkeyframelocation);
attrs.add(new KeyframesAttr("keyframes", keyframes));
}
private int length = 0;
public void addVideoFrame(int filePos, int timestamp, boolean isKeyframe) {
hasVideo.set(true);
if (isKeyframe) {
keyframes.add(new Keyframe(filePos, timestamp));
hasKeyframes.set(true);
lastkeyframelocation.set((double)filePos);
lastkeyframetimestamp.set((double)timestamp);
}
lasttimestamp.set((double)timestamp);
lastFrameIsKeyframe.set(isKeyframe);
}
public void addAudioFrame(int filePos, int timestamp) {
hasAudio.set(true);
}
public int calculateLength() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeOnMetadata(baos);
return baos.size();
}
public void setFileOffsetDelta(int fileOffsetDelta) {
this.filePositionDelta = fileOffsetDelta;
}
public void writeOnMetadata(OutputStream out) throws IOException {
// ScriptDataObject
out.write(2);
writeFlvEcmaArray(out, "onMetaData", length);
for (Attr a : attrs) {
if (a.isPresent()) {
a.write(out);
}
}
writeFlvVariableArrayEnd(out);
}
void writeStringAttr(OutputStream out, String name, String 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);
writeUint64(out, x);
}
void writeFlvString(OutputStream out, String s) throws IOException {
byte[] bytes = s.getBytes();
writeUint16(out, bytes.length);
out.write(bytes);
}
private void writeFlvEcmaArray(OutputStream out, String name, int len) throws IOException {
writeFlvString(out, name);
out.write(8); // ECMAArray
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 {
out.write(0);
out.write(0);
out.write(9);
}
private void writeFlvValueArray(OutputStream out, String name, int len) throws IOException {
writeFlvString(out, name);
out.write(10); // Value Array
writeUint32(out, len);
}
void writeUint64(OutputStream out, long value) throws IOException {
out.write((int) ((value >> 56) & 0xff));
out.write((int) ((value >> 48) & 0xff));
out.write((int) ((value >> 40) & 0xff));
out.write((int) ((value >> 32) & 0xff));
out.write((int) ((value >> 24) & 0xff));
out.write((int) ((value >> 16) & 0xff));
out.write((int) ((value >> 8) & 0xff));
out.write((int) ((value >> 0) & 0xff));
}
void writeUint32(OutputStream out, int value) throws IOException {
out.write((value >> 24) & 0xff);
out.write((value >> 16) & 0xff);
out.write((value >> 8) & 0xff);
out.write((value >> 0) & 0xff);
}
void writeUint16(OutputStream out, int value) throws IOException {
out.write((value >> 8) & 0xff);
out.write((value >> 0) & 0xff);
}
private static class Keyframe {
private int fileOffset;
private int timestamp;
public Keyframe(int fileOffset, int timestamp) {
super();
this.fileOffset = fileOffset;
this.timestamp = timestamp;
}
public int getFileOffset(int delta) {
return fileOffset + delta;
}
public void setFileOffset(int fileOffset) {
this.fileOffset = fileOffset;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
}
}

View file

@ -1,8 +1,8 @@
package org.forkalsrud.album.video;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import junit.framework.TestCase;
@ -13,8 +13,11 @@ public class FlvFilterTest extends TestCase {
public void testWrite() throws IOException {
InputStream is = getClass().getResourceAsStream("/VideoAd.flv");
OutputStream os = new FlvFilter();
FlvFilter os = new FlvFilter();
IOUtils.copy(is, os);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
os.generateHeader(baos);
assertEquals(579, baos.size());
}
}