From 43fef6e92fd047b2daac82ec2cd4cf5d46e39c87 Mon Sep 17 00:00:00 2001 From: Knut Forkalsrud Date: Mon, 4 Jul 2011 02:09:57 -0700 Subject: [PATCH] More FLV metadata logic --- .../org/forkalsrud/album/video/FlvFilter.java | 38 +- .../forkalsrud/album/video/FlvMetadata.java | 370 ++++++++++++++++++ .../forkalsrud/album/video/FlvFilterTest.java | 7 +- 3 files changed, 409 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/forkalsrud/album/video/FlvMetadata.java diff --git a/src/main/java/org/forkalsrud/album/video/FlvFilter.java b/src/main/java/org/forkalsrud/album/video/FlvFilter.java index b53fab6..f7c0a20 100644 --- a/src/main/java/org/forkalsrud/album/video/FlvFilter.java +++ b/src/main/java/org/forkalsrud/album/video/FlvFilter.java @@ -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 }); } + } diff --git a/src/main/java/org/forkalsrud/album/video/FlvMetadata.java b/src/main/java/org/forkalsrud/album/video/FlvMetadata.java new file mode 100644 index 0000000..95b68e5 --- /dev/null +++ b/src/main/java/org/forkalsrud/album/video/FlvMetadata.java @@ -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 { + + 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 { + + 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 { + + 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 { + + 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 { + + 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> { + + public KeyframesAttr(String name, List 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 keyframes = new LinkedList(); + + private ArrayList attrs = new ArrayList(); + + 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; + } + + } +} diff --git a/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java b/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java index 63fbe5c..c877ea2 100644 --- a/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java +++ b/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java @@ -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()); } }