RTMP header:
first byte :
0×03 in case of a 12 byte length header
or
0×43 in case of a 8 byte length header
or
0xC3 in case of a 0 byte length header
next three bytes:
unknown, can be 0×00
next three bytes:
body length, without inter-chunk headers
maximum body size is FFFFFF = 16777215 bytes
next byte:
body type
0×14 : invoke with AMF data
next four bytes:
unknown, can be 0×00
Body bytes in case of invoke
invoke identifier encoded as an AMF string
followed by a number in double precision floating point number ( maybe result request? )
followed by the AMF-encoded arguments
at connection, an AMF encoded object with compulsory properties needed:
– app : the application identifier to connect to
– swfURL : referrer of the swf
– flashVer : agent
– audioCodecs
– videoCodes
– pageURL
body have to be split up into 128-byte length chunks inserting a 0-byte rtmp header ( 0xC3 ) between chunks
AMF encoding
String:
0×02 followed by the size of the string on two bytes, max length: 65535
null:
0×05
Object
0×03 closed by 0×00 0×00 0×09
key – value pairs are defined inside them, keys as amf strings
values as amf encoded data
Fortunately we don’t need to know how to encode AMF data, because ByteArray has a built-in AMF encoder function, and it is also faster than a simple implementation.
And that’s all. Let’s see how to realize it:
package { import flash.net.Socket; import flash.net.ObjectEncoding; import flash.utils.ByteArray; import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.display.Sprite; public class OpenRTMPConnection extends Sprite { // connection status : inactive, pending, handshake, active public var status : String; public var socket : Socket; public function OpenRTMPConnection ( ) { // reset status status = "inactive"; // create new socket socket = new Socket( ); // initializing events socket.addEventListener( Event.CLOSE , onClose ); socket.addEventListener( Event.CONNECT , onConnect ); socket.addEventListener( IOErrorEvent.IO_ERROR , onIOError ); socket.addEventListener( ProgressEvent.SOCKET_DATA , onData ); socket.addEventListener( SecurityErrorEvent.SECURITY_ERROR , onSecurityError ); // connect socket.connect( "localhost" , 1935 ); } public function onClose ( event:Event ):void { trace( "onClose: " + event ); } public function onIOError ( event:IOErrorEvent ):void { trace( "onIOError: " + event ); } public function onSecurityError ( event:SecurityErrorEvent ):void { trace( "onSecurityError " + event ); } public function onData ( event:ProgressEvent ):void { trace( "onData " + status ); switch ( status ) { case "active" : break; case "pending" : sendConnection( ); break; case "inactive" : break; case "handshake" : openConnection( ); break; } } public function onConnect ( event:Event ):void { trace( "onConnect" ); status = "pending"; var count : int = -1; var bytes : ByteArray = new ByteArray( ); // send first handshake : 0x03 followed by 1536 bytes // write header byte bytes.writeByte( 0x03 ); // write 1536 random ( zero in my case ) bytes while ( ++count < 1536 ) bytes.writeByte( 0x00 ); socket.writeBytes( bytes ); // send data socket.flush( ); } public function sendConnection ( ):void { status = "handshake"; // create instances var agentInfo : Object = new Object( ); var copyBytes : ByteArray = new ByteArray( ); var rtmpBytes : ByteArray = new ByteArray( ); var bodyBytes : ByteArray = new ByteArray( ); // CREATE PLAYER SETTINGS // application to connect to agentInfo["app" ] = "milgra"; // referrer agentInfo["swfUrl" ] = "Kilroy was here..."; // page url agentInfo["pageUrl" ] = "She sells sea shells..."; // agent agentInfo["flashVer" ] = "DOS 5.00 with Norton Commander"; agentInfo["tcUrl" ] = "rtmp://localhost/milgra"; agentInfo["audioCodecs" ] = 615; agentInfo["videoCodecs" ] = 76; agentInfo["videoFunction" ] = 0; agentInfo["objectEncoding" ] = 0; // CREATE BODY // we use ByteArray's built-in AMF encoder, amf0 is needed bodyBytes.objectEncoding = ObjectEncoding.AMF0; // write method id // ( AMF String id is 0x03 then string length on 2 bytes, then UTF-encoded string ) bodyBytes.writeObject( "connect" ); // a 64-bit double-precision floating point number is next // ( AMF Number is 0x00 then number on 8 bytes as a signed, little - endian encoded // double precision floating point number ) bodyBytes.writeObject( 1 ); // encoding agent info // ( AMF object starts with the id 0x03 , ends with 0x00 0x00 0x09, // keys are AMF strings without starting 0x03, values are standard AMF values bodyBytes.writeObject( agentInfo ); // CREATE FIRST chunk // cloning second 1536 bytes from server respose socket.readByte( ); socket.readBytes( copyBytes , 0 , 1536 ); socket.readBytes( copyBytes , 0 , 1536 ); // CREATE RTMP header // first byte, header size is 12 byte rtmpBytes.writeByte( 0x03 ); // next three bytes rtmpBytes.writeByte( 0x00 ); rtmpBytes.writeByte( 0x00 ); rtmpBytes.writeByte( 0x00 ); // body size in three bytes var firstByte : int = bodyBytes.length >> 16; rtmpBytes.writeByte( firstByte ); rtmpBytes.writeShort( bodyBytes.length ); // body type in one byte = 0x14 == invoke rtmpBytes.writeByte( 0x14 ); // last four bytes rtmpBytes.writeByte( 0x00 ); rtmpBytes.writeByte( 0x00 ); rtmpBytes.writeByte( 0x00 ); rtmpBytes.writeByte( 0x00 ); // split body into 128 byte chunks with zero-byte rtmp headers var counter : int = -1; var newBody : ByteArray = new ByteArray( ); bodyBytes.position = 0; while( ++counter < bodyBytes.length ) { // write header if ( counter % 128 == 0 && counter != 0 ) newBody.writeByte( 0xc3 ); // copy byte newBody.writeByte( bodyBytes.readByte( ) ); } // CREATE MESSAGE socket.writeBytes( copyBytes ); socket.writeBytes( rtmpBytes ); socket.writeBytes( newBody ); // send data socket.flush( ); } public function openConnection ( ):void { trace( "openConnection" ); // server log : // agent: DOS 5.00 with Norton Commander // referrer: Kilroy was here... } } }
