├── SampleRTMPClient.as ├── SampleRTMPClient.swf ├── __init__.py ├── rtmp_protocol.py ├── rtmp_protocol_base.py ├── sample_rtmp_client.py └── sample_rtmp_server.py /SampleRTMPClient.as: -------------------------------------------------------------------------------- 1 | package { 2 | import flash.display.Sprite; 3 | import flash.text.TextField; 4 | import flash.net.NetConnection; 5 | import flash.net.SharedObject; 6 | import flash.net.ObjectEncoding; 7 | import flash.events.*; 8 | 9 | public class SampleRTMPClient extends Sprite { 10 | internal var display_txt:TextField; 11 | internal var netconn:NetConnection; 12 | internal var so:SharedObject; 13 | internal var so2:SharedObject; 14 | 15 | public function SampleRTMPClient() { 16 | display_txt = new TextField(); 17 | display_txt.text = "Connecting..."; 18 | display_txt.width = 400; 19 | display_txt.height = 300; 20 | display_txt.border = true; 21 | display_txt.multiline = true; 22 | display_txt.wordWrap = true; 23 | addChild(display_txt); 24 | 25 | NetConnection.defaultObjectEncoding = ObjectEncoding.AMF0; 26 | netconn = new NetConnection(); 27 | netconn.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler); 28 | 29 | netconn.connect("rtmp://127.0.0.1:80/test","arg1",42); 30 | } 31 | 32 | public function netStatusHandler(event:NetStatusEvent):void{ 33 | switch(event.info.code){ 34 | case 'NetConnection.Connect.Failed': 35 | case 'NetConnection.Connect.Rejected': 36 | display_txt.text = "Connection failed."; 37 | break; 38 | case 'NetConnection.Connect.Success': 39 | display_txt.text = "Connection OK."; 40 | so = SharedObject.getRemote("so_name", netconn.uri); 41 | so.addEventListener(SyncEvent.SYNC, soSyncHandler); 42 | so.connect(netconn); 43 | break; 44 | } 45 | } 46 | 47 | public function soSyncHandler(event:SyncEvent):void{ 48 | display_txt.text = "so_name.sparam = " + so.data.sparam; 49 | so2 = SharedObject.getRemote("so2_name", netconn.uri); 50 | so2.addEventListener(SyncEvent.SYNC, so2SyncHandler); 51 | so2.connect(netconn); 52 | } 53 | 54 | public function so2SyncHandler(event:SyncEvent):void{ 55 | display_txt.text = "so2_name.sparam = " + so2.data.sparam; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SampleRTMPClient.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prekageo/rtmp-python/765480bb34e1ace52a2a2af48279eb2746dcc01e/SampleRTMPClient.swf -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prekageo/rtmp-python/765480bb34e1ace52a2a2af48279eb2746dcc01e/__init__.py -------------------------------------------------------------------------------- /rtmp_protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides classes for creating RTMP (Real Time Message Protocol) servers and 3 | clients. 4 | """ 5 | 6 | import pyamf.amf0 7 | import pyamf.util.pure 8 | import rtmp_protocol_base 9 | import socket 10 | import logging 11 | 12 | class FileDataTypeMixIn(pyamf.util.pure.DataTypeMixIn): 13 | """ 14 | Provides a wrapper for a file object that enables reading and writing of raw 15 | data types for the file. 16 | """ 17 | 18 | def __init__(self, fileobject): 19 | self.fileobject = fileobject 20 | pyamf.util.pure.DataTypeMixIn.__init__(self) 21 | 22 | def read(self, length): 23 | return self.fileobject.read(length) 24 | 25 | def write(self, data): 26 | self.fileobject.write(data) 27 | 28 | def flush(self): 29 | self.fileobject.flush() 30 | 31 | def at_eof(self): 32 | return False 33 | 34 | class DataTypes: 35 | """ Represents an enumeration of the RTMP message datatypes. """ 36 | NONE = -1 37 | SET_CHUNK_SIZE = 1 38 | USER_CONTROL = 4 39 | WINDOW_ACK_SIZE = 5 40 | SET_PEER_BANDWIDTH = 6 41 | SHARED_OBJECT = 19 42 | COMMAND = 20 43 | 44 | class SOEventTypes: 45 | """ Represents an enumeration of the shared object event types. """ 46 | USE = 1 47 | RELEASE = 2 48 | CHANGE = 4 49 | MESSAGE = 6 50 | CLEAR = 8 51 | DELETE = 9 52 | USE_SUCCESS = 11 53 | 54 | class UserControlTypes: 55 | """ Represents an enumeration of the user control event types. """ 56 | STREAM_BEGIN = 0 57 | STREAM_EOF = 1 58 | STREAM_DRY = 2 59 | SET_BUFFER_LENGTH = 3 60 | STREAM_IS_RECORDED = 4 61 | PING_REQUEST = 6 62 | PING_RESPONSE = 7 63 | 64 | class RtmpReader: 65 | """ This class reads RTMP messages from a stream. """ 66 | 67 | chunk_size = 128 68 | 69 | def __init__(self, stream): 70 | """ 71 | Initialize the RTMP reader and set it to read from the specified stream. 72 | """ 73 | self.stream = stream 74 | 75 | def __iter__(self): 76 | return self 77 | 78 | def next(self): 79 | """ Read one RTMP message from the stream and return it. """ 80 | if self.stream.at_eof(): 81 | raise StopIteration 82 | 83 | # Read the message into body_stream. The message may span a number of 84 | # chunks (each one with its own header). 85 | message_body = [] 86 | msg_body_len = 0 87 | header = rtmp_protocol_base.header_decode(self.stream) 88 | # FIXME: this should be really implemented inside header_decode 89 | if header.datatype == DataTypes.NONE: 90 | header = self.prv_header 91 | self.prv_header = header 92 | while True: 93 | read_bytes = min(header.bodyLength - msg_body_len, self.chunk_size) 94 | message_body.append(self.stream.read(read_bytes)) 95 | msg_body_len += read_bytes 96 | if msg_body_len >= header.bodyLength: 97 | break 98 | next_header = rtmp_protocol_base.header_decode(self.stream) 99 | # WORKAROUND: even though the RTMP specification states that the 100 | # extended timestamp field DOES NOT follow type 3 chunks, it seems 101 | # that Flash player 10.1.85.3 and Flash Media Server 3.0.2.217 send 102 | # and expect this field here. 103 | if header.timestamp >= 0x00ffffff: 104 | self.stream.read_ulong() 105 | assert next_header.streamId == -1, (header, next_header) 106 | assert next_header.datatype == -1, (header, next_header) 107 | assert next_header.timestamp == -1, (header, next_header) 108 | assert next_header.bodyLength == -1, (header, next_header) 109 | assert header.bodyLength == msg_body_len, (header, msg_body_len) 110 | body_stream = pyamf.util.BufferedByteStream(''.join(message_body)) 111 | 112 | # Decode the message based on the datatype present in the header 113 | ret = {'msg':header.datatype} 114 | if ret['msg'] == DataTypes.USER_CONTROL: 115 | ret['event_type'] = body_stream.read_ushort() 116 | ret['event_data'] = body_stream.read() 117 | elif ret['msg'] == DataTypes.WINDOW_ACK_SIZE: 118 | ret['window_ack_size'] = body_stream.read_ulong() 119 | elif ret['msg'] == DataTypes.SET_PEER_BANDWIDTH: 120 | ret['window_ack_size'] = body_stream.read_ulong() 121 | ret['limit_type'] = body_stream.read_uchar() 122 | elif ret['msg'] == DataTypes.SHARED_OBJECT: 123 | decoder = pyamf.amf0.Decoder(body_stream) 124 | obj_name = decoder.readString() 125 | curr_version = body_stream.read_ulong() 126 | flags = body_stream.read(8) 127 | 128 | # A shared object message may contain a number of events. 129 | events = [] 130 | while not body_stream.at_eof(): 131 | event = self.read_shared_object_event(body_stream, decoder) 132 | events.append(event) 133 | 134 | ret['obj_name'] = obj_name 135 | ret['curr_version'] = curr_version 136 | ret['flags'] = flags 137 | ret['events'] = events 138 | elif ret['msg'] == DataTypes.COMMAND: 139 | decoder = pyamf.amf0.Decoder(body_stream) 140 | commands = [] 141 | while not body_stream.at_eof(): 142 | commands.append(decoder.readElement()) 143 | ret['command'] = commands 144 | #elif ret['msg'] == DataTypes.NONE: 145 | # print 'WARNING: message with no datatype received.', header 146 | # return self.next() 147 | elif ret['msg'] == DataTypes.SET_CHUNK_SIZE: 148 | ret['chunk_size'] = body_stream.read_ulong() 149 | else: 150 | assert False, header 151 | 152 | logging.debug('recv %r', ret) 153 | return ret 154 | 155 | def read_shared_object_event(self, body_stream, decoder): 156 | """ 157 | Helper method that reads one shared object event found inside a shared 158 | object RTMP message. 159 | """ 160 | so_body_type = body_stream.read_uchar() 161 | so_body_size = body_stream.read_ulong() 162 | 163 | event = {'type':so_body_type} 164 | if event['type'] == SOEventTypes.USE: 165 | assert so_body_size == 0, so_body_size 166 | event['data'] = '' 167 | elif event['type'] == SOEventTypes.RELEASE: 168 | assert so_body_size == 0, so_body_size 169 | event['data'] = '' 170 | elif event['type'] == SOEventTypes.CHANGE: 171 | start_pos = body_stream.tell() 172 | changes = {} 173 | while body_stream.tell() < start_pos + so_body_size: 174 | attrib_name = decoder.readString() 175 | attrib_value = decoder.readElement() 176 | assert attrib_name not in changes, (attrib_name,changes.keys()) 177 | changes[attrib_name] = attrib_value 178 | assert body_stream.tell() == start_pos + so_body_size,\ 179 | (body_stream.tell(),start_pos,so_body_size) 180 | event['data'] = changes 181 | elif event['type'] == SOEventTypes.MESSAGE: 182 | start_pos = body_stream.tell() 183 | msg_params = [] 184 | while body_stream.tell() < start_pos + so_body_size: 185 | msg_params.append(decoder.readElement()) 186 | assert body_stream.tell() == start_pos + so_body_size,\ 187 | (body_stream.tell(),start_pos,so_body_size) 188 | event['data'] = msg_params 189 | elif event['type'] == SOEventTypes.CLEAR: 190 | assert so_body_size == 0, so_body_size 191 | event['data'] = '' 192 | elif event['type'] == SOEventTypes.DELETE: 193 | event['data'] = decoder.readString() 194 | elif event['type'] == SOEventTypes.USE_SUCCESS: 195 | assert so_body_size == 0, so_body_size 196 | event['data'] = '' 197 | else: 198 | assert False, event['type'] 199 | 200 | return event 201 | 202 | class RtmpWriter: 203 | """ This class writes RTMP messages into a stream. """ 204 | 205 | chunk_size = 128 206 | 207 | def __init__(self, stream): 208 | """ 209 | Initialize the RTMP writer and set it to write into the specified 210 | stream. 211 | """ 212 | self.stream = stream 213 | 214 | def flush(self): 215 | """ Flush the underlying stream. """ 216 | self.stream.flush() 217 | 218 | def write(self, message): 219 | logging.debug('send %r', message) 220 | """ Encode and write the specified message into the stream. """ 221 | datatype = message['msg'] 222 | body_stream = pyamf.util.BufferedByteStream() 223 | encoder = pyamf.amf0.Encoder(body_stream) 224 | 225 | if datatype == DataTypes.USER_CONTROL: 226 | body_stream.write_ushort(message['event_type']) 227 | body_stream.write(message['event_data']) 228 | elif datatype == DataTypes.WINDOW_ACK_SIZE: 229 | body_stream.write_ulong(message['window_ack_size']) 230 | elif datatype == DataTypes.SET_PEER_BANDWIDTH: 231 | body_stream.write_ulong(message['window_ack_size']) 232 | body_stream.write_uchar(message['limit_type']) 233 | elif datatype == DataTypes.COMMAND: 234 | for command in message['command']: 235 | encoder.writeElement(command) 236 | elif datatype == DataTypes.SHARED_OBJECT: 237 | encoder.serialiseString(message['obj_name']) 238 | body_stream.write_ulong(message['curr_version']) 239 | body_stream.write(message['flags']) 240 | 241 | for event in message['events']: 242 | self.write_shared_object_event(event, body_stream) 243 | else: 244 | assert False, message 245 | 246 | self.send_msg(datatype, body_stream.getvalue()) 247 | 248 | def write_shared_object_event(self, event, body_stream): 249 | """ 250 | Helper method that writes one shared object inside a shared object RTMP 251 | message. 252 | """ 253 | 254 | inner_stream = pyamf.util.BufferedByteStream() 255 | encoder = pyamf.amf0.Encoder(inner_stream) 256 | 257 | event_type = event['type'] 258 | if event_type == SOEventTypes.USE: 259 | assert event['data'] == '', event['data'] 260 | elif event_type == SOEventTypes.CHANGE: 261 | for attrib_name in event['data']: 262 | attrib_value = event['data'][attrib_name] 263 | encoder.serialiseString(attrib_name) 264 | encoder.writeElement(attrib_value) 265 | elif event['type'] == SOEventTypes.CLEAR: 266 | assert event['data'] == '', event['data'] 267 | elif event['type'] == SOEventTypes.USE_SUCCESS: 268 | assert event['data'] == '', event['data'] 269 | else: 270 | assert False, event 271 | 272 | body_stream.write_uchar(event_type) 273 | body_stream.write_ulong(len(inner_stream)) 274 | body_stream.write(inner_stream.getvalue()) 275 | 276 | def send_msg(self, datatype, body): 277 | """ 278 | Helper method that send the specified message into the stream. Takes 279 | care to prepend the necessary headers and split the message into 280 | appropriately sized chunks. 281 | """ 282 | 283 | # Values that just work. :-) 284 | if datatype >= 1 and datatype <= 7: 285 | channel_id = 2 286 | stream_id = 0 287 | else: 288 | channel_id = 3 289 | stream_id = 0 290 | timestamp = 0 291 | 292 | header = rtmp_protocol_base.Header( 293 | channelId=channel_id, 294 | streamId=stream_id, 295 | datatype=datatype, 296 | bodyLength=len(body), 297 | timestamp=timestamp) 298 | rtmp_protocol_base.header_encode(self.stream, header) 299 | 300 | for i in xrange(0,len(body),self.chunk_size): 301 | chunk = body[i:i+self.chunk_size] 302 | self.stream.write(chunk) 303 | if i+self.chunk_size < len(body): 304 | rtmp_protocol_base.header_encode(self.stream, header, header) 305 | 306 | class FlashSharedObject: 307 | """ 308 | This class represents a Flash Remote Shared Object. Its data are located 309 | inside the self.data dictionary. 310 | """ 311 | 312 | def __init__(self, name): 313 | """ 314 | Initialize a new Flash Remote SO with a given name and empty data. 315 | """ 316 | self.name = name 317 | self.data = {} 318 | self.use_success = False 319 | 320 | def use(self, reader, writer): 321 | """ 322 | Initialize usage of the SO by contacting the Flash Media Server. Any 323 | remote changes to the SO should be now propagated to the client. 324 | """ 325 | self.use_success = False 326 | 327 | msg = { 328 | 'msg': DataTypes.SHARED_OBJECT, 329 | 'curr_version': 0, 330 | 'flags': '\x00\x00\x00\x00\x00\x00\x00\x00', 331 | 'events': [ 332 | { 333 | 'data': '', 334 | 'type': SOEventTypes.USE 335 | } 336 | ], 337 | 'obj_name': self.name 338 | } 339 | writer.write(msg) 340 | writer.flush() 341 | 342 | def handle_message(self, message): 343 | """ 344 | Handle an incoming RTMP message. Check if it is of any relevance for the 345 | specific SO and process it, otherwise ignore it. 346 | """ 347 | if message['msg'] == DataTypes.SHARED_OBJECT and \ 348 | message['obj_name'] == self.name: 349 | events = message['events'] 350 | 351 | if not self.use_success: 352 | assert events[0]['type'] == SOEventTypes.USE_SUCCESS, events[0] 353 | assert events[1]['type'] == SOEventTypes.CLEAR, events[1] 354 | events = events[2:] 355 | self.use_success = True 356 | 357 | self.handle_events(events) 358 | return True 359 | else: 360 | return False 361 | 362 | def handle_events(self, events): 363 | """ Handle SO events that target the specific SO. """ 364 | for event in events: 365 | event_type = event['type'] 366 | if event_type == SOEventTypes.CHANGE: 367 | for key in event['data']: 368 | self.data[key] = event['data'][key] 369 | self.on_change(key) 370 | elif event_type == SOEventTypes.DELETE: 371 | key = event['data'] 372 | assert key in self.data, (key,self.data.keys()) 373 | del self.data[key] 374 | self.on_delete(key) 375 | elif event_type == SOEventTypes.MESSAGE: 376 | self.on_message(event['data']) 377 | else: 378 | assert False, event 379 | 380 | def on_change(self, key): 381 | pass 382 | 383 | def on_delete(self, key): 384 | pass 385 | 386 | def on_message(self, data): 387 | pass 388 | 389 | class RtmpClient: 390 | """ Represents an RTMP client. """ 391 | 392 | def __init__(self, ip, port, tc_url, page_url, swf_url, app): 393 | """ Initialize a new RTMP client. """ 394 | self.ip = ip 395 | self.port = port 396 | self.tc_url = tc_url 397 | self.page_url = page_url 398 | self.swf_url = swf_url 399 | self.app = app 400 | self.shared_objects = [] 401 | 402 | def handshake(self): 403 | """ Perform the handshake sequence with the server. """ 404 | self.stream.write_uchar(3) 405 | c1 = rtmp_protocol_base.Packet() 406 | c1.first = 0 407 | c1.second = 0 408 | c1.payload = 'x'*1528 409 | c1.encode(self.stream) 410 | self.stream.flush() 411 | 412 | self.stream.read_uchar() 413 | s1 = rtmp_protocol_base.Packet() 414 | s1.decode(self.stream) 415 | 416 | c2 = rtmp_protocol_base.Packet() 417 | c2.first = s1.first 418 | c2.second = s1.second 419 | c2.payload = s1.payload 420 | c2.encode(self.stream) 421 | self.stream.flush() 422 | 423 | s2 = rtmp_protocol_base.Packet() 424 | s2.decode(self.stream) 425 | 426 | def connect_rtmp(self, connect_params): 427 | """ Initiate a NetConnection with a Flash Media Server. """ 428 | msg = { 429 | 'msg': DataTypes.COMMAND, 430 | 'command': 431 | [ 432 | u'connect', 433 | 1, 434 | { 435 | 'videoCodecs': 252, 436 | 'audioCodecs': 3191, 437 | 'flashVer': u'WIN 10,1,85,3', 438 | 'app': self.app, 439 | 'tcUrl': self.tc_url, 440 | 'videoFunction': 1, 441 | 'capabilities': 239, 442 | 'pageUrl': self.page_url, 443 | 'fpad': False, 444 | 'swfUrl': self.swf_url, 445 | 'objectEncoding': 0 446 | } 447 | ] 448 | } 449 | msg['command'].extend(connect_params) 450 | self.writer.write(msg) 451 | self.writer.flush() 452 | 453 | while True: 454 | msg = self.reader.next() 455 | if self.handle_message_pre_connect(msg): 456 | break 457 | 458 | def call(self, proc_name, parameters = {}, trans_id = 0): 459 | """ Runs remote procedure calls (RPC) at the receiving end. """ 460 | msg = { 461 | 'msg': DataTypes.COMMAND, 462 | 'command': 463 | [ 464 | proc_name, 465 | trans_id, 466 | parameters 467 | ] 468 | } 469 | self.writer.write(msg) 470 | self.writer.flush() 471 | 472 | def handle_message_pre_connect(self, msg): 473 | """ Handle messages arriving before the connection is established. """ 474 | if msg['msg'] == DataTypes.COMMAND: 475 | assert msg['command'][0] == '_result', msg 476 | assert msg['command'][1] == 1, msg 477 | assert msg['command'][3]['code'] == \ 478 | 'NetConnection.Connect.Success', msg 479 | return True 480 | elif msg['msg'] == DataTypes.WINDOW_ACK_SIZE: 481 | assert msg['window_ack_size'] == 2500000, msg 482 | elif msg['msg'] == DataTypes.SET_PEER_BANDWIDTH: 483 | assert msg['window_ack_size'] == 2500000, msg 484 | assert msg['limit_type'] == 2, msg 485 | elif msg['msg'] == DataTypes.USER_CONTROL: 486 | assert msg['event_type'] == UserControlTypes.STREAM_BEGIN, msg 487 | assert msg['event_data'] == '\x00\x00\x00\x00', msg 488 | elif msg['msg'] == DataTypes.SET_CHUNK_SIZE: 489 | assert msg['chunk_size'] > 0 and msg['chunk_size'] <= 65536, msg 490 | self.reader.chunk_size = msg['chunk_size'] 491 | else: 492 | assert False, msg 493 | 494 | return False 495 | 496 | def connect(self, connect_params): 497 | """ Connect to the server with the given connect parameters. """ 498 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 499 | self.socket.connect((self.ip, self.port)) 500 | self.file = self.socket.makefile() 501 | self.stream = FileDataTypeMixIn(self.file) 502 | 503 | self.handshake() 504 | 505 | self.reader = RtmpReader(self.stream) 506 | self.writer = RtmpWriter(self.stream) 507 | 508 | self.connect_rtmp(connect_params) 509 | 510 | def shared_object_use(self, so): 511 | """ Use a shared object and add it to the managed list of SOs. """ 512 | if so in self.shared_objects: 513 | return 514 | so.use(self.reader, self.writer) 515 | self.shared_objects.append(so) 516 | 517 | def handle_messages(self): 518 | """ Start the message handling loop. """ 519 | while True: 520 | msg = self.reader.next() 521 | 522 | handled = self.handle_simple_message(msg) 523 | 524 | if handled: 525 | continue 526 | 527 | for so in self.shared_objects: 528 | if so.handle_message(msg): 529 | handled = True 530 | break 531 | if not handled: 532 | assert False, msg 533 | 534 | def handle_simple_message(self, msg): 535 | """ Handle simple messages, e.g. ping requests. """ 536 | if msg['msg'] == DataTypes.USER_CONTROL and msg['event_type'] == \ 537 | UserControlTypes.PING_REQUEST: 538 | resp = { 539 | 'msg':DataTypes.USER_CONTROL, 540 | 'event_type':UserControlTypes.PING_RESPONSE, 541 | 'event_data':msg['event_data'], 542 | } 543 | self.writer.write(resp) 544 | self.writer.flush() 545 | return True 546 | 547 | return False 548 | -------------------------------------------------------------------------------- /rtmp_protocol_base.py: -------------------------------------------------------------------------------- 1 | # Source code taken from rtmpy project (http://rtmpy.org/): 2 | # rtmpy/protocol/handshake.py 3 | # rtmpy/protocol/rtmp/header.py 4 | 5 | import time 6 | 7 | HANDSHAKE_LENGTH = 1536 8 | 9 | class Packet(object): 10 | """ 11 | A handshake packet. 12 | 13 | @ivar first: The first 4 bytes of the packet, represented as an unsigned 14 | long. 15 | @type first: 32bit unsigned int. 16 | @ivar second: The second 4 bytes of the packet, represented as an unsigned 17 | long. 18 | @type second: 32bit unsigned int. 19 | @ivar payload: A blob of data which makes up the rest of the packet. This 20 | must be C{HANDSHAKE_LENGTH} - 8 bytes in length. 21 | @type payload: C{str} 22 | @ivar timestamp: Timestamp that this packet was created (in milliseconds). 23 | @type timestamp: C{int} 24 | """ 25 | 26 | first = None 27 | second = None 28 | payload = None 29 | timestamp = None 30 | 31 | def __init__(self, **kwargs): 32 | timestamp = kwargs.get('timestamp', None) 33 | 34 | if timestamp is None: 35 | kwargs['timestamp'] = int(time.time()) 36 | 37 | self.__dict__.update(kwargs) 38 | 39 | def encode(self, buffer): 40 | """ 41 | Encodes this packet to a stream. 42 | """ 43 | buffer.write_ulong(self.first or 0) 44 | buffer.write_ulong(self.second or 0) 45 | 46 | buffer.write(self.payload) 47 | 48 | def decode(self, buffer): 49 | """ 50 | Decodes this packet from a stream. 51 | """ 52 | self.first = buffer.read_ulong() 53 | self.second = buffer.read_ulong() 54 | 55 | self.payload = buffer.read(HANDSHAKE_LENGTH - 8) 56 | 57 | def header_decode(stream): 58 | """ 59 | Reads a header from the incoming stream. 60 | 61 | A header can be of varying lengths and the properties that get updated 62 | depend on the length. 63 | 64 | @param stream: The byte stream to read the header from. 65 | @type stream: C{pyamf.util.BufferedByteStream} 66 | @return: The read header from the stream. 67 | @rtype: L{Header} 68 | """ 69 | # read the size and channelId 70 | channelId = stream.read_uchar() 71 | bits = channelId >> 6 72 | channelId &= 0x3f 73 | 74 | if channelId == 0: 75 | channelId = stream.read_uchar() + 64 76 | 77 | if channelId == 1: 78 | channelId = stream.read_uchar() + 64 + (stream.read_uchar() << 8) 79 | 80 | header = Header(channelId) 81 | 82 | if bits == 3: 83 | return header 84 | 85 | header.timestamp = stream.read_24bit_uint() 86 | 87 | if bits < 2: 88 | header.bodyLength = stream.read_24bit_uint() 89 | header.datatype = stream.read_uchar() 90 | 91 | if bits < 1: 92 | # streamId is little endian 93 | stream.endian = '<' 94 | header.streamId = stream.read_ulong() 95 | stream.endian = '!' 96 | 97 | header.full = True 98 | 99 | if header.timestamp == 0xffffff: 100 | header.timestamp = stream.read_ulong() 101 | 102 | return header 103 | 104 | def header_encode(stream, header, previous=None): 105 | """ 106 | Encodes a RTMP header to C{stream}. 107 | 108 | We expect the stream to already be in network endian mode. 109 | 110 | The channel id can be encoded in up to 3 bytes. The first byte is special as 111 | it contains the size of the rest of the header as described in 112 | L{getHeaderSize}. 113 | 114 | 0 >= channelId > 64: channelId 115 | 64 >= channelId > 320: 0, channelId - 64 116 | 320 >= channelId > 0xffff + 64: 1, channelId - 64 (written as 2 byte int) 117 | 118 | @param stream: The stream to write the encoded header. 119 | @type stream: L{util.BufferedByteStream} 120 | @param header: The L{Header} to encode. 121 | @param previous: The previous header (if any). 122 | """ 123 | if previous is None: 124 | size = 0 125 | else: 126 | size = min_bytes_required(header, previous) 127 | 128 | channelId = header.channelId 129 | 130 | if channelId < 64: 131 | stream.write_uchar(size | channelId) 132 | elif channelId < 320: 133 | stream.write_uchar(size) 134 | stream.write_uchar(channelId - 64) 135 | else: 136 | channelId -= 64 137 | 138 | stream.write_uchar(size + 1) 139 | stream.write_uchar(channelId & 0xff) 140 | stream.write_uchar(channelId >> 0x08) 141 | 142 | if size == 0xc0: 143 | return 144 | 145 | if size <= 0x80: 146 | if header.timestamp >= 0xffffff: 147 | stream.write_24bit_uint(0xffffff) 148 | else: 149 | stream.write_24bit_uint(header.timestamp) 150 | 151 | if size <= 0x40: 152 | stream.write_24bit_uint(header.bodyLength) 153 | stream.write_uchar(header.datatype) 154 | 155 | if size == 0: 156 | stream.endian = '<' 157 | stream.write_ulong(header.streamId) 158 | stream.endian = '!' 159 | 160 | if size <= 0x80: 161 | if header.timestamp >= 0xffffff: 162 | stream.write_ulong(header.timestamp) 163 | 164 | class Header(object): 165 | """ 166 | An RTMP Header. Holds contextual information for an RTMP Channel. 167 | """ 168 | 169 | __slots__ = ('streamId', 'datatype', 'timestamp', 'bodyLength', 170 | 'channelId', 'full') 171 | 172 | def __init__(self, channelId, timestamp=-1, datatype=-1, 173 | bodyLength=-1, streamId=-1, full=False): 174 | self.channelId = channelId 175 | self.timestamp = timestamp 176 | self.datatype = datatype 177 | self.bodyLength = bodyLength 178 | self.streamId = streamId 179 | self.full = full 180 | 181 | def __repr__(self): 182 | attrs = [] 183 | 184 | for k in self.__slots__: 185 | v = getattr(self, k, None) 186 | 187 | if v == -1: 188 | v = None 189 | 190 | attrs.append('%s=%r' % (k, v)) 191 | 192 | return '<%s.%s %s at 0x%x>' % ( 193 | self.__class__.__module__, 194 | self.__class__.__name__, 195 | ' '.join(attrs), 196 | id(self)) 197 | 198 | def min_bytes_required(old, new): 199 | """ 200 | Returns the number of bytes needed to de/encode the header based on the 201 | differences between the two. 202 | 203 | Both headers must be from the same channel. 204 | 205 | @type old: L{Header} 206 | @type new: L{Header} 207 | """ 208 | if old is new: 209 | return 0xc0 210 | 211 | if old.channelId != new.channelId: 212 | raise HeaderError('channelId mismatch on diff old=%r, new=%r' % ( 213 | old, new)) 214 | 215 | if old.streamId != new.streamId: 216 | return 0 # full header 217 | 218 | if old.datatype == new.datatype and old.bodyLength == new.bodyLength: 219 | if old.timestamp == new.timestamp: 220 | return 0xc0 221 | 222 | return 0x80 223 | 224 | return 0x40 225 | -------------------------------------------------------------------------------- /sample_rtmp_client.py: -------------------------------------------------------------------------------- 1 | """ Sample implementation of an RTMP client. """ 2 | 3 | import rtmp_protocol 4 | 5 | class SO(rtmp_protocol.FlashSharedObject): 6 | """ Represents a sample shared object. """ 7 | 8 | def on_change(self, key): 9 | """ Handle change events for the specific shared object. """ 10 | print '%s.sparam = "%s"' % (self.name, self.data['sparam']) 11 | 12 | def main(): 13 | """ 14 | Start the client, connect to 127.0.0.1:80 and use 2 remote flash shared 15 | objects. 16 | """ 17 | client = rtmp_protocol.RtmpClient('127.0.0.1', 80, '', '', '', '') 18 | client.connect([]) 19 | 20 | so_name = SO('so_name') 21 | client.shared_object_use(so_name) 22 | 23 | so2_name = SO('so2_name') 24 | client.shared_object_use(so2_name) 25 | 26 | client.handle_messages() 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /sample_rtmp_server.py: -------------------------------------------------------------------------------- 1 | """ Sample implementation of an RTMP server. """ 2 | 3 | import pyamf.util 4 | import rtmp_protocol_base 5 | import rtmp_protocol 6 | import SocketServer 7 | import time 8 | 9 | class RTMPHandler(SocketServer.BaseRequestHandler): 10 | """ Handles a client connection. """ 11 | 12 | WAITING_C1 = 0 13 | WAITING_C2 = 1 14 | WAITING_COMMAND_CONNECT = 2 15 | WAITING_DATA = 3 16 | 17 | def handle(self): 18 | """ 19 | This method gets called when a client connects to the RTMP server. It 20 | implements a state machine. 21 | """ 22 | state = self.WAITING_C1 23 | self.state2 = 0 24 | f = self.request.makefile() 25 | self.sock_stream = rtmp_protocol.FileDataTypeMixIn(f) 26 | self.reader = rtmp_protocol.RtmpReader(self.sock_stream) 27 | self.writer = rtmp_protocol.RtmpWriter(self.sock_stream) 28 | 29 | while True: 30 | if state == self.WAITING_C1: 31 | self.handle_C1() 32 | state += 1 33 | elif state == self.WAITING_C2: 34 | self.handle_C2() 35 | state += 1 36 | elif state == self.WAITING_COMMAND_CONNECT: 37 | self.handle_command_connect() 38 | state += 1 39 | elif state == self.WAITING_DATA: 40 | self.handle_data() 41 | else: 42 | assert False, state 43 | 44 | def handle_C1(self): 45 | """ 46 | Handle version byte and first handshake packet sent by client. Send 47 | version byte and two handshake packets to client. 48 | """ 49 | self.sock_stream.read_uchar() 50 | c1 = rtmp_protocol_base.Packet() 51 | c1.decode(self.sock_stream) 52 | 53 | self.sock_stream.write_uchar(3) 54 | s1 = rtmp_protocol_base.Packet(first=0,second=0,payload='x'*1528) 55 | s1.encode(self.sock_stream) 56 | s2 = rtmp_protocol_base.Packet(first=0,second=0,payload='x'*1528) 57 | s2.encode(self.sock_stream) 58 | self.sock_stream.flush() 59 | 60 | def handle_C2(self): 61 | """ Handle second handshake packet from client. """ 62 | c2 = rtmp_protocol_base.Packet() 63 | c2.decode(self.sock_stream) 64 | 65 | def handle_command_connect(self): 66 | """ Handle the first RTMP message that initiates the connection. """ 67 | self.reader.next() 68 | msg = { 69 | 'msg': rtmp_protocol.DataTypes.COMMAND, 70 | 'command': 71 | [ 72 | u'_result', 73 | 1, 74 | {'capabilities': 31, 'fmsVer': u'FMS/3,0,2,217'}, 75 | { 76 | 'code': u'NetConnection.Connect.Success', 77 | 'objectEncoding': 0, 78 | 'description': u'Connection succeeded.', 79 | 'level': u'status' 80 | } 81 | ] 82 | } 83 | self.writer.write(msg) 84 | self.writer.flush() 85 | 86 | def handle_data(self): 87 | """ 88 | Handle additional RTMP messages from the client. In this sample 89 | implementation the server waits for 2 shared object use events and 90 | responds with the use_success, clear and change events for each one of 91 | them. 92 | """ 93 | 94 | msg = { 95 | 'msg': rtmp_protocol.DataTypes.SHARED_OBJECT, 96 | 'curr_version': 0, 97 | 'flags': '\x00\x00\x00\x00\x00\x00\x00\x00', 98 | 'events': 99 | [ 100 | { 101 | 'type':rtmp_protocol.SOEventTypes.USE_SUCCESS, 102 | 'data':'' 103 | }, 104 | { 105 | 'type':rtmp_protocol.SOEventTypes.CLEAR, 106 | 'data':'' 107 | }, 108 | { 109 | 'type': rtmp_protocol.SOEventTypes.CHANGE, 110 | } 111 | ] 112 | } 113 | 114 | if self.state2 == 0: 115 | self.state2 += 1 116 | print self.reader.next() 117 | time.sleep(2) 118 | msg['obj_name'] = 'so_name' 119 | msg['events'][2]['data'] = {'sparam':'1234567890 '*5} 120 | self.writer.write(msg) 121 | self.writer.flush() 122 | elif self.state2 == 1: 123 | self.state2 += 1 124 | print self.reader.next() 125 | time.sleep(2) 126 | msg['obj_name'] = 'so2_name' 127 | msg['events'][2]['data'] = {'sparam':'QWERTY '*20} 128 | self.writer.write(msg) 129 | self.writer.flush() 130 | else: 131 | print self.reader.next() 132 | 133 | def main(): 134 | """ Start the RTMP server on 127.0.0.1 at port 80. """ 135 | server = SocketServer.TCPServer(('127.0.0.1', 80), RTMPHandler) 136 | server.serve_forever() 137 | 138 | if __name__ == '__main__': 139 | main() 140 | --------------------------------------------------------------------------------