More FLV metadata logic
This commit is contained in:
parent
df04071177
commit
43fef6e92f
3 changed files with 409 additions and 6 deletions
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
370
src/main/java/org/forkalsrud/album/video/FlvMetadata.java
Normal file
370
src/main/java/org/forkalsrud/album/video/FlvMetadata.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue