diff --git a/src/main/java/org/forkalsrud/album/video/FlvFilter.java b/src/main/java/org/forkalsrud/album/video/FlvFilter.java new file mode 100644 index 0000000..b53fab6 --- /dev/null +++ b/src/main/java/org/forkalsrud/album/video/FlvFilter.java @@ -0,0 +1,170 @@ +package org.forkalsrud.album.video; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +/** + * Separates the FLV header boxes from the body boxes. + * After streaming a file through the header can be rewritten with metadata appropriate for streaming. + */ +public class FlvFilter extends OutputStream { + + private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FlvFilter.class); + + private final static int FLV_SIZE_TAGHEADER = 11; + private final static int FLV_SIZE_TAGFOOTER = 4; + private final static int FLV_SIZE_FILEHEADER = 13; + + private final static int FLV_TAG_AUDIO = 8; + private final static int FLV_TAG_VIDEO = 9; + private final static int FLV_TAG_SCRIPTDATA = 18; + + private OutputStream headerDst; + private OutputStream bodyDst; + + private byte[] fileHeader = new byte[FLV_SIZE_FILEHEADER]; + private int byteCounter = 0; + private byte[] currentBoxHeader = new byte[FLV_SIZE_TAGHEADER]; + private int currentBoxWriteIdx = 0; + + private int currentTagPos; + private int currentTagType; + private int currentDataSize; + private int currentTagTimestamp; + + private int currentTagSize; + private byte[] currentBox; + + /** + * Receive some bytes of FLV output + * @param b + * @param off + * @param len + * @throws IOException + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + + // stateful + // initially we receive the header 'FLV.....' + // then we receive one box after another + // if a box can be classified as a header we write it to header dst + // else it is for body dst + int remainingInputLength = len; + int readOffset = off; + if (byteCounter < FLV_SIZE_FILEHEADER) { + int headerBytesToRead = Math.min(FLV_SIZE_FILEHEADER - byteCounter, remainingInputLength); + for (int i = 0; i < headerBytesToRead; i++) { + fileHeader[byteCounter++] = b[readOffset++]; + } + remainingInputLength -= headerBytesToRead; + currentTagPos = byteCounter; + } + while (remainingInputLength > 0) { + // read the header first + if (currentBoxWriteIdx < FLV_SIZE_TAGHEADER) { + int headerBytesToRead = Math.min(FLV_SIZE_TAGHEADER - currentBoxWriteIdx, remainingInputLength); + for (int i = 0; i < headerBytesToRead; i++) { + currentBoxHeader[currentBoxWriteIdx++] = b[readOffset++]; + byteCounter++; + } + remainingInputLength -= headerBytesToRead; + if (currentBoxWriteIdx < FLV_SIZE_TAGHEADER) { + // Don't have a full header yet, wait for next + if (remainingInputLength > 0) { + throw new RuntimeException("something is off in the lengths we're trying to read"); + } + return; + } + // Got the header, prepare the buffer for the entire box + currentTagType = decodeUint8(currentBoxHeader, 0); + currentDataSize = decodeUint24(currentBoxHeader, 1); + currentTagTimestamp = decodeTimestamp(currentBoxHeader, 4); + currentTagSize = currentDataSize + FLV_SIZE_TAGHEADER + FLV_SIZE_TAGFOOTER; + currentBox = new byte[currentTagSize]; + for (int i = 0; i < FLV_SIZE_TAGHEADER; i++) { + currentBox[i] = currentBoxHeader[i]; + } + } + int bodyBytesToRead = Math.min(currentTagSize - currentBoxWriteIdx, remainingInputLength); + for (int i = 0; i < bodyBytesToRead; i++) { + currentBox[currentBoxWriteIdx++] = b[readOffset++]; + byteCounter++; + } + remainingInputLength -= bodyBytesToRead; + if (currentBoxWriteIdx < currentTagSize) { + if (remainingInputLength > 0) { + throw new RuntimeException("something is off in the lengths we're trying to read"); + } + return; + } + processBox(); + currentTagType = 0; + currentDataSize = 0; + currentTagTimestamp = 0; + currentTagSize = 0; + currentBox = null; + currentBoxWriteIdx = 0; + Arrays.fill(currentBoxHeader, (byte)0); + currentTagPos = byteCounter; + } + } + + + void processBox() { + + switch (currentTagType) { + 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) + ")"); + } + break; + case FLV_TAG_SCRIPTDATA: + log.info("SCRIPTDATA @ " + currentTagPos + " - len: " + currentTagSize); + break; + case FLV_TAG_AUDIO: + break; + default: + log.error("Unknown box type: " + currentTagType); + break; + } + } + + + int decodeUint32(byte[] buf, int offset) { + int ch1 = buf[offset + 0] & 0xff; + int ch2 = buf[offset + 1] & 0xff; + int ch3 = buf[offset + 2] & 0xff; + int ch4 = buf[offset + 3] & 0xff; + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); + } + + int decodeUint24(byte[] buf, int offset) { + int ch1 = buf[offset + 0] & 0xff; + int ch2 = buf[offset + 1] & 0xff; + int ch3 = buf[offset + 2] & 0xff; + return ((ch1 << 16) + (ch2 << 8) + (ch3 << 0)); + } + + int decodeUint8(byte[] buf, int offset) { + return buf[offset] & 0xff; + } + + int decodeTimestamp(byte[] buf, int offset) { + int ch1 = buf[offset + 0] & 0xff; + int ch2 = buf[offset + 1] & 0xff; + int ch3 = buf[offset + 2] & 0xff; + int ch4 = buf[offset + 3] & 0xff; + return ((ch4 << 24) + (ch1 << 16) + (ch2 << 8) + (ch3 << 0)); + } + + + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte)b }); + } + +} diff --git a/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java b/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java new file mode 100644 index 0000000..25913cd --- /dev/null +++ b/src/test/java/org/forkalsrud/album/video/FlvFilterTest.java @@ -0,0 +1,20 @@ +package org.forkalsrud.album.video; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import junit.framework.TestCase; + +import org.apache.commons.io.IOUtils; + +public class FlvFilterTest extends TestCase { + + public void testWrite() throws IOException { + + InputStream is = getClass().getResourceAsStream("VideoAd.flv"); + OutputStream os = new FlvFilter(); + IOUtils.copy(is, os); + } + +} diff --git a/src/test/java/org/forkalsrud/album/video/VideoAd.flv b/src/test/java/org/forkalsrud/album/video/VideoAd.flv new file mode 100644 index 0000000..fb14458 Binary files /dev/null and b/src/test/java/org/forkalsrud/album/video/VideoAd.flv differ