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;
|
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.io.UnsupportedEncodingException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Separates the FLV header boxes from the body boxes.
|
* 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 byte[] fileHeader = new byte[FLV_SIZE_FILEHEADER];
|
||||||
private int byteCounter = 0;
|
private int byteCounter = 0;
|
||||||
|
private int incomingMetadataLength = 0;
|
||||||
|
|
||||||
private byte[] currentBoxHeader = new byte[FLV_SIZE_TAGHEADER];
|
private byte[] currentBoxHeader = new byte[FLV_SIZE_TAGHEADER];
|
||||||
private int currentBoxWriteIdx = 0;
|
private int currentBoxWriteIdx = 0;
|
||||||
|
|
||||||
|
|
@ -36,6 +41,9 @@ public class FlvFilter extends OutputStream {
|
||||||
private int currentTagSize;
|
private int currentTagSize;
|
||||||
private byte[] currentBox;
|
private byte[] currentBox;
|
||||||
|
|
||||||
|
private FlvMetadata metadata = new FlvMetadata();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receive some bytes of FLV output
|
* Receive some bytes of FLV output
|
||||||
* @param b
|
* @param b
|
||||||
|
|
@ -118,14 +126,13 @@ public class FlvFilter extends OutputStream {
|
||||||
case FLV_TAG_VIDEO:
|
case FLV_TAG_VIDEO:
|
||||||
int flags = decodeUint8(currentBox, FLV_SIZE_TAGHEADER);
|
int flags = decodeUint8(currentBox, FLV_SIZE_TAGHEADER);
|
||||||
boolean isKeyFrame = ((flags >> 4) & 0xf) == 1;
|
boolean isKeyFrame = ((flags >> 4) & 0xf) == 1;
|
||||||
if (isKeyFrame) {
|
metadata.addVideoFrame(currentTagPos, currentTagTimestamp, isKeyFrame);
|
||||||
log.info("Keyframe ts " + currentTagTimestamp + " @ " + currentTagPos + " (" + (currentTagPos + currentTagSize) + ")");
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case FLV_TAG_SCRIPTDATA:
|
case FLV_TAG_SCRIPTDATA:
|
||||||
log.info("SCRIPTDATA @ " + currentTagPos + " - len: " + currentTagSize);
|
incomingMetadataLength = currentTagSize;
|
||||||
break;
|
break;
|
||||||
case FLV_TAG_AUDIO:
|
case FLV_TAG_AUDIO:
|
||||||
|
metadata.addAudioFrame(currentTagPos, currentTagTimestamp);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
log.error("Unknown box type: " + currentTagType);
|
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 decodeUint32(byte[] buf, int offset) {
|
||||||
int ch1 = buf[offset + 0] & 0xff;
|
int ch1 = buf[offset + 0] & 0xff;
|
||||||
int ch2 = buf[offset + 1] & 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
|
@Override
|
||||||
public void write(int b) throws IOException {
|
public void write(int b) throws IOException {
|
||||||
write(new byte[] { (byte)b });
|
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;
|
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;
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
|
@ -13,8 +13,11 @@ public class FlvFilterTest extends TestCase {
|
||||||
public void testWrite() throws IOException {
|
public void testWrite() throws IOException {
|
||||||
|
|
||||||
InputStream is = getClass().getResourceAsStream("/VideoAd.flv");
|
InputStream is = getClass().getResourceAsStream("/VideoAd.flv");
|
||||||
OutputStream os = new FlvFilter();
|
FlvFilter os = new FlvFilter();
|
||||||
IOUtils.copy(is, os);
|
IOUtils.copy(is, os);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
os.generateHeader(baos);
|
||||||
|
assertEquals(579, baos.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue