/* SimpleFlvWriter.as Lee Felarca http://www.zeropointnine.com/blog 5-2007 v0.8 Singleton class to create uncompressed FLV files. Does not handle audio. Feel free to extend. Source code licensed under a Creative Commons Attribution 3.0 License. http://creativecommons.org/licenses/by/3.0/ Some Rights Reserved. EXAMPLE USAGE: var myWriter:SimpleFlvWriter = SimpleFlvWriter.getInstance(); myWriter.createFile(myFile, 320,240, 30, 120); myWriter.saveFrame( myBitmapData1 ); myWriter.saveFrame( myBitmapData2 ); myWriter.saveFrame( myBitmapData3 ); // etc. myWriter.closeFile(); */ package { import flash.filesystem.File; import flash.filesystem.FileMode; import flash.filesystem.FileStream; import flash.display.BitmapData; import flash.utils.ByteArray; public class SimpleFlvWriter { static private var _instance:SimpleFlvWriter; private var frameWidth:int; private var frameHeight:int; private var frameRate:Number; private var duration:Number; private var fs:FileStream = new FileStream(); private var f:File; private const blockWidth:int = 32; private const blockHeight:int = 32; private var previousTagSize:uint = 0; private var iteration:int = 0; private var bmp:BitmapData; public static function getInstance():SimpleFlvWriter { if(SimpleFlvWriter._instance == null) SimpleFlvWriter._instance = new SimpleFlvWriter(new SingletonEnforcer()); return SimpleFlvWriter._instance; } public function SimpleFlvWriter(singletonEnforcer:SingletonEnforcer) { } public function createFile(pFile:File, pWidth:int, pHeight:int, pFramesPerSecond:Number, pDurationInSeconds:Number=0):void { /* Parameters: pFile: The file which will be created and written to pWidth: Video height pWidth: Video width pFramesPerSecond: Determines framerate of created video pDurationInSeconds: Duration of video file to be created. Used for metadata only. Optional. */ frameWidth = pWidth; frameHeight = pHeight; frameRate = pFramesPerSecond; duration = pDurationInSeconds; f = pFile; fs.openAsync(f, FileMode.WRITE); // create header fs.writeBytes( header() ); // create metadata tag fs.writeUnsignedInt( previousTagSize ); fs.writeBytes( flvTagOnMetaData() ); } public function saveFrame(pBitmapData:BitmapData):void { // bitmap dimensions should of course match parameters passed to createFile() bmp = pBitmapData; fs.writeUnsignedInt( previousTagSize ); fs.writeBytes( flvTagVideo() ); } public function closeFile():void { fs.close(); } private function header():ByteArray { var ba:ByteArray = new ByteArray(); ba.writeByte(0x46) // 'F' ba.writeByte(0x4C) // 'L' ba.writeByte(0x56) // 'V' ba.writeByte(0x01) // Version 1 ba.writeByte(0x01) // misc flags - video stream only ba.writeUnsignedInt(0x09) // header length return ba; } private function flvTagVideo():ByteArray { var tag:ByteArray = new ByteArray(); var dat:ByteArray = videoData(); var timeStamp:uint = uint(1000/frameRate * iteration++); // tag 'header' tag.writeByte( 0x09 ); // tagType = video writeUI24(tag, dat.length); // data size writeUI24(tag, timeStamp); // timestamp in ms tag.writeByte(0); // timestamp extended, not using *** writeUI24(tag, 0); // streamID always 0 // videodata tag.writeBytes( dat ); previousTagSize = tag.length; return tag; } private function videoData():ByteArray { var v:ByteArray = new ByteArray; // VIDEODATA 'header' v.writeByte(0x13); // frametype (1) + codecid (3) // SCREENVIDEOPACKET 'header' // blockwidth/16-1 (4bits) + imagewidth (12bits) writeUI4_12(v, int(blockWidth/16) - 1, frameWidth); // blockheight/16-1 (4bits) + imageheight (12bits) writeUI4_12(v, int(blockHeight/16) - 1, frameHeight); // VIDEODATA > SCREENVIDEOPACKET > IMAGEBLOCKS: var yMax:int = int(frameHeight/blockHeight); var yRemainder:int = frameHeight % blockHeight; if (yRemainder > 0) yMax += 1; var xMax:int = int(frameWidth/blockWidth); var xRemainder:int = frameWidth % blockWidth; if (xRemainder > 0) xMax += 1; for (var y1:int = 0; y1 < yMax; y1++) { for (var x1:int = 0; x1 < xMax; x1++) { // create block var block:ByteArray = new ByteArray(); var yLimit:int = blockHeight; if (yRemainder > 0 && y1 + 1 == yMax) yLimit = yRemainder; for (var y2:int = 0; y2 < yLimit; y2++) { var xLimit:int = blockWidth; if (xRemainder > 0 && x1 + 1 == xMax) xLimit = xRemainder; for (var x2:int = 0; x2 < xLimit; x2++) { var px:int = (x1 * blockWidth) + x2; var py:int = frameHeight - ((y1 * blockHeight) + y2); // (flv's save from bottom to top) var p:uint = bmp.getPixel(px, py); block.writeByte( p & 0xff ); // blue block.writeByte( p >> 8 & 0xff ); // green block.writeByte( p >> 16 ); // red } } block.compress(); writeUI16(v, block.length); // write block length (UI16) v.writeBytes( block ); // write block } } return v; } private function flvTagOnMetaData():ByteArray { var tag:ByteArray = new ByteArray(); var dat:ByteArray = metaData(); // tag 'header' tag.writeByte( 18 ); // tagType = script data writeUI24(tag, dat.length); // data size writeUI24(tag, 0); // timestamp should be 0 for onMetaData tag tag.writeByte(0); // timestamp extended writeUI24(tag, 0); // streamID always 0 // data tag tag.writeBytes( dat ); previousTagSize = tag.length; return tag; } private function metaData():ByteArray { // onMetaData info goes in a ScriptDataObject of data type 'ECMA Array' var b:ByteArray = new ByteArray(); // ObjectNameType (always 2) b.writeByte(2); // ObjectName (type SCRIPTDATASTRING): writeUI16(b, "onMetaData".length); // StringLength b.writeUTFBytes( "onMetaData" ); // StringData // ObjectData (type SCRIPTDATAVALUE): b.writeByte(8); // Type (ECMA array = 8) b.writeUnsignedInt(7) // // Elements in array // SCRIPTDATAVARIABLES... if (duration > 0) { writeUI16(b, "duration".length); b.writeUTFBytes("duration"); b.writeByte(0); b.writeDouble(duration); } writeUI16(b, "width".length); b.writeUTFBytes("width"); b.writeByte(0); b.writeDouble(frameWidth); writeUI16(b, "height".length); b.writeUTFBytes("height"); b.writeByte(0); b.writeDouble(frameHeight); writeUI16(b, "framerate".length); b.writeUTFBytes("framerate"); b.writeByte(0); b.writeDouble(frameRate); writeUI16(b, "videocodecid".length); b.writeUTFBytes("videocodecid"); b.writeByte(0); b.writeDouble(3); // 'Screen Video' = 3 writeUI16(b, "canSeekToEnd".length); b.writeUTFBytes("canSeekToEnd"); b.writeByte(1); b.writeByte(int(true)); var mdc:String = "SimpleFLVWriter.as v0.8 zeropointnine.com"; writeUI16(b, "metadatacreator".length); b.writeUTFBytes("metadatacreator"); b.writeByte(2); writeUI16(b, mdc.length); b.writeUTFBytes(mdc); // VariableEndMarker1 (type UI24 - always 9) writeUI24(b, 9); return b; } private function writeUI24(stream:*, p:uint):void { var byte1:int = p >> 16; var byte2:int = p >> 8 & 0xff; var byte3:int = p & 0xff; stream.writeByte(byte1); stream.writeByte(byte2); stream.writeByte(byte3); } private function writeUI16(stream:*, p:uint):void { stream.writeByte( p >> 8 ) stream.writeByte( p & 0xff ); } private function writeUI4_12(stream:*, p1:uint, p2:uint):void { // writes a 4-bit value followed by a 12-bit value in two sequential bytes var byte1a:int = p1 << 4; var byte1b:int = p2 >> 8; var byte1:int = byte1a + byte1b; var byte2:int = p2 & 0xff; stream.writeByte(byte1); stream.writeByte(byte2); } } } class SingletonEnforcer {} /* FLV structure summary: header previoustagsize flvtag [info] videodata [info] screenvideopacket [info] imageblocks imageblocks ... previoustagsize flvtag ... FLV file format: header last tag size FLVTAG: tagtype datasize timestamp timestampextended streamid data [VIDEODATA]: frametype codecid videodata [SCREENVIDEOPACKET]: blockwidth ub[4] imagewidth ub[12] blockheight ub[4] imageheight ub[12] imageblocks [IMAGEBLOCKS[]]: datasize ub[16] data.. last tag size FLVTAG etc. */