Reading FLV Dimensions with AIR

Monday I set out to build a desktop Flash Video player using HTML, JavaScript and AIR. When I assumed the size of the video to be played, I was able to knock the project out in just a few hours. On Tuesday I decided that playing a specific sized video wasn’t good enough, so on Wednesday, I set out allow my player window to resize to a user-selected video. And that’s where my problems began.

Video was first introduced in Flash Player 6 with the Sorenson codec (H.263). This is what YouTube and many others still use today in order to be viewed by the largest possible audience. While all FLV files are capable of having metadata about their size, most do not. Since I wanted to resize my video player to the size of the video itself, this left me in a bit of a quandary. Then I remember that Adobe had recently published the SWF v9 specification, to include the FLV format.

With AIR, since I have low-level file IO, this proved to be right up my alley.

All the values in an FLV file “big endian”. This is actually the reverse of the SWF file format itself. The AIR FileStream class has an “endian” property however, making the appropriate byte order sequencing a snap. The header of an FLV contains some content that is useless in this case. The header is also always nine bytes, however, so you can jump right by it using this FileStream.position property.

var flv = air.File.desktopDirectory.resolvePath( 'myvideo.flv' );
var flvs = new air.FileStream();

// Open the file and set the byte order
flvs.open( flv, air.FileMode.READ );
flvs.endian = air.Endian.BIG_ENDIAN;

// Assume header size and jump ahead to tags
flvs.position = 9;

The remainder of the FLV format is followed by interleaving data tags with details about the size of the tags themselves. The types of tags may vary, with one such tag being where all the metadata is stored. In this case however, the metadata doesn’t contain the video dimensions, so you can just keep processing the tags until you get to a video data tag. Note also that rather than processing all the tag data, that there’s a byte that lets us know how much data is in the tag. Reaching out to our friend FileStream.position then allows us to move quickly to the next tag.

// Converts a three-byte value to decimal
// Used by FLV format
function threeQuarterByte( bytes )
{
    var value = 0;
    var shift = 0;   

    for( var i = 0; i < bytes.length; i++ )
    {
        shift = ( 3 - 1 - i ) * 8;
        value += ( bytes[i] & 0x000000FF) << shift;
    }

    return value;
}

...

// Find video tag
while( tagType != 9 )
{
    previous = flvs.readUnsignedInt();
    tagType = flvs.readUnsignedByte();

    dataSize = new air.ByteArray();
    flvs.readBytes( dataSize, 0, 3 );
    dataSize = threeQuarterByte( dataSize );

    if( tagType != 9 )
    {
        flvs.position = flvs.position + 7 + dataSize;
    } else {
        flvs.position = flvs.position + 7;
    }
}

Once we get to the video tag, there’s data about the codec being used. The specification goes on to outline all the details about the Sorenson codec, the On2 codec however is still their intellectual property (IP) and not provided. The good news is that I’ve not found an On2 encoded FLV that doesn’t include the width and height metadata. This problem seems to be specific to the Sorenson codec and the open source encoders that do a poor job providing the appropriate details.

The Sorenson codec uses a ton of bit-level data. That is to say that individual bits (not bytes) represent information about the video data. I can only assume that they chose to use bits to save on every last ounce of file size. This does, however, make my job a little harder since the lowest level of file IO directly provided for by the FileStream class is the byte. You can read a byte and use the toString() method to get the binary representation (by passing a radix value of 2). Going the other way required me to write a little converter function.

function bitsToDecimal( bits )
{
    var value = 0;
    var order = 0;
    var digit = 0;

    for( var b = bits.length - 1; b > 0; b-- )
    {
        digit = new Number( bits.charAt( b ) );
        value = value + ( digit * Math.pow( 2, order ) );
        order = order + 1;
    }

    return value;
}

...

// Read the codec byte
codec = flvs.readUnsignedByte();
bits = codec.toString( 2 );

// Convert the last four bits to decimal
// Gets the actual codec value
codec = bitsToDecimal( bits.substring( 3, bits.length ) );

Among the first ten bytes (eighty bits) of the video data is the information about the dimensions of the video itself. Specifically the thirtieth through thirty-third bits. The codec provides five preset dimension values based on certain combinations of the bits. In the case of a YouTube video as an example, the bit values “101″ mean a video dimension of 320 by 240. Bit values of “000″ and “001″ are available for custom dimensions.

function sorenson( stream )
{
    var bits = new String();
    var digit = null;
    var dim = new Object();
    var pic = null;

    // Read in enough bytes to cover even custom double
    for( var b = 0; b < 9; b++ )
    {
        digit = stream.readUnsignedByte().toString( 2 );

        while( digit.length < 8 )
        {
            digit = '0' + digit;
        }		

        bits = bits + digit;
    }

    pic = bits.substr( 30, 3 );

    switch( pic )
    {
        case '010':
            dim.width = 352;
            dim.height = 288;
            break;

        case '011':
            dim.width = 176;
            dim.height = 144;
            break;

        case '100':
            dim.width = 128;
            dim.height = 96;
            break;

        case '101':
            dim.width = 320;
            dim.height = 240;
            break;

        case '110':
            dim.width = 160;
            dim.height = 120;
            break;

        case '000':
            dim.width = bitsToDecimal( bits.substring( 33, 8 ) );
            dim.height = bitsToDecimal( bits.substring( 41, 8 ) );
            break;

        case '001':
            dim.width = bitsToDecimal( bits.substring( 33, 16 ) );
            dim.height = bitsToDecimal( bits.substring( 49, 16 ) );
            break;
    }

    return dim;
}

With the dimension information now in hand, I can resize my AIR window to fit the video size (plus some additional chrome for a control bar). You might have noticed that the code here is all JavaScript; that’s because I’m writing my AIR video player using HTML and JavaScript, not Flash or Flex. You might then in turn be curious about how I can write a video player in HTML if the HTMLControl in AIR doesn’t support embedded Flash. Stay tuned!

WordPress database error: [Can't open file: 'wp_comments.MYI' (errno: 145)]
SELECT * FROM wp_comments WHERE comment_post_ID = '120' AND comment_approved = '1' ORDER BY comment_date

Leave a Reply