├── .gitignore ├── README.md ├── amf.py ├── av.py ├── common.py ├── handshake.py └── rtmp.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Installer logs 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | 27 | # Unit test / coverage reports 28 | .tox/ 29 | .coverage 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Rope 43 | .ropeproject 44 | 45 | # Django stuff: 46 | *.log 47 | *.pot 48 | 49 | # Sphinx documentation 50 | docs/_build/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Python RTMP Server Protocol 2 | 3 | This is a simple Python implementation of the Real-Time Messaging Protocol (RTMP) server protocol. It provides basic handlers for processing RTMP messages. 4 | 5 | ## Table of Contents 6 | - [Introduction](#introduction) 7 | - [References](#references) 8 | - [Dependencies](#dependencies) 9 | - [Usage](#usage) 10 | 11 | ## Introduction 12 | 13 | RTMP is a protocol used for streaming audio, video, and data over the internet. This project aims to provide a basic RTMP server implementation in Python, allowing you to build your own streaming server or integrate RTMP functionality into your applications. 14 | 15 | Please note that this implementation is not complete and only includes basic handlers. You may need to extend or modify it to suit your specific requirements. 16 | 17 | ## References 18 | 19 | If you're interested in learning more about RTMP or need additional information while working with this project, the following references can be helpful: 20 | 21 | - [RFC 7425](https://datatracker.ietf.org/doc/html/rfc7425): RTMP Chunk Stream Protocol (RTMP Chunking) 22 | - [RTMP Specification 1.0](https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf) 23 | - [RTMP Chunk Stream](https://ossrs.io/lts/en-us/assets/files/rtmp.part1.Chunk-Stream-ae21a33115a2205de5f1532c3da44d44.pdf) 24 | - [Real-Time Messaging Protocol (Wikipedia)](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol) 25 | - [media-server](https://github.com/ireader/media-server) 26 | - [obs-studio](https://github.com/obsproject/obs-studio/) 27 | - [FFmpeg](https://github.com/FFmpeg/FFmpeg) 28 | - [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 29 | - [rtmplite3](https://github.com/KnugiHK/rtmplite3) 30 | - [Node-Media-Server](https://github.com/illuspas/Node-Media-Server) 31 | 32 | ## Dependencies 33 | 34 | The following dependencies are required to run the Python RTMP server: 35 | 36 | - Python 3.x 37 | 38 | ## Usage 39 | 40 | To use this RTMP server implementation, follow these steps: 41 | 42 | 1. Clone or download the repository to your local machine. 43 | 2. Install the required dependencies listed in the `requirements.txt` file, if any. 44 | 3. Customize the server implementation by adding your own logic to the provided basic handlers or extending the existing functionality. 45 | 4. Start the RTMP server by running the main Python file, typically named `rtmp.py` or similar. 46 | 47 | ```bash 48 | python rtmp.py 49 | ``` 50 | 51 | ## Author 52 | 53 | - [masterking32](https://github.com/masterking32) -------------------------------------------------------------------------------- /amf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011, Kundan Singh. All rights reserved. see README for details. 2 | # 3 | # Implementation of 4 | # http://opensource.adobe.com/wiki/download/attachments/1114283/amf0_spec_121207.pdf 5 | # https://github.com/diederickh/ofxFlashCommunication/blob/master/doc/amf0_spec_121207.pdf 6 | # http://opensource.adobe.com/wiki/download/attachments/1114283/amf3_spec_121207.pdf 7 | # https://github.com/diederickh/ofxFlashCommunication/blob/master/doc/amf3_spec_05_05_08.pdf 8 | 9 | import struct 10 | import datetime 11 | import time 12 | import types 13 | from io import BytesIO 14 | import xml.etree.ElementTree as ET 15 | 16 | 17 | # a typed object or received object. Typed object has _classname attr. 18 | class Object(object): 19 | def __init__(self, **kwargs): 20 | for key, val in list(kwargs.items()): 21 | setattr(self, key, val) 22 | 23 | def __len__(self): 24 | return len(self.__dict__) 25 | 26 | 27 | class Class: 28 | __slots__ = ('name', 'encoding', 'attrs') 29 | 30 | 31 | class _Undefined(object): 32 | def __bool__(self): return False # always treated as False 33 | def __repr__(self): return 'amf.undefined' 34 | 35 | 36 | undefined = _Undefined() # received undefined is different from null (None) 37 | 38 | 39 | class AMFBytesIO( 40 | BytesIO): # raise EOFError if needed, allow read with optional length, and peek next byte 41 | def __init__(self, 42 | *args, 43 | **kwargs): BytesIO.__init__(self, 44 | *args, 45 | **kwargs) 46 | 47 | # return true if next read will cause EOFError 48 | def eof(self): return self.tell() >= len(self.getvalue()) 49 | 50 | def remaining(self): return len(self.getvalue()) - \ 51 | self.tell() # return number of remaining bytes 52 | 53 | def read(self, length=-1): 54 | if length > 0 and self.eof(): 55 | raise EOFError # raise error if reading beyond EOF 56 | if length > 0 and self.tell() + length > len(self.getvalue()): 57 | # don't read more than available bytes 58 | length = len(self.getvalue()) - self.tell() 59 | return BytesIO.read(self, length) 60 | 61 | def peek(self): 62 | if self.eof(): 63 | return None 64 | else: 65 | c = self.read(1) 66 | self.seek(self.tell() - 1) 67 | return c 68 | 69 | def read_u8(self): 70 | return struct.unpack("!B", self.read(1))[0] 71 | 72 | def write_u8(self, c): 73 | self.write(struct.pack("!B", c)) 74 | 75 | def read_s8(self): 76 | return struct.unpack("!b", self.read(1))[0] 77 | 78 | def write_s8(self, c): 79 | self.write(struct.pack("!b", c)) 80 | 81 | def read_u16(self): 82 | return struct.unpack("!H", self.read(2))[0] 83 | 84 | def write_u16(self, c): 85 | self.write(struct.pack("!H", c)) 86 | 87 | def read_s16(self): 88 | return struct.unpack("!h", self.read(2))[0] 89 | 90 | def write_s16(self, c): 91 | self.write(struct.pack("!h", c)) 92 | 93 | def read_u32(self): 94 | return struct.unpack("!L", self.read(4))[0] 95 | 96 | def write_u32(self, c): 97 | self.write(struct.pack("!L", c)) 98 | 99 | def read_s32(self): 100 | return struct.unpack("!l", self.read(4))[0] 101 | 102 | def write_s32(self, c): 103 | self.write(struct.pack("!l", c)) 104 | 105 | def read_double(self): 106 | return struct.unpack("!d", self.read(8))[0] 107 | 108 | def write_double(self, c): 109 | self.write(struct.pack("!d", c)) 110 | 111 | def read_utf8(self, length): 112 | return str(self.read(length), 'utf8') 113 | 114 | def write_utf8(self, c): 115 | self.write(c.encode('utf8')) 116 | 117 | def read_u29(self): 118 | n = result = 0 119 | b = self.read_u8() 120 | while b & 0x80 and n < 3: 121 | result <<= 7 122 | result |= b & 0x7f 123 | b = self.read_u8() 124 | n += 1 125 | if n < 3: 126 | result <<= 7 127 | result |= b 128 | else: 129 | result <<= 8 130 | result |= b 131 | assert result & 0xe0000000 == 0 132 | return result 133 | 134 | def read_s29(self): 135 | result = self.read_u29() 136 | if result & 0x10000000: 137 | result -= 0x20000000 138 | return result 139 | 140 | def write_u29(self, c): 141 | if c < 0 or c > 0x1fffffff: 142 | raise ValueError('uint29 out of range') 143 | bytes = '' 144 | if c >= 0x200000: 145 | bytes += chr(0x80 | ((c >> 22) & 0x7f)) 146 | if c >= 0x4000: 147 | bytes += chr(0x80 | ((c >> 15) & 0x7f)) 148 | if c >= 0x80: 149 | bytes += chr(0x80 | ((c >> 8) & 0x7f)) 150 | if c >= 0x200000: 151 | bytes += chr(c & 0xff) 152 | else: 153 | bytes += chr(c & 0x7f) 154 | self.write(bytes) 155 | 156 | def write_s29(self, c): 157 | if c < -0x10000000 or c > 0x0fffffff: 158 | raise ValueError('sint29 out of range') 159 | if c < 0: 160 | c += 0x20000000 161 | self.write_u29(c) 162 | 163 | 164 | class AMF0(object): 165 | NUMBER, BOOL, STRING, OBJECT, MOVIECLIP, NULL, UNDEFINED, REFERENCE, ECMA_ARRAY, OBJECT_END, ARRAY, DATE, LONG_STRING, UNSUPPORTED, RECORDSET, XML, TYPED_OBJECT, TYPE_AMF3 = list( 166 | range(0x12)) 167 | 168 | def __init__(self, data=None): 169 | self._obj_refs, self.data = list(), data if isinstance( 170 | data, AMFBytesIO) else AMFBytesIO(data) if data is not None else AMFBytesIO() 171 | 172 | def _created(self, obj): # new object-reference is created 173 | self._obj_refs.append(obj) 174 | return obj 175 | 176 | def read(self): 177 | global undefined 178 | marker = self.data.read_u8() 179 | if marker == AMF0.NUMBER: 180 | return self.data.read_double() 181 | elif marker == AMF0.BOOL: 182 | return bool(self.data.read_u8()) 183 | elif marker == AMF0.STRING: 184 | return self.readString() 185 | elif marker == AMF0.OBJECT: 186 | return self.readObject() 187 | elif marker == AMF0.MOVIECLIP: 188 | raise NotImplementedError() 189 | elif marker == AMF0.NULL: 190 | return None 191 | elif marker == AMF0.UNDEFINED: 192 | return undefined 193 | elif marker == AMF0.REFERENCE: 194 | return self.readReference() 195 | elif marker == AMF0.ECMA_ARRAY: 196 | return self.readEcmaArray() 197 | elif marker == AMF0.ARRAY: 198 | return self.readArray() 199 | elif marker == AMF0.DATE: 200 | return self.readDate() 201 | elif marker == AMF0.LONG_STRING: 202 | return self.readLongString() 203 | elif marker == AMF0.UNSUPPORTED: 204 | return None 205 | elif marker == AMF0.RECORDSET: 206 | raise NotImplementedError() 207 | elif marker == AMF0.XML: 208 | return self.readXML() 209 | elif marker == AMF0.TYPED_OBJECT: 210 | return self.readTypedObject() 211 | elif marker == AMF0.TYPE_AMF3: 212 | return AMF3(self.data).read() 213 | else: 214 | raise ValueError( 215 | 'Invalid AMF0 marker 0x%02x at %d' % 216 | (marker, self.data.tell() - 1)) 217 | 218 | def write(self, data): 219 | global undefined 220 | if data is None: 221 | self.data.write_u8(AMF0.NULL) 222 | elif data == undefined: 223 | self.data.write_u8(AMF0.UNDEFINED) 224 | elif isinstance(data, bool): 225 | self.data.write_u8(AMF0.BOOL) 226 | self.data.write_u8(1 if data else 0) 227 | elif isinstance(data, (int, float)): 228 | self.data.write_u8(AMF0.NUMBER) 229 | self.data.write_double(float(data)) 230 | elif isinstance(data, (str,)): 231 | self.writeString(data) 232 | elif isinstance(data, (list, tuple)): 233 | self.writeArray(data) 234 | elif isinstance(data, (datetime.date, datetime.datetime)): 235 | self.writeDate(data) 236 | elif isinstance(data, ET.Element): 237 | self.writeXML(data) 238 | elif isinstance(data, dict): 239 | self.writeEcmaArray(data) 240 | elif isinstance(data, Object) and hasattr(data, '_classname'): 241 | self.writeTypedObject(data) 242 | elif isinstance(data, (Object, object)): 243 | self.writeObject(data) 244 | else: 245 | raise ValueError( 246 | 'Invalid AMF0 data %r type %r' % 247 | (data, type(data))) 248 | 249 | def readString(self): return self.data.read_utf8(self.data.read_u16()) 250 | def readLongString(self): return self.data.read_utf8(self.data.read_u32()) 251 | 252 | def writeString(self, data, writeType=True): 253 | data = str(data).encode('utf8') if isinstance(data, str) else data 254 | if writeType: 255 | self.data.write_u8(AMF0.LONG_STRING if len(data) 256 | > 0xffff else AMF0.STRING) 257 | if len(data) > 0xffff: 258 | self.data.write_u32(len(data)) 259 | else: 260 | self.data.write_u16(len(data)) 261 | self.data.write(data) 262 | 263 | def readObject(self): 264 | obj, key = self._created(Object()), self.readString() 265 | while key != '' or self.data.peek() != chr(AMF0.OBJECT_END).encode(): 266 | setattr(obj, key, self.read()) 267 | key = self.readString() 268 | self.data.read() # discard OBJECT_END 269 | return obj 270 | 271 | def writeObject(self, data): 272 | if not self.writePossibleReference(data): 273 | self.data.write_u8(AMF0.OBJECT) 274 | for key, val in list(data.__dict__.items()): 275 | if not key.startswith('_'): 276 | self.writeString(key, False) 277 | self.write(val) 278 | self.writeString('', False) 279 | self.data.write_u8(AMF0.OBJECT_END) 280 | 281 | def readReference(self): 282 | try: 283 | return self._obj_refs[self.data.read_u16()] 284 | except IndexError: 285 | raise ValueError('invalid reference index') 286 | 287 | def writePossibleReference(self, data): 288 | if data in self._obj_refs: 289 | self.data.write_u8(AMF0.REFERENCE) 290 | self.data.write_u16(self._obj_refs.index(data)) 291 | return True 292 | elif len(self._obj_refs) < 0xfffe: 293 | self._obj_refs.append(data) 294 | 295 | def readEcmaArray(self): 296 | len_ignored = self.data.read_u32() 297 | obj, key = self._created(dict()), self.readString() 298 | 299 | while key != '' and self.data.peek() != chr(AMF0.OBJECT_END): 300 | obj[int(key) if key.isdigit() else key] = self.read() 301 | key = self.readString() 302 | self.data.read(1) # discard OBJECT_END 303 | return obj 304 | 305 | def writeEcmaArray(self, data): 306 | if not self.writePossibleReference(data): 307 | self.data.write_u8(AMF0.ECMA_ARRAY) 308 | self.data.write_u32(len(data)) 309 | for key, val in list(data.items()): 310 | self.writeString(key, writeType=False) 311 | self.write(val) 312 | self.writeString('', writeType=False) 313 | self.data.write_u8(AMF0.OBJECT_END) 314 | 315 | def readArray(self): 316 | count, obj = self.data.read_u32(), self._created([]) 317 | obj.extend(self.read() for i in range(count)) 318 | return obj 319 | 320 | def writeArray(self, data): 321 | if not self.writePossibleReference(data): 322 | self.data.write_u8(AMF0.ARRAY) 323 | self.data.write_u32(len(data)) 324 | for val in data: 325 | self.write(val) 326 | 327 | def readDate(self): 328 | ms, tz = self.data.read_double(), self.data.read_s16() 329 | 330 | class TZ(datetime.tzinfo): 331 | def utcoffset(self, dt): return datetime.timedelta(minutes=tz) 332 | def dst(self, dt): return None 333 | def tzname(self, dt): return None 334 | return datetime.datetime.fromtimestamp(ms / 1000.0, TZ()) 335 | 336 | def writeDate(self, data): 337 | if isinstance(data, datetime.date): 338 | data = datetime.datetime.combine(data, datetime.time(0)) 339 | self.data.write_u8(AMF0.DATE) 340 | ms = time.mktime(data.timetuple) 341 | tz = 0 if not data.tzinfo else ( 342 | data.tzinfo.utcoffset.days * 343 | 1440 + 344 | data.tzinfo.utcoffset.seconds / 345 | 60) 346 | self.data.write_double(ms) 347 | self.data.write_s16(tz) 348 | 349 | def readXML(self): return ET.fromstring(self.readLongString()) 350 | 351 | def writeXML(self, data): 352 | data = ET.tostring(data, 'utf8') 353 | self.data.write_u8(AMF0.XML) 354 | self.data.write_u32(len(data)) 355 | self.data.write(data) 356 | 357 | def readTypedObject(self): 358 | classname = self.readString() 359 | obj = self.readObject() 360 | obj._classname = classname 361 | return obj 362 | 363 | def writeTypedObject(self, data): 364 | if not self.writePossibleReference(data): 365 | self.data.write_u8(AMF0.TYPED_OBJECT) 366 | self.data.writeString(data._classname) 367 | for key, val in list(data.__dict__.items()): 368 | if not key.startswith('_'): 369 | self.writeString(key, False) 370 | self.write(val) 371 | self.writeString('', False) 372 | self.data.write_u8(AMF0.OBJECT_END) 373 | 374 | 375 | class AMF3(object): 376 | UNDEFINED, NULL, BOOL_FALSE, BOOL_TRUE, INTEGER, NUMBER, STRING, XML, DATE, ARRAY, OBJECT, XMLSTRING, BYTEARRAY = list( 377 | range(0x0d)) 378 | ANONYMOUS, TYPED, DYNAMIC, EXTERNALIZABLE = 0x01, 0x02, 0x04, 0x08 379 | 380 | def __init__(self, data=None): 381 | self._obj_refs, self._str_refs, self._class_refs = list(), list(), list() 382 | self.data = data if isinstance(data, AMFBytesIO) else AMFBytesIO( 383 | data) if data is not None else AMFBytesIO() 384 | 385 | def read(self): 386 | global undefined 387 | type = self.data.read_u8() 388 | if type == AMF3.UNDEFINED: 389 | return undefined 390 | elif type == AMF3.NULL: 391 | return None 392 | elif type == AMF3.BOOL_FALSE: 393 | return False 394 | elif type == AMF3.BOOL_TRUE: 395 | return True 396 | elif type == AMF3.INTEGER: 397 | return self.readInteger() 398 | elif type == AMF3.NUMBER: 399 | return self.data.read_double() 400 | elif type == AMF3.STRING: 401 | return self.readString() 402 | elif type == AMF3.XML: 403 | return self.readXML() 404 | elif type == AMF3.DATE: 405 | return self.readDate() 406 | elif type == AMF3.ARRAY: 407 | return self.readArray() 408 | elif type == AMF3.OBJECT: 409 | return self.readObject() 410 | elif type == AMF3.XMLSTRING: 411 | return self.readXMLString() 412 | elif type == AMF3.BYTEARRAY: 413 | return self.readByteArray() 414 | else: 415 | raise ValueError( 416 | 'Invalid AMF3 type 0x%02x at %d' % 417 | (type, self.data.tell() - 1)) 418 | 419 | def write(self, data): 420 | global undefined 421 | if data is None: 422 | self.data.write_u8(AMF3.NULL) 423 | elif data == undefined: 424 | self.data.write_u8(AMF3.UNDEFINED) 425 | elif isinstance(data, bool): 426 | self.data.write_u8( 427 | AMF3.BOOL_FALSE if data is False else AMF3.BOOL_TRUE) 428 | elif isinstance(data, (int, float)): 429 | self.writeNumber(data) 430 | elif isinstance(data, (str,)): 431 | self.writeString(data) 432 | elif isinstance(data, ET._ElementInterface): 433 | self.writeXML(data) 434 | elif isinstance(data, (datetime.date, datetime.datetime)): 435 | self.writeDate(data) 436 | elif isinstance(data, (list, tuple)): 437 | self.writeList(data) 438 | elif isinstance(data, dict): 439 | self.writeDict(data) 440 | elif isinstance(data, (types.InstanceType, Object)): 441 | self.writeObject(data) 442 | # no implicit way to invoke writeXMLString and writeByteArray 443 | else: 444 | raise ValueError( 445 | 'Invalid AMF3 data %r type %r' % 446 | (data, type(data))) 447 | 448 | def _readLengthRef(self): 449 | val = self.data.read_u29() 450 | return (val >> 1, val & 0x01 == 0) 451 | 452 | def readInteger(self, signed=True): 453 | self.data.read_u29() if not signed else self.data.read_s29() 454 | 455 | def writeNumber(self, data, writeType=True, type=None): 456 | if type is None: 457 | type = AMF3.INTEGER if isinstance( 458 | data, int) and -0x10000000 <= data <= 0x0FFFFFFF else AMF3.NUMBER 459 | if writeType: 460 | self.data.write_u8(type) 461 | if type == AMF3.INTEGER: 462 | self.data.write_s29(data) 463 | else: 464 | self.data.write_double(float(data)) 465 | 466 | def readString(self, refs=None, decode=True): 467 | length, is_reference = self._readLengthRef() 468 | if refs is None: 469 | refs = self._str_refs 470 | if is_reference: 471 | return refs[length] 472 | if length == 0: 473 | return '' 474 | result = self.data.read(length) 475 | if decode: 476 | try: 477 | # Try decoding as regular utf8 first. TODO: will it always 478 | # raise exception? 479 | result = str(result, 'utf8') 480 | except UnicodeDecodeError: 481 | result = AMF3._decode_utf8_modified(result) 482 | if len(result) > 0: 483 | refs.append(result) 484 | return result 485 | 486 | def writeString(self, data, writeType=True, refs=None, encode=True): 487 | if writeType: 488 | self.data.write_u8(AMF3.STRING) 489 | if refs is None: 490 | refs = self._str_refs 491 | if len(data) == 0: 492 | self.data.write_u8(0x01) 493 | elif not self._writePossibleReference(data, refs): 494 | if encode and isinstance(data, str): 495 | data = str(data).encode('utf8') 496 | self.data.write_u29((len(data) << 1) & 0x01) 497 | self.data.write(data) 498 | 499 | def _writePossibleReference(self, data, refs): 500 | if data in refs: 501 | self.data.write_u29(refs.index(data) << 1) 502 | return True 503 | elif len(refs) < 0x1ffffffe: 504 | refs.append(data) 505 | 506 | # Ported from http://viewvc.rubyforge.mmmultiworks.com/cgi/viewvc.cgi/trunk/lib/ruva/class.rb 507 | # Ruby version is Copyright (c) 2006 Ross Bamford (rosco AT roscopeco DOT 508 | # co DOT uk). The string is first converted to UTF16 BE 509 | @staticmethod 510 | # Modified UTF-8 data. See http://en.wikipedia.org/wiki/UTF-8#Java for 511 | # details 512 | def _decode_utf8_modified(data): 513 | utf16, i, b = [], 0, list(map(ord, data)) 514 | while i < len(b): 515 | c = b[i:i + 516 | 1] if b[i] & 0x80 == 0 else b[i:i + 517 | 2] if b[i] & 0xc0 == 0xc0 else b[i:i + 518 | 3] if b[i] & 0xe0 == 0xe0 else b[i:i + 519 | 4] if b[i] & 0xf8 == 0xf8 else [] 520 | if len(c) == 0: 521 | raise ValueError('invalid modified utf-8') 522 | utf16.append( 523 | c[0] if len(c) == 1 else ( 524 | ((c[0] & 0x1f) << 6) | ( 525 | c[1] & 0x3f)) if len(c) == 2 else ( 526 | ((c[0] & 0x0f) << 12) | ( 527 | (c[1] & 0x3f) << 6) | ( 528 | c[2] & 0x3f)) if len(c) == 3 else ( 529 | ((c[0] & 0x07) << 18) | ( 530 | (c[1] & 0x3f) << 12) | ( 531 | (c[2] & 0x3f) << 6) | ( 532 | c[3] & 0x3f)) if len(c) == 4 else - 533 | 1) 534 | for c in utf16: 535 | if c > 0xffff: 536 | raise ValueError('does not implement more than 16 bit unicode') 537 | return str(''.join([chr((c >> 8) & 0xff) + chr(c & 0xff) 538 | for c in utf16]), 'utf_16_be') 539 | 540 | @staticmethod 541 | def _encode_utf8_modified(data): 542 | ch = [ord(i) for i in str(data).encode('utf_16_be')] 543 | utf16 = [(((ch[i] & 0xff) << 8) + (ch[i + 1] & 0xff)) 544 | for i in range(0, len(ch), 2)] 545 | b = [ 546 | (struct.pack( 547 | '>B', c) if c <= 0x7f else struct.pack( 548 | '>BB', 0xc0 | ( 549 | c >> 6) & 0x1f, 0x80 | c & 0x3f) if c <= 0x7ff else struct.pack( 550 | '>BBB', 0xe0 | ( 551 | c >> 12) & 0xf, 0x80 | ( 552 | c >> 6) & 0x3f, 0x80 | c & 0x3f) if c <= 0xffff else struct.pack( 553 | '!B', 0xf0 | ( 554 | c >> 18) & 0x7, 0x80 | ( 555 | c >> 12) & 0x3f, 0x80 | ( 556 | c >> 6) & 0x3f, 0x80 | c & 0x3f) if c <= 0x10ffff else None) for c in utf16] 557 | return ''.join(b) 558 | 559 | def readDate(self): 560 | length, is_reference = self._readLengthRef() 561 | if is_reference: 562 | return self._obj_refs[length] 563 | ms = self.data.read_double(), 564 | ts = datetime.datetime.fromtimestamp(ms / 1000.0) 565 | self._obj_refs.append(ts) 566 | return ts 567 | 568 | def writeDate(self, data): 569 | self.data.write_u8(AMF3.DATE) 570 | if not self._writePossibleReference(data, self._obj_refs): 571 | if isinstance(data, datetime.time): 572 | raise ValueError('invalid type datetime.time found') 573 | if isinstance(data, datetime.date): 574 | data = datetime.datetime.combine(data, datetime.time(0)) 575 | ms = time.mktime(data.timetuple) 576 | self.data.write_u29(0x01) 577 | self.data.write_double(ms * 1000.0) 578 | 579 | def readArray(self): 580 | length, is_reference = self._readLengthRef() 581 | if is_reference: 582 | return self._obj_refs[length] 583 | key = self.readString(refs=self._str_refs) 584 | if key == '': # return python list since only integer index 585 | result = [self.read() for i in range(length)] 586 | else: # return python dict with key, value 587 | result = {} 588 | while key != '': 589 | result[key] = self.read() 590 | key = self.readString(refs=self._str_refs) 591 | for i in range(length): 592 | result[i] = self.read() 593 | self._obj_refs.append(result) 594 | return result 595 | 596 | def writeList(self, data): 597 | self.data.write_u8(AMF3.ARRAY) 598 | if not self._writePossibleReference(data, refs=self._obj_refs): 599 | self.data.write_u29((len(data) << 1) & 0x01) 600 | self.data.write_u8(0x01) # empty key, value 601 | for val in data: 602 | self.write(val) 603 | 604 | def writeDict(self, data, mixed=True): 605 | if '' in data: 606 | raise ValueError('dict cannot have empty string keys') 607 | self.data.write_u8(AMF3.ARRAY) 608 | if not self._writePossibleReference(data, refs=self._obj_refs): 609 | if mixed: 610 | keys, int_keys, str_keys = list(data.keys()), [], [] 611 | # assume max of 256 values 612 | int_keys = sorted([x for x in keys if isinstance(x, int)]) 613 | str_keys = [x for x in keys if isinstance(x, (str,))] 614 | if len(int_keys) + len(str_keys) < len(keys): 615 | raise ValueError('non-int or str key found in dict') 616 | if len( 617 | int_keys) <= 0 or int_keys[0] != 0 or int_keys[-1] != len(int_keys) - 1: # not dense 618 | str_keys.extend(int_keys) 619 | int_keys[:] = [] 620 | else: 621 | int_keys, str_keys = [], list(data.keys()) 622 | self.data.write_u29((len(int_keys) << 1) & 0x01) 623 | for key in str_keys: 624 | self.writeString(str(key), writeType=False) 625 | self.write(data[key]) 626 | self.data.write_u8(0x01) 627 | for key in int_keys: 628 | self.write(data[key]) 629 | 630 | # (U29O-ref | (U29O-traits-ext class-name *(U8)) | U29O-traits-ref | (U29O-traits class-name *(UTF-8-vr))) 631 | # *(value-type) *(dynamic-member))) 632 | def readObject(self): 633 | type, is_reference = self._readLengthRef() 634 | if is_reference: 635 | return self._obj_refs[type] 636 | if type & 0x03 == 0x03: 637 | raise ValueError('externalizable object is not implemented') 638 | elif type & 0x01 == 0: 639 | class_ = self._class_refs[type >> 1] 640 | elif type & 0x03 == 0x01: # class information 641 | class_ = Class() 642 | class_.name = self.readString() 643 | class_.attrs = [self.read() for i in range(type >> 3)] 644 | if type & 0x04 != 0: 645 | class_.encoding |= AMF3.DYNAMIC 646 | if not class_.name: 647 | class_.encoding |= AMF3.ANONYMOUS 648 | if len(class_.attrs) > 0: 649 | class_.encoding |= AMF3.TYPED 650 | self._class_refs.append(class_) 651 | obj = Object(_class=class_) 652 | for attr in class_.attrs: 653 | setattr(obj, attr, self.read()) 654 | if class_.encoding & AMF3.DYNAMIC: 655 | attr = self.readString() 656 | while attr != '': 657 | setattr(obj, attr, self.read()) 658 | attr = self.readString() 659 | self._obj_refs.append(obj) 660 | return obj 661 | 662 | def writeObject(self, data): 663 | self.data.write_u8(AMF3.OBJECT) 664 | if not self._writePossibleReference(data, refs=self._obj_refs): 665 | if isinstance(data, Object) and hasattr(data, '_class'): 666 | class_ = data._class 667 | if class_ in self._class_refs: 668 | self.data.write_u29( 669 | (self._class_refs.index(class_) << 2) | 0x01) 670 | else: 671 | is_dynamic = 0x80 if class_.encoding & AMF3.DYNAMIC else 0 672 | attr_len = len( 673 | class_.attrs) if hasattr( 674 | class_, 'attrs') and class_.attrs else 0 675 | self.data.write_u29((attr_len << 4) | 0x03 | is_dynamic) 676 | if hasattr(class_, 'name') and class_.name: 677 | self.writeString(class_.name, writeType=False) 678 | else: 679 | self.data.write_u8(0x01) 680 | for attr in class_.attrs: 681 | self.writeString(attr, writeType=False) 682 | self._class_refs.append(class_) 683 | for attr in class_.attrs: 684 | self.write(getattr(data, attr)) 685 | if class_.encoding & AMF3.DYNAMIC: 686 | for key, value in list(data.__dict__.items()): 687 | if key not in class_.attrs: 688 | self.writeString(key, writeType=False) 689 | self.write(getattr(data, key)) 690 | self.data.write_u8(0x01) 691 | else: # encode as anonymous and dynamic object. 692 | self.data.write_u29(0x0b) # no typed attr, dynamic, class def 693 | self.data.write_u8(0x01) # anonymous 694 | for key, value in list(data.__dict__.items()): 695 | self.writeString(key, writeType=False) 696 | self.write(getattr(data, key)) 697 | self.data.write_u8(0x01) 698 | 699 | def readXML(self): 700 | return ET.fromstring(self.readString(refs=self._obj_refs)) 701 | 702 | def writeXML(self, data): 703 | self.data.write_u8(AMF3.XML) 704 | self.writeString( 705 | ET.tostring( 706 | data, 707 | 'utf8'), 708 | writeType=False, 709 | refs=self._obj_refs) 710 | # following variants return str or take data as str 711 | 712 | def readXMLString(self): 713 | return self.readString(refs=self._obj_refs) 714 | 715 | def writeXMLString(self, data): # not implicitly invoked by write() 716 | self.data.write_u8(AMF3.XMLSTRING) 717 | self.writeString(data, writeType=False, refs=self._obj_refs) 718 | 719 | def readByteArray(self): 720 | return self.readString(refs=self._obj_refs, decode=False) 721 | 722 | def writeByteArray(self, data): # not implicitly invoked by write() 723 | self.data.write_u8(AMF3.BYTEARRAY) 724 | self.writeString( 725 | data, 726 | writeType=False, 727 | refs=self._obj_refs, 728 | encode=False) 729 | 730 | # Original source was from rtmpy.org's amf.py, util.py with following Copyright. 731 | # The source in this file has been re-written based on Adobe's AMF0/AMF3 spec. 732 | # 733 | # Copyright (c) 2007 The RTMPy Project. All rights reserved. 734 | # 735 | # Arnar Birgisson 736 | # Thijs Triemstra 737 | # 738 | # Permission is hereby granted, free of charge, to any person obtaining 739 | # a copy of this software and associated documentation files (the 740 | # "Software"), to deal in the Software without restriction, including 741 | # without limitation the rights to use, copy, modify, merge, publish, 742 | # distribute, sublicense, and/or sell copies of the Software, and to 743 | # permit persons to whom the Software is furnished to do so, subject to 744 | # the following conditions: 745 | # 746 | # The above copyright notice and this permission notice shall be 747 | # included in all copies or substantial portions of the Software. 748 | # 749 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 750 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 751 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 752 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 753 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 754 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 755 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 756 | -------------------------------------------------------------------------------- /av.py: -------------------------------------------------------------------------------- 1 | 2 | AAC_SAMPLE_RATE = [ 3 | 96000, 88200, 64000, 48000, 4 | 44100, 32000, 24000, 22050, 5 | 16000, 12000, 11025, 8000, 6 | 7350, 0, 0, 0 7 | ] 8 | 9 | AAC_CHANNELS = [ 10 | 0, 1, 2, 3, 4, 5, 6, 8 11 | ] 12 | 13 | AUDIO_CODEC_NAME = [ 14 | '', 15 | 'ADPCM', 16 | 'MP3', 17 | 'LinearLE', 18 | 'Nellymoser16', 19 | 'Nellymoser8', 20 | 'Nellymoser', 21 | 'G711A', 22 | 'G711U', 23 | '', 24 | 'AAC', 25 | 'Speex', 26 | '', 27 | 'OPUS', 28 | 'MP3-8K', 29 | 'DeviceSpecific', 30 | 'Uncompressed' 31 | ] 32 | 33 | AUDIO_SOUND_RATE = [ 34 | 5512, 11025, 22050, 44100 35 | ] 36 | 37 | VIDEO_CODEC_NAME = [ 38 | '', 39 | 'Jpeg', 40 | 'Sorenson-H263', 41 | 'ScreenVideo', 42 | 'On2-VP6', 43 | 'On2-VP6-Alpha', 44 | 'ScreenVideo2', 45 | 'H264', 46 | '', 47 | '', 48 | '', 49 | '', 50 | 'H265', 51 | 'AV1' 52 | ] 53 | 54 | class Bitop: 55 | def __init__(self, buffer): 56 | self.buffer = buffer 57 | self.buflen = len(buffer) 58 | self.bufpos = 0 59 | self.bufoff = 0 60 | self.iserro = False 61 | 62 | def read(self, n): 63 | v = 0 64 | d = 0 65 | while n: 66 | if n < 0 or self.bufpos >= self.buflen: 67 | self.iserro = True 68 | return 0 69 | 70 | self.iserro = False 71 | d = self.bufoff + n > 8 and 8 - self.bufoff or n 72 | 73 | v <<= d 74 | v += (self.buffer[self.bufpos] >> (8 - self.bufoff - d)) & (0xff >> (8 - d)) 75 | 76 | self.bufoff += d 77 | n -= d 78 | 79 | if self.bufoff == 8: 80 | self.bufpos += 1 81 | self.bufoff = 0 82 | 83 | return v 84 | 85 | def look(self, n): 86 | p = self.bufpos 87 | o = self.bufoff 88 | v = self.read(n) 89 | self.bufpos = p 90 | self.bufoff = o 91 | return v 92 | 93 | def read_golomb(self): 94 | n = 0 95 | while self.read(1) == 0 and not self.iserro: 96 | n += 1 97 | return (1 << n) + self.read(n) - 1 98 | 99 | def get_object_type(bitop): 100 | audio_object_type = bitop.read(5) 101 | if audio_object_type == 31: 102 | audio_object_type = bitop.read(6) + 32 103 | return audio_object_type 104 | 105 | 106 | def get_sample_rate(bitop, info): 107 | info['sampling_index'] = bitop.read(4) 108 | return info['sampling_index'] == 0x0f and bitop.read(24) or AAC_SAMPLE_RATE[info['sampling_index']] 109 | 110 | 111 | def read_aac_specific_config(aac_sequence_header): 112 | info = {} 113 | bitop = Bitop(aac_sequence_header) 114 | bitop.read(16) 115 | info["object_type"] = get_object_type(bitop) 116 | info["sample_rate"] = get_sample_rate(bitop, info) 117 | info["chan_config"] = bitop.read(4) 118 | if info["chan_config"] < len(AAC_CHANNELS): 119 | info["channels"] = AAC_CHANNELS[info["chan_config"]] 120 | info["sbr"] = -1 121 | info["ps"] = -1 122 | if info["object_type"] == 5 or info["object_type"] == 29: 123 | if info["object_type"] == 29: 124 | info["ps"] = 1 125 | info["ext_object_type"] = 5 126 | info["sbr"] = 1 127 | info["sample_rate"] = get_sample_rate(bitop, info) 128 | info["object_type"] = get_object_type(bitop) 129 | 130 | return info 131 | 132 | 133 | def get_aac_profile_name(info): 134 | if info['object_type'] == 1: 135 | return 'Main' 136 | elif info['object_type'] == 2: 137 | return 'HEv2' if info['ps'] > 0 else 'HE' if info['sbr'] > 0 else 'LC' 138 | elif info['object_type'] == 3: 139 | return 'SSR' 140 | elif info['object_type'] == 4: 141 | return 'LTP' 142 | elif info['object_type'] == 5: 143 | return 'SBR' 144 | else: 145 | return '' 146 | 147 | def read_h264_specific_config(avc_sequence_header): 148 | info = {} 149 | profile_idc, width, height, crop_left, crop_right, crop_top, crop_bottom, frame_mbs_only, n, cf_idc, num_ref_frames = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 150 | bitop = Bitop(avc_sequence_header) 151 | bitop.read(48) 152 | info['width'] = 0 153 | info['height'] = 0 154 | info['level'] = 0 155 | 156 | while True: 157 | profile_idc = bitop.read(8) 158 | info['compat'] = bitop.read(8) 159 | level = bitop.read(8) 160 | info['nalu'] = (bitop.read(8) & 0x03) + 1 161 | info['nb_sps'] = bitop.read(8) & 0x1F 162 | if info['nb_sps'] == 0: 163 | break 164 | 165 | bitop.read(16) 166 | 167 | if bitop.read(8) != 0x67: 168 | break 169 | 170 | bitop.read(8) 171 | bitop.read(8) 172 | bitop.read_golomb() 173 | 174 | if profile_idc in [100, 110, 122, 244, 44, 83, 86, 118]: 175 | cf_idc = bitop.read_golomb() 176 | 177 | if cf_idc == 3: 178 | bitop.read(1) 179 | 180 | bitop.read_golomb() 181 | bitop.read_golomb() 182 | bitop.read(1) 183 | 184 | if bitop.read(1): 185 | for n in range(8 if cf_idc != 3 else 12): 186 | if bitop.read(1): 187 | pass # TODO: scaling_list() 188 | 189 | bitop.read_golomb() 190 | 191 | case = bitop.read_golomb() 192 | if case == 0: 193 | bitop.read_golomb() 194 | elif case == 1: 195 | bitop.read(1) 196 | bitop.read_golomb() 197 | bitop.read_golomb() 198 | bitop.read_golomb() 199 | num_ref_frames = bitop.read_golomb() 200 | for n in range(num_ref_frames): 201 | bitop.read_golomb() 202 | 203 | info['avc_ref_frames'] = bitop.read_golomb() 204 | bitop.read(1) 205 | width = bitop.read_golomb() 206 | height = bitop.read_golomb() 207 | frame_mbs_only = bitop.read(1) 208 | 209 | if not frame_mbs_only: 210 | bitop.read(1) 211 | 212 | bitop.read(1) 213 | 214 | if bitop.read(1): 215 | crop_left = bitop.read_golomb() 216 | crop_right = bitop.read_golomb() 217 | crop_top = bitop.read_golomb() 218 | crop_bottom = bitop.read_golomb() 219 | else: 220 | crop_left = 0 221 | crop_right = 0 222 | crop_top = 0 223 | crop_bottom = 0 224 | 225 | info['profile'] = profile_idc 226 | info['level'] = level / 10.0 227 | info['width'] = (width + 1) * 16 - (crop_left + crop_right) * 2 228 | info['height'] = (2 - frame_mbs_only) * (height + 1) * 16 - (crop_top + crop_bottom) * 2 229 | 230 | return info 231 | 232 | 233 | def hevc_parse_ptl(bitop, hevc, max_sub_layers_minus1): 234 | general_ptl = {} # Define general_ptl as a dictionary 235 | 236 | general_ptl['profile_space'] = bitop.read(2) 237 | general_ptl['tier_flag'] = bitop.read(1) 238 | general_ptl['profile_idc'] = bitop.read(5) 239 | general_ptl['profile_compatibility_flags'] = bitop.read(32) 240 | general_ptl['general_progressive_source_flag'] = bitop.read(1) 241 | general_ptl['general_interlaced_source_flag'] = bitop.read(1) 242 | general_ptl['general_non_packed_constraint_flag'] = bitop.read(1) 243 | general_ptl['general_frame_only_constraint_flag'] = bitop.read(1) 244 | bitop.read(32) 245 | bitop.read(12) 246 | general_ptl['level_idc'] = bitop.read(8) 247 | 248 | general_ptl['sub_layer_profile_present_flag'] = [] 249 | general_ptl['sub_layer_level_present_flag'] = [] 250 | 251 | for i in range(max_sub_layers_minus1): 252 | general_ptl['sub_layer_profile_present_flag'].append(bitop.read(1)) 253 | general_ptl['sub_layer_level_present_flag'].append(bitop.read(1)) 254 | 255 | if max_sub_layers_minus1 > 0: 256 | for i in range(max_sub_layers_minus1, 8): 257 | bitop.read(2) 258 | 259 | general_ptl['sub_layer_profile_space'] = [] 260 | general_ptl['sub_layer_tier_flag'] = [] 261 | general_ptl['sub_layer_profile_idc'] = [] 262 | general_ptl['sub_layer_profile_compatibility_flag'] = [] 263 | general_ptl['sub_layer_progressive_source_flag'] = [] 264 | general_ptl['sub_layer_interlaced_source_flag'] = [] 265 | general_ptl['sub_layer_non_packed_constraint_flag'] = [] 266 | general_ptl['sub_layer_frame_only_constraint_flag'] = [] 267 | general_ptl['sub_layer_level_idc'] = [] 268 | 269 | for i in range(max_sub_layers_minus1): 270 | if general_ptl['sub_layer_profile_present_flag'][i]: 271 | general_ptl['sub_layer_profile_space'].append(bitop.read(2)) 272 | general_ptl['sub_layer_tier_flag'].append(bitop.read(1)) 273 | general_ptl['sub_layer_profile_idc'].append(bitop.read(5)) 274 | general_ptl['sub_layer_profile_compatibility_flag'].append(bitop.read(32)) 275 | general_ptl['sub_layer_progressive_source_flag'].append(bitop.read(1)) 276 | general_ptl['sub_layer_interlaced_source_flag'].append(bitop.read(1)) 277 | general_ptl['sub_layer_non_packed_constraint_flag'].append(bitop.read(1)) 278 | general_ptl['sub_layer_frame_only_constraint_flag'].append(bitop.read(1)) 279 | bitop.read(32) 280 | bitop.read(12) 281 | if general_ptl['sub_layer_level_present_flag'][i]: 282 | general_ptl['sub_layer_level_idc'].append(bitop.read(8)) 283 | else: 284 | general_ptl['sub_layer_level_idc'].append(1) 285 | 286 | return general_ptl 287 | 288 | def hevc_parse_sps(sps, hevc): 289 | psps = {} 290 | num_bytes_in_nal_unit = len(sps) 291 | num_bytes_in_rbsp = 0 292 | rbsp_array = [] 293 | bitop = Bitop(sps) 294 | 295 | bitop.read(1) 296 | bitop.read(6) 297 | bitop.read(6) 298 | bitop.read(3) 299 | 300 | for i in range(2, num_bytes_in_nal_unit): 301 | if i + 2 < num_bytes_in_nal_unit and bitop.look(24) == 0x000003: 302 | rbsp_array.append(bitop.read(8)) 303 | rbsp_array.append(bitop.read(8)) 304 | i += 2 305 | bitop.read(8) 306 | else: 307 | rbsp_array.append(bitop.read(8)) 308 | 309 | rbsp = bytes(rbsp_array) 310 | rbsp_bitop = Bitop(rbsp) 311 | psps['sps_video_parameter_set_id'] = rbsp_bitop.read(4) 312 | psps['sps_max_sub_layers_minus1'] = rbsp_bitop.read(3) 313 | psps['sps_temporal_id_nesting_flag'] = rbsp_bitop.read(1) 314 | psps['profile_tier_level'] = hevc_parse_ptl(rbsp_bitop, hevc, psps['sps_max_sub_layers_minus1']) 315 | psps['sps_seq_parameter_set_id'] = rbsp_bitop.read_golomb() 316 | psps['chroma_format_idc'] = rbsp_bitop.read_golomb() 317 | if psps['chroma_format_idc'] == 3: 318 | psps['separate_colour_plane_flag'] = rbsp_bitop.read(1) 319 | else: 320 | psps['separate_colour_plane_flag'] = 0 321 | psps['pic_width_in_luma_samples'] = rbsp_bitop.read_golomb() 322 | psps['pic_height_in_luma_samples'] = rbsp_bitop.read_golomb() 323 | psps['conformance_window_flag'] = rbsp_bitop.read(1) 324 | psps['conf_win_left_offset'] = 0 325 | psps['conf_win_right_offset'] = 0 326 | psps['conf_win_top_offset'] = 0 327 | psps['conf_win_bottom_offset'] = 0 328 | if psps['conformance_window_flag']: 329 | vert_mult = 1 + (psps['chroma_format_idc'] < 2) 330 | horiz_mult = 1 + (psps['chroma_format_idc'] < 3) 331 | psps['conf_win_left_offset'] = rbsp_bitop.read_golomb() * horiz_mult 332 | psps['conf_win_right_offset'] = rbsp_bitop.read_golomb() * horiz_mult 333 | psps['conf_win_top_offset'] = rbsp_bitop.read_golomb() * vert_mult 334 | psps['conf_win_bottom_offset'] = rbsp_bitop.read_golomb() * vert_mult 335 | 336 | return psps 337 | 338 | 339 | def read_hevc_specific_config(hevc_sequence_header): 340 | info = {} 341 | info["width"] = 0 342 | info["height"] = 0 343 | info["profile"] = 0 344 | info["level"] = 0 345 | hevc_sequence_header = hevc_sequence_header[5:] 346 | 347 | while True: 348 | hevc = {} 349 | if len(hevc_sequence_header) < 23: 350 | break 351 | 352 | hevc["configurationVersion"] = hevc_sequence_header[0] 353 | if hevc["configurationVersion"] != 1: 354 | break 355 | 356 | hevc["general_profile_space"] = (hevc_sequence_header[1] >> 6) & 0x03 357 | hevc["general_tier_flag"] = (hevc_sequence_header[1] >> 5) & 0x01 358 | hevc["general_profile_idc"] = hevc_sequence_header[1] & 0x1F 359 | hevc["general_profile_compatibility_flags"] = (hevc_sequence_header[2] << 24) | (hevc_sequence_header[3] << 16) | (hevc_sequence_header[4] << 8) | hevc_sequence_header[5] 360 | hevc["general_constraint_indicator_flags"] = ((hevc_sequence_header[6] << 24) | (hevc_sequence_header[7] << 16) | (hevc_sequence_header[8] << 8) | hevc_sequence_header[9]) 361 | hevc["general_constraint_indicator_flags"] = (hevc["general_constraint_indicator_flags"] << 16) | (hevc_sequence_header[10] << 8) | hevc_sequence_header[11] 362 | hevc["general_level_idc"] = hevc_sequence_header[12] 363 | hevc["min_spatial_segmentation_idc"] = ((hevc_sequence_header[13] & 0x0F) << 8) | hevc_sequence_header[14] 364 | hevc["parallelismType"] = hevc_sequence_header[15] & 0x03 365 | hevc["chromaFormat"] = hevc_sequence_header[16] & 0x03 366 | hevc["bitDepthLumaMinus8"] = hevc_sequence_header[17] & 0x07 367 | hevc["bitDepthChromaMinus8"] = hevc_sequence_header[18] & 0x07 368 | hevc["avgFrameRate"] = (hevc_sequence_header[19] << 8) | hevc_sequence_header[20] 369 | hevc["constantFrameRate"] = (hevc_sequence_header[21] >> 6) & 0x03 370 | hevc["numTemporalLayers"] = (hevc_sequence_header[21] >> 3) & 0x07 371 | hevc["temporalIdNested"] = (hevc_sequence_header[21] >> 2) & 0x01 372 | hevc["lengthSizeMinusOne"] = hevc_sequence_header[21] & 0x03 373 | num_of_arrays = hevc_sequence_header[22] 374 | p = hevc_sequence_header[23:] 375 | 376 | for i in range(num_of_arrays): 377 | if len(p) < 3: 378 | break 379 | nalutype = p[0] 380 | n = (p[1] << 8) | p[2] 381 | p = p[3:] 382 | 383 | for j in range(n): 384 | if len(p) < 2: 385 | break 386 | k = (p[0] << 8) | p[1] 387 | p = p[2:] 388 | 389 | if len(p) < k: 390 | break 391 | if nalutype == 33: 392 | # SPS 393 | sps = p[:k] 394 | hevc["psps"] = hevc_parse_sps(sps, hevc) 395 | info["profile"] = hevc["general_profile_idc"] 396 | info["level"] = hevc["general_level_idc"] / 30.0 397 | info["width"] = hevc["psps"]["pic_width_in_luma_samples"] - (hevc["psps"]["conf_win_left_offset"] + hevc["psps"]["conf_win_right_offset"]) 398 | info["height"] = hevc["psps"]["pic_height_in_luma_samples"] - (hevc["psps"]["conf_win_top_offset"] + hevc["psps"]["conf_win_bottom_offset"]) 399 | p = p[k:] 400 | 401 | break 402 | 403 | return info 404 | 405 | 406 | 407 | def parse_flv_header(header): 408 | if len(header) < 13: 409 | return {} 410 | 411 | flv_header = {} 412 | flv_header["signature"] = header[:3] 413 | flv_header["version"] = header[3] 414 | flv_header["flags"] = header[4] 415 | flv_header["offset"] = (header[5] << 24) | (header[6] << 16) | (header[7] << 8) | header[8] 416 | flv_header["previousTagSize"] = (header[9] << 24) | (header[10] << 16) | (header[11] << 8) | header[12] 417 | 418 | return flv_header 419 | 420 | def parse_tag_header(header): 421 | if len(header) < 11: 422 | return {} 423 | 424 | tag_header = {} 425 | tag_header['type'] = header[0] 426 | tag_header['dataSize'] = (header[1] << 16) | (header[2] << 8) | header[3] 427 | tag_header['timestamp'] = (header[4] << 16) | (header[5] << 8) | header[6] 428 | tag_header['timestampExtended'] = header[7] 429 | tag_header['streamID'] = (header[8] << 16) | (header[9] << 8) | header[10] 430 | 431 | return tag_header 432 | 433 | def parse_flv_body(data): 434 | tags = [] 435 | offset = 0 436 | 437 | while offset < len(data): 438 | tag_header = parse_tag_header(data[offset:]) 439 | offset += 11 440 | 441 | if not tag_header: 442 | break 443 | 444 | tag_data = data[offset:offset + tag_header['dataSize']] 445 | offset += tag_header['dataSize'] 446 | 447 | if tag_header['type'] == 8: 448 | audio_info = {} 449 | audio_info['soundFormat'] = (tag_data[0] >> 4) & 0x0F 450 | audio_info['soundRate'] = (tag_data[0] >> 2) & 0x03 451 | audio_info['soundSize'] = (tag_data[0] >> 1) & 0x01 452 | audio_info['soundType'] = tag_data[0] & 0x01 453 | audio_info['aacPacketType'] = tag_data[1] 454 | 455 | if audio_info['soundFormat'] == 10 and audio_info['aacPacketType'] == 0: 456 | audio_info['aacSequenceHeader'] = tag_data[2:] 457 | audio_info['aacSpecificConfig'] = read_aac_specific_config(audio_info['aacSequenceHeader']) 458 | audio_info['codecName'] = AUDIO_CODEC_NAME[10] 459 | audio_info['profile'] = get_aac_profile_name(audio_info['aacSpecificConfig']) 460 | audio_info['sampleRate'] = audio_info['aacSpecificConfig']['sample_rate'] 461 | audio_info['channels'] = audio_info['aacSpecificConfig']['channels'] 462 | 463 | tags.append(audio_info) 464 | elif tag_header['type'] == 9: 465 | video_info = {} 466 | video_info['frameType'] = (tag_data[0] >> 4) & 0x0F 467 | video_info['codecID'] = tag_data[0] & 0x0F 468 | 469 | if video_info['codecID'] == 7: 470 | video_info['avcPacketType'] = tag_data[1] 471 | if video_info['avcPacketType'] == 0: 472 | video_info['avcSequenceHeader'] = tag_data[2:] 473 | video_info['avcSpecificConfig'] = read_h264_specific_config(video_info['avcSequenceHeader']) 474 | video_info['codecName'] = VIDEO_CODEC_NAME[7] 475 | video_info['profile'] = video_info['avcSpecificConfig']['profile'] 476 | video_info['level'] = video_info['avcSpecificConfig']['level'] 477 | video_info['width'] = video_info['avcSpecificConfig']['width'] 478 | video_info['height'] = video_info['avcSpecificConfig']['height'] 479 | 480 | elif video_info['codecID'] == 12: 481 | video_info['avcPacketType'] = tag_data[1] 482 | if video_info['avcPacketType'] == 0: 483 | video_info['hevcSequenceHeader'] = tag_data[2:] 484 | video_info['hevcSpecificConfig'] = read_hevc_specific_config(video_info['hevcSequenceHeader']) 485 | video_info['codecName'] = VIDEO_CODEC_NAME[12] 486 | video_info['profile'] = video_info['hevcSpecificConfig']['profile_name'] 487 | video_info['level'] = video_info['hevcSpecificConfig']['level'] 488 | video_info['width'] = video_info['hevcSpecificConfig']['width'] 489 | video_info['height'] = video_info['hevcSpecificConfig']['height'] 490 | elif video_info['codecID'] == 13: 491 | video_info['avcPacketType'] = tag_data[1] 492 | video_info['hevcSequenceHeader'] = tag_data[2:] 493 | video_info['hevcSpecificConfig'] = read_hevc_specific_config(video_info['hevcSequenceHeader']) 494 | video_info['codecName'] = VIDEO_CODEC_NAME[13] 495 | video_info['profile'] = 0 496 | video_info['level'] = 0 497 | video_info['width'] = 0 498 | video_info['height'] = 0 499 | 500 | tags.append(video_info) 501 | 502 | return tags 503 | 504 | def read_av1_specific_config(av1_sequence_header): 505 | info = {} 506 | info["width"] = 0 507 | info["height"] = 0 508 | info["profile"] = 0 509 | info["level"] = 0 510 | av1_sequence_header = av1_sequence_header[5:] 511 | 512 | while True: 513 | av1 = {} 514 | if len(av1_sequence_header) < 23: 515 | break 516 | 517 | av1["seq_profile"] = av1_sequence_header[0] 518 | av1["still_picture"] = (av1_sequence_header[1] >> 7) & 0x01 519 | av1["reduced_still_picture_header"] = (av1_sequence_header[1] >> 6) & 0x01 520 | av1["timing_info_present"] = (av1_sequence_header[1] >> 5) & 0x01 521 | av1["decoder_model_info_present"] = (av1_sequence_header[1] >> 4) & 0x01 522 | av1["initial_display_delay_present"] = (av1_sequence_header[1] >> 3) & 0x01 523 | av1["operating_points_cnt_minus_1"] = av1_sequence_header[1] & 0x07 524 | av1["decoder_model_info"] = {} 525 | av1["display_model_info"] = {} 526 | av1["initial_display_delay"] = {} 527 | 528 | if av1["decoder_model_info_present"]: 529 | av1["decoder_model_info"]["buffer_delay_length_minus_1"] = (av1_sequence_header[2] >> 5) & 0x07 530 | av1["decoder_model_info"]["num_units_in_decoding_tick"] = (av1_sequence_header[2] & 0x1F) << 16 | av1_sequence_header[3] << 8 | av1_sequence_header[4] 531 | av1["decoder_model_info"]["buffer_removal_time_length_minus_1"] = (av1_sequence_header[5] >> 5) & 0x07 532 | av1["decoder_model_info"]["frame_presentation_time_length_minus_1"] = (av1_sequence_header[5] & 0x1F) 533 | 534 | av1_sequence_header = av1_sequence_header[6:] 535 | 536 | for i in range(av1["operating_points_cnt_minus_1"] + 1): 537 | av1["operating_point_idc"] = av1_sequence_header[0] 538 | av1["seq_level_idx"] = av1_sequence_header[1] 539 | 540 | if av1["decoder_model_info_present"]: 541 | av1["seq_tier"] = av1_sequence_header[2] >> 7 542 | av1["decoder_model_present_for_this_op"] = (av1_sequence_header[2] >> 6) & 0x01 543 | av1["initial_display_delay_present_for_this_op"] = (av1_sequence_header[2] >> 5) & 0x01 544 | av1_sequence_header = av1_sequence_header[3:] 545 | else: 546 | av1_sequence_header = av1_sequence_header[2:] 547 | 548 | if av1["initial_display_delay_present_for_this_op"]: 549 | av1["initial_display_delay"]["initial_display_delay_minus_1"] = ((av1_sequence_header[0] & 0x3F) << 16) | (av1_sequence_header[1] << 8) | av1_sequence_header[2] 550 | av1_sequence_header = av1_sequence_header[3:] 551 | 552 | av1["frame_width_bits_minus_1"] = (av1_sequence_header[0] >> 4) & 0x0F 553 | av1["frame_height_bits_minus_1"] = av1_sequence_header[0] & 0x0F 554 | av1["max_frame_width_minus_1"] = ((av1_sequence_header[1] & 0x7F) << 8) | av1_sequence_header[2] 555 | av1["max_frame_height_minus_1"] = ((av1_sequence_header[3] & 0x7F) << 8) | av1_sequence_header[4] 556 | 557 | info["width"] = av1["max_frame_width_minus_1"] + 1 558 | info["height"] = av1["max_frame_height_minus_1"] + 1 559 | info["profile"] = av1["seq_profile"] 560 | info["level"] = av1["seq_level_idx"] / 8.0 561 | 562 | av1_sequence_header = av1_sequence_header[5:] 563 | 564 | break 565 | 566 | return info 567 | 568 | 569 | def readAVCSpecificConfig(avcSequenceHeader): 570 | codec_id = avcSequenceHeader[0] & 0x0f 571 | 572 | if codec_id == 7: 573 | return read_h264_specific_config(avcSequenceHeader) 574 | elif codec_id == 12: 575 | return read_hevc_specific_config(avcSequenceHeader) 576 | elif codec_id == 13: 577 | return read_av1_specific_config(avcSequenceHeader) 578 | 579 | def getAVCProfileName(info): 580 | profile = info['profile'] 581 | if profile == 1: 582 | return 'Main' 583 | elif profile == 2: 584 | return 'Main 10' 585 | elif profile == 3: 586 | return 'Main Still Picture' 587 | elif profile == 66: 588 | return 'Baseline' 589 | elif profile == 77: 590 | return 'Main' 591 | elif profile == 100: 592 | return 'High' 593 | else: 594 | return '' 595 | 596 | def SimpleGetVideoInfo(avcSequenceHeader): 597 | # Determine codec based on codec ID 598 | codec_id = avcSequenceHeader[0] & 0x0f 599 | info = {} 600 | info['codec_id'] = codec_id 601 | info['codec'] = None 602 | info['profile_name'] = None 603 | info['video_level'] = None 604 | info['width'] = None 605 | info['height'] = None 606 | 607 | if codec_id == 7: 608 | # H.264 codec 609 | profile_name = avcSequenceHeader[5] # Extract profile name 610 | video_level = avcSequenceHeader[6] # Extract video level 611 | width = (avcSequenceHeader[7] << 8) | avcSequenceHeader[8] # Extract width 612 | height = (avcSequenceHeader[9] << 8) | avcSequenceHeader[10] # Extract height 613 | info['codec'] = 'H.264' 614 | info['profile_name'] = profile_name 615 | info['video_level'] = video_level 616 | info['width'] = width 617 | info['height'] = height 618 | elif codec_id == 12: 619 | # HEVC (H.265) codec 620 | profile_name = avcSequenceHeader[5] # Extract profile name 621 | video_level = avcSequenceHeader[6] # Extract video level 622 | width = ((avcSequenceHeader[16] & 0x03) << 8) | avcSequenceHeader[17] # Extract width 623 | height = ((avcSequenceHeader[18] & 0xF8) << 5) | ((avcSequenceHeader[19] & 0xF8) >> 3) # Extract height 624 | info['codec'] = 'HEVC (H.265)' 625 | info['profile_name'] = profile_name 626 | info['video_level'] = video_level 627 | info['width'] = width 628 | info['height'] = height 629 | elif codec_id == 13: 630 | # VP9 codec 631 | profile_name = avcSequenceHeader[5] # Extract profile name 632 | video_level = avcSequenceHeader[6] # Extract video level 633 | width = ((avcSequenceHeader[16] & 0x3F) << 8) | avcSequenceHeader[17] # Extract width 634 | height = ((avcSequenceHeader[18] & 0x3F) << 8) | avcSequenceHeader[19] # Extract height 635 | info['codec'] = 'VP9' 636 | info['profile_name'] = profile_name 637 | info['video_level'] = video_level 638 | info['width'] = width 639 | info['height'] = height 640 | elif codec_id == 176: 641 | # AV1 (AOM AV1 or STV-av1) codec 642 | profile_name = avcSequenceHeader[5] # Extract profile name 643 | video_level = avcSequenceHeader[6] # Extract video level 644 | width = ((avcSequenceHeader[18] & 0x3F) << 8) | avcSequenceHeader[19] # Extract width 645 | height = ((avcSequenceHeader[16] & 0x3F) << 8) | avcSequenceHeader[17] # Extract height 646 | info['codec'] = 'AV1 (AOM AV1 or STV-av1)' 647 | info['profile_name'] = profile_name 648 | info['video_level'] = video_level 649 | info['width'] = width 650 | info['height'] = height 651 | elif codec_id == 32: 652 | # QuickTime codec (H.264) 653 | width = (avcSequenceHeader[5] << 8) | avcSequenceHeader[6] # Extract width 654 | height = (avcSequenceHeader[7] << 8) | avcSequenceHeader[8] # Extract height 655 | info['codec'] = 'QuickTime (H.264)' 656 | info['width'] = width 657 | info['height'] = height 658 | elif codec_id == 4: 659 | # MPEG-4 codec 660 | width = (avcSequenceHeader[5] << 8) | avcSequenceHeader[6] # Extract width 661 | height = (avcSequenceHeader[7] << 8) | avcSequenceHeader[8] # Extract height 662 | info['codec'] = 'MPEG-4' 663 | info['width'] = width 664 | info['height'] = height 665 | elif codec_id == 27: 666 | # x264 codec 667 | profile_name = avcSequenceHeader[5] # Extract profile name 668 | video_level = avcSequenceHeader[6] # Extract video level 669 | width = (avcSequenceHeader[7] << 8) | avcSequenceHeader[8] # Extract width 670 | height = (avcSequenceHeader[9] << 8) | avcSequenceHeader[10] # Extract height 671 | info['codec'] = 'x264' 672 | info['profile_name'] = profile_name 673 | info['video_level'] = video_level 674 | info['width'] = width 675 | info['height'] = height 676 | elif codec_id == 33: 677 | # MP4 codec 678 | width = (avcSequenceHeader[5] << 8) | avcSequenceHeader[6] # Extract width 679 | height = (avcSequenceHeader[7] << 8) | avcSequenceHeader[8] # Extract height 680 | info['codec'] = 'MP4' 681 | info['width'] = width 682 | info['height'] = height 683 | else: 684 | info['codec'] = f"Unknown codec ID: {codec_id}" 685 | 686 | return info 687 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import amf 3 | 4 | VIDEO_CODEC_NAME = [ 5 | '', 6 | 'Jpeg', 7 | 'Sorenson-H263', 8 | 'ScreenVideo', 9 | 'On2-VP6', 10 | 'On2-VP6-Alpha', 11 | 'ScreenVideo2', 12 | 'H264', 13 | 'MPEG-4-ASP', 14 | 'MPEG-4-AVC (H.264)', 15 | 'MPEG-H Part 2 (H.265)', 16 | 'HEVC (H.265)', 17 | 'AV1', 18 | 'VP9', 19 | 'VP10', 20 | 'VC-1', 21 | 'MPEG-1', 22 | 'MPEG-2', 23 | 'Theora', 24 | 'DV', 25 | 'MJPEG', 26 | 'Huffyuv', 27 | 'FFV1', 28 | 'VP3', 29 | 'VP6', 30 | 'AVS', 31 | 'AVS2', 32 | 'Daala', 33 | 'DNxHD', 34 | 'DNxHR', 35 | 'CineForm', 36 | 'ProRes', 37 | 'RealVideo', 38 | 'Dirac', 39 | 'RV40', 40 | 'Indeo', 41 | 'Flash Video', 42 | 'WebM', 43 | 'Xvid', 44 | 'DivX', 45 | 'WMV', 46 | 'FLV', 47 | 'MKV', 48 | 'MOV', 49 | 'MP4', 50 | '3GP', 51 | ] 52 | 53 | def truncate(data, max=100): 54 | data1 = data and len(data) > max and data[:max] 55 | if isinstance(data1, str): 56 | data2 = f'...({len(data)})' or data 57 | elif isinstance(data1, bytes): 58 | data2 = b'...(%d)' % len(data) or data 59 | else: 60 | data1 = str(data1) 61 | data2 = f'...({len(data)})' or data 62 | return str(data1 + data2) 63 | 64 | class Header(object): 65 | # Chunk type 0 = FULL 66 | # Chunk type 1 = MESSAGE 67 | # Chunk type 2 = TIME 68 | # Chunk type 3 = SEPARATOR 69 | FULL, MESSAGE, TIME, SEPARATOR, MASK = 0x00, 0x40, 0x80, 0xC0, 0xC0 70 | 71 | def __init__(self, channel=0, time=0, size=None, type=None, streamId=0): 72 | 73 | self.channel = channel # in fact, this will be the fmt + cs id 74 | self.time = time # timestamp[delta] 75 | self.size = size # message length 76 | self.type = type # message type id 77 | self.streamId = streamId # message stream id 78 | 79 | if (channel < 64): 80 | self.hdrdata = struct.pack('>B', channel) 81 | elif (channel < 320): 82 | self.hdrdata = b'\x00' + struct.pack('>B', channel - 64) 83 | else: 84 | self.hdrdata = b'\x01' + struct.pack('>H', channel - 64) 85 | 86 | def toBytes(self, control): 87 | data = (self.hdrdata[0] | control).to_bytes(1, 'big') 88 | if len(self.hdrdata) >= 2: 89 | data += self.hdrdata[1:] 90 | 91 | # if the chunk type is not 3 92 | if control != Header.SEPARATOR: 93 | data += struct.pack('>I', self.time if self.time < 94 | 0xFFFFFF else 0xFFFFFF)[1:] # add time in 3 bytes 95 | # if the chunk type is not 2 96 | if control != Header.TIME: 97 | data += struct.pack('>I', self.size)[1:] # add size in 3 bytes 98 | data += struct.pack('>B', self.type) # add type in 1 byte 99 | # if the chunk type is not 1 100 | if control != Header.MESSAGE: 101 | # add streamId in little-endian 4 bytes 102 | data += struct.pack('= 104 | # 16777215 105 | if self.time >= 0xFFFFFF: 106 | data += struct.pack('>I', self.time) 107 | return data 108 | 109 | def __repr__(self): 110 | return ( 111 | f"
") 112 | 113 | def dup(self): 114 | return Header( 115 | channel=self.channel, 116 | time=self.time, 117 | size=self.size, 118 | type=self.type, 119 | streamId=self.streamId) 120 | 121 | class Message(object): 122 | # message types: RPC3, DATA3,and SHAREDOBJECT3 are used with AMF3 123 | CHUNK_SIZE, ABORT, ACK, USER_CONTROL, WIN_ACK_SIZE, SET_PEER_BW, AUDIO, VIDEO, DATA3, SHAREDOBJ3, RPC3, DATA, SHAREDOBJ, RPC, AGGREGATE = \ 124 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, 0x09, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x16 125 | type_name = dict( 126 | enumerate( 127 | 'unknown chunk-size abort ack user-control win-ack-size set-peer-bw unknown audio video unknown unknown unknown unknown unknown data3 sharedobj3 rpc3 data sharedobj rpc unknown aggregate'.split())) 128 | 129 | def __init__(self, hdr=None, data=''): 130 | self.header, self.data = hdr or Header(), data 131 | 132 | # define properties type, streamId and time to access 133 | # self.header.(property) 134 | def _gtype(self): 135 | return self.header.type 136 | 137 | def _stype(self, type): 138 | self.header.type = type 139 | 140 | type = property(fget=_gtype, fset=_stype) 141 | 142 | def _gstreamId(self): 143 | return self.header.streamId 144 | 145 | def _sstreamId(self, streamId): 146 | self.header.streamId = streamId 147 | 148 | streamId = property(fget=_gstreamId, fset=_sstreamId) 149 | 150 | def _gtime(self): 151 | return self.header.time 152 | 153 | def _stime(self, time): 154 | self.header.time = time 155 | 156 | time = property(fget=_gtime, fset=_stime) 157 | 158 | @property 159 | def size(self): return len(self.data) 160 | 161 | def __repr__(self): 162 | return (f"") 163 | 164 | def dup(self): 165 | return Message(self.header.dup(), self.data[:]) 166 | 167 | 168 | class Command(object): 169 | ''' Class for command / data messages''' 170 | 171 | def __init__( 172 | self, 173 | type=Message.RPC, 174 | name=None, 175 | id=None, 176 | tm=0, 177 | cmdData=None, 178 | args=[]): 179 | '''Create a new command with given type, name, id, cmdData and args list.''' 180 | self.type, self.name, self.id, self.time, self.cmdData, self.args = type, name, id, tm, cmdData, args[ 181 | :] 182 | 183 | def __repr__(self): 184 | return (f"") 185 | 186 | def setArg(self, arg): 187 | self.args.append(arg) 188 | 189 | def getArg(self, index): 190 | return self.args[index] 191 | 192 | @classmethod 193 | def fromMessage(cls, message): 194 | ''' initialize from a parsed RTMP message''' 195 | assert ( 196 | message.type in [ 197 | Message.RPC, 198 | Message.RPC3, 199 | Message.DATA, 200 | Message.DATA3]) 201 | 202 | length = len(message.data) 203 | if length == 0: 204 | raise ValueError('zero length message data') 205 | 206 | if message.type == Message.RPC3 or message.type == Message.DATA3: 207 | assert message.data[0] == b'\x00' # must be 0 in AMF3 208 | data = message.data[1:] 209 | else: 210 | data = message.data 211 | 212 | #from pyamf import remoting 213 | amfReader = amf.AMF0(data) 214 | inst = cls() 215 | inst.type = message.type 216 | inst.time = message.time 217 | inst.name = amfReader.read() # first field is command name 218 | 219 | try: 220 | if message.type == Message.RPC or message.type == Message.RPC3: 221 | inst.id = amfReader.read() # second field *may* be message id 222 | inst.cmdData = amfReader.read() # third is command data 223 | else: 224 | inst.id = 0 225 | inst.args = [] # others are optional 226 | while True: 227 | inst.args.append(amfReader.read()) # amfReader.read()) 228 | except EOFError: 229 | pass 230 | return inst 231 | 232 | def toMessage(self): 233 | msg = Message() 234 | assert self.type 235 | msg.type = self.type 236 | msg.time = self.time 237 | output = amf.AMFBytesIO() 238 | amfWriter = amf.AMF0(output) 239 | amfWriter.write(self.name) 240 | if msg.type == Message.RPC or msg.type == Message.RPC3: 241 | amfWriter.write(self.id) 242 | amfWriter.write(self.cmdData) 243 | for arg in self.args: 244 | amfWriter.write(arg) 245 | output.seek(0) 246 | # hexdump.hexdump(output) 247 | # output.seek(0) 248 | if msg.type == Message.RPC3 or msg.type == Message.DATA3: 249 | data = b'\x00' + output.read() 250 | else: 251 | data = output.read() 252 | msg.data = data 253 | output.close() 254 | return msg -------------------------------------------------------------------------------- /handshake.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | import hashlib 4 | import hmac 5 | 6 | MESSAGE_FORMAT_0 = 0 7 | MESSAGE_FORMAT_1 = 1 8 | MESSAGE_FORMAT_2 = 2 9 | 10 | RTMP_SIG_SIZE = 1536 11 | SHA256DL = 32 12 | 13 | RandomCrud = bytes([ 14 | 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8, 15 | 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57, 16 | 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab, 17 | 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae 18 | ]) 19 | 20 | GenuineFMSConst = 'Genuine Adobe Flash Media Server 001' 21 | GenuineFMSConstCrud = bytes(GenuineFMSConst, 'utf8') + RandomCrud 22 | 23 | GenuineFPConst = 'Genuine Adobe Flash Player 001' 24 | GenuineFPConstCrud = bytes(GenuineFPConst, 'utf8') + RandomCrud 25 | 26 | 27 | def calcHmac(data, key): 28 | return hmac.new(key, data, hashlib.sha256).digest() 29 | 30 | def GetClientGenuineConstDigestOffset(buf): 31 | offset = buf[0] + buf[1] + buf[2] + buf[3] 32 | offset = (offset % 728) + 12 33 | return offset 34 | 35 | def GetServerGenuineConstDigestOffset(buf): 36 | offset = buf[0] + buf[1] + buf[2] + buf[3] 37 | offset = (offset % 728) + 776 38 | return offset 39 | 40 | def detectClientMessageFormat(clientsig): 41 | sdl = GetServerGenuineConstDigestOffset(clientsig[772:776]) 42 | msg = clientsig[:sdl] + clientsig[sdl + SHA256DL:] 43 | computedSignature = calcHmac(msg, bytes(GenuineFPConst, 'utf8')) 44 | providedSignature = clientsig[sdl:sdl + SHA256DL] 45 | if computedSignature == providedSignature: 46 | return MESSAGE_FORMAT_2 47 | sdl = GetClientGenuineConstDigestOffset(clientsig[8:12]) 48 | msg = clientsig[:sdl] + clientsig[sdl + SHA256DL:] 49 | computedSignature = calcHmac(msg, bytes(GenuineFPConst, 'utf8')) 50 | providedSignature = clientsig[sdl:sdl + SHA256DL] 51 | if computedSignature == providedSignature: 52 | return MESSAGE_FORMAT_1 53 | return MESSAGE_FORMAT_0 54 | 55 | def generateS1(messageFormat): 56 | randomBytes = bytes([random.randint(0, 255) for _ in range(RTMP_SIG_SIZE - 8)]) 57 | handshakeBytes = bytes([0, 0, 0, 0, 1, 2, 3, 4]) + randomBytes 58 | 59 | if messageFormat == 1: 60 | serverDigestOffset = GetClientGenuineConstDigestOffset(handshakeBytes[8:12]) 61 | else: 62 | serverDigestOffset = GetServerGenuineConstDigestOffset(handshakeBytes[772:776]) 63 | 64 | msg = handshakeBytes[:serverDigestOffset] + handshakeBytes[serverDigestOffset + SHA256DL:] 65 | hashValue = calcHmac(msg, bytes(GenuineFMSConst, 'utf8')) 66 | handshakeBytes = handshakeBytes[:serverDigestOffset] + hashValue + handshakeBytes[serverDigestOffset + SHA256DL:] 67 | return handshakeBytes 68 | 69 | def generateS2(messageFormat, clientsig): 70 | randomBytes = bytes([random.randint(0, 255) for _ in range(RTMP_SIG_SIZE - 32)]) 71 | 72 | if messageFormat == 1: 73 | challengeKeyOffset = GetClientGenuineConstDigestOffset(clientsig[8:12]) 74 | else: 75 | challengeKeyOffset = GetServerGenuineConstDigestOffset(clientsig[772:776]) 76 | 77 | challengeKey = clientsig[challengeKeyOffset:challengeKeyOffset + 32] 78 | hashValue = calcHmac(challengeKey, bytes(GenuineFMSConstCrud)) 79 | signature = calcHmac(randomBytes, hashValue) 80 | s2Bytes = randomBytes + signature 81 | return s2Bytes 82 | 83 | def generateS0S1S2(clientsig): 84 | clientType = bytes([3]) 85 | messageFormat = detectClientMessageFormat(clientsig) 86 | if messageFormat == MESSAGE_FORMAT_0: 87 | allBytes = clientType + clientsig + clientsig 88 | else: 89 | s1Bytes = generateS1(messageFormat) 90 | s2Bytes = generateS2(messageFormat, clientsig) 91 | allBytes = clientType + s1Bytes + s2Bytes 92 | return allBytes -------------------------------------------------------------------------------- /rtmp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import amf 4 | import av 5 | import common 6 | import struct 7 | from typing import Optional 8 | import time 9 | import handshake 10 | import uuid 11 | 12 | # Config 13 | LogLevel = logging.INFO 14 | 15 | # RTMP packet types 16 | RTMP_TYPE_SET_CHUNK_SIZE = 1 # Set Chunk Size message (RTMP_PACKET_TYPE_CHUNK_SIZE 0x01) - The Set Chunk Size message is used to inform the peer about the chunk size for subsequent chunks. 17 | RTMP_TYPE_ABORT = 2 # Abort message - The Abort message is used to notify the peer to discard a partially received message. 18 | RTMP_TYPE_ACKNOWLEDGEMENT = 3 # Acknowledgement message (RTMP_PACKET_TYPE_BYTES_READ_REPORT 0x03) - The Acknowledgement message is used to report the number of bytes received so far. 19 | RTMP_PACKET_TYPE_CONTROL = 4 # Control message - Control messages carry protocol control information between the RTMP peers. 20 | RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE = 5 # Window Acknowledgement Size message (RTMP_PACKET_TYPE_SERVER_BW 0x05) - The Window Acknowledgement Size message is used to inform the peer about the window acknowledgement size. 21 | RTMP_TYPE_SET_PEER_BANDWIDTH = 6 # Set Peer Bandwidth message (RTMP_PACKET_TYPE_CLIENT_BW 0x06) - The Set Peer Bandwidth message is used to inform the peer about the available outgoing bandwidth. 22 | RTMP_TYPE_AUDIO = 8 # Audio data message (RTMP_PACKET_TYPE_AUDIO 0x08) - The Audio data message carries audio data. 23 | RTMP_TYPE_VIDEO = 9 # Video data message (RTMP_PACKET_TYPE_VIDEO 0x09) - The Video data message carries video data. 24 | RTMP_TYPE_FLEX_STREAM = 15 # Flex Stream message (RTMP_PACKET_TYPE_FLEX_STREAM_SEND 0x0F) - The Flex Stream message is used to send AMF3-encoded stream metadata. 25 | RTMP_TYPE_FLEX_OBJECT = 16 # Flex Shared Object message (RTMP_PACKET_TYPE_FLEX_SHARED_OBJECT 0x10) - The Flex Shared Object message is used to send AMF3-encoded shared object data. 26 | RTMP_TYPE_FLEX_MESSAGE = 17 # Flex Message message (RTMP_PACKET_TYPE_FLEX_MESSAGE 0x11) - The Flex Message message is used to send AMF3-encoded RPC or shared object events. 27 | RTMP_TYPE_DATA = 18 # AMF0 Data message (RTMP_PACKET_TYPE_INFO 0x12) - The AMF0 Data message carries generic AMF0-encoded data. 28 | RTMP_TYPE_SHARED_OBJECT = 19 # AMF0 Shared Object message (RTMP_PACKET_TYPE_INFO 0x12) - The AMF0 Shared Object message carries AMF0-encoded shared object data. 29 | RTMP_TYPE_INVOKE = 20 # AMF0 Invoke message (RTMP_PACKET_TYPE_SHARED_OBJECT 0x13) - The AMF0 Invoke message is used for remote procedure calls (RPC) or command execution. 30 | RTMP_TYPE_METADATA = 22 # Metadata message (RTMP_PACKET_TYPE_FLASH_VIDEO 0x16) - The Metadata message carries metadata related to the media stream. 31 | 32 | RTMP_CHUNK_TYPE_0 = 0 # 11-bytes: timestamp(3) + length(3) + stream type(1) + stream id(4) 33 | RTMP_CHUNK_TYPE_1 = 1 # 7-bytes: delta(3) + length(3) + stream type(1) 34 | RTMP_CHUNK_TYPE_2 = 2 # 3-bytes: delta(3) 35 | RTMP_CHUNK_TYPE_3 = 3 # 0-byte 36 | 37 | # RTMP channel constants 38 | RTMP_CHANNEL_PROTOCOL = 2 39 | RTMP_CHANNEL_INVOKE = 3 40 | RTMP_CHANNEL_AUDIO = 4 41 | RTMP_CHANNEL_VIDEO = 5 42 | RTMP_CHANNEL_DATA = 6 43 | 44 | # Protocol channel ID 45 | PROTOCOL_CHANNEL_ID = 2 46 | 47 | MAX_CHUNK_SIZE = 10485760 48 | 49 | # Constants for Packet Types 50 | PacketTypeSequenceStart = 0 # Represents the start of a video/audio sequence 51 | PacketTypeCodedFrames = 1 # Represents a video/audio frame 52 | PacketTypeSequenceEnd = 2 # Represents the end of a video/audio sequence 53 | PacketTypeCodedFramesX = 3 # Represents an extended video/audio frame 54 | PacketTypeMetadata = 4 # Represents a packet with metadata 55 | PacketTypeMPEG2TSSequenceStart = 5 # Represents the start of an MPEG2-TS video/audio sequence 56 | 57 | # Constants for FourCC values 58 | FourCC_AV1 = b'av01' # AV1 video codec 59 | FourCC_VP9 = b'vp09' # VP9 video codec 60 | FourCC_HEVC = b'hvc1' # HEVC video codec 61 | 62 | # Dictionary to store live users 63 | LiveUsers = {} 64 | # Dictionary to store player users 65 | PlayerUsers = {} 66 | 67 | # Custom exception for disconnecting clients 68 | class DisconnectClientException(Exception): 69 | pass 70 | 71 | # Class representing the state of a connected client 72 | class ClientState: 73 | def __init__(self): 74 | self.id = str(uuid.uuid4()) 75 | self.client_ip = '0.0.0.0' 76 | 77 | # RTMP properties 78 | self.chunk_size = 128 # Default chunk size 79 | self.out_chunk_size = 4096 # Default out chunk size 80 | self.window_acknowledgement_size = 5000000 # Default window acknowledgement size 81 | self.peer_bandwidth = 0 # Default peer bandwidth 82 | 83 | # RTMP Invoke Connect Data 84 | self.flashVer = 'FMLE/3.0 (compatible; FMSc/1.0)' 85 | self.connectType = 'nonprivate' 86 | self.tcUrl = '' 87 | self.swfUrl = '' 88 | self.app = '' 89 | self.objectEncoding = 0 90 | 91 | self.reader: Optional[asyncio.StreamReader] = None 92 | self.writer: Optional[asyncio.StreamWriter] = None 93 | 94 | self.lastWriteHeaders = dict() 95 | self.nextChannelId = PROTOCOL_CHANNEL_ID + 1 96 | self.streams = 0 97 | self._time0 = time.time() 98 | self.stream_mode = None 99 | 100 | self.streamPath = '' 101 | self.publishStreamId = 0 102 | self.publishStreamPath = '' 103 | self.CacheState = 0 104 | self.IncomingPackets = {} 105 | self.Players = {} 106 | 107 | # Meta Data 108 | self.metaData = None 109 | self.metaDataPayload = None 110 | self.audioSampleRate = 0 111 | self.audioChannels = 1 112 | self.videoWidth = 0 113 | self.videoHeight = 0 114 | self.videoFps = 0 115 | self.Bitrate = 0 116 | 117 | self.isFirstAudioReceived = False 118 | self.isReceiveVideo = False 119 | self.aacSequenceHeader = None 120 | self.avcSequenceHeader = None 121 | self.audioCodec = 0 122 | self.audioCodecName = '' 123 | self.audioProfileName = '' 124 | self.videoCodec = 0 125 | self.videoCodecName = '' 126 | self.videoProfileName = '' 127 | self.videoCount = 0 128 | self.videoLevel = 0 129 | 130 | self.inAckSize = 0 131 | self.inLastAck = 0 132 | 133 | # RTMP server class 134 | class RTMPServer: 135 | def __init__(self, host='0.0.0.0', port=1935): 136 | # Socket 137 | # Server socket properties 138 | self.host = host 139 | self.port = port 140 | self.client_states = {} 141 | 142 | self.logger = logging.getLogger('RTMPServer') 143 | self.logger.setLevel(LogLevel) 144 | 145 | async def handle_client(self, reader, writer): 146 | # Create a new client state for each connected client 147 | client_state = ClientState() 148 | self.client_states[client_state.id] = client_state 149 | self.client_states[client_state.id].clientID = client_state.id 150 | 151 | self.client_states[client_state.id].reader = reader 152 | self.client_states[client_state.id].writer = writer 153 | 154 | self.client_states[client_state.id].client_ip = writer.get_extra_info('peername') 155 | self.logger.info("New client connected: %s", self.client_states[client_state.id].client_ip) 156 | 157 | # Perform RTMP handshake 158 | try: 159 | await asyncio.wait_for(self.perform_handshake(client_state.id), timeout=5) 160 | except asyncio.TimeoutError: 161 | self.logger.error("Handshake timeout. Closing connection: %s", self.client_states[client_state.id].client_ip) 162 | await self.disconnect(client_state.id) 163 | return 164 | 165 | # Process RTMP messages 166 | while True: 167 | try: 168 | await self.get_chunk_data(client_state.id) 169 | 170 | except asyncio.TimeoutError: 171 | self.logger.debug("Connection timeout. Closing connection: %s", self.client_states[client_state.id].client_ip) 172 | break 173 | 174 | except DisconnectClientException: 175 | self.logger.debug("Disconnecting client: %s", self.client_states[client_state.id].client_ip) 176 | break 177 | 178 | except ConnectionAbortedError as e: 179 | self.logger.debug("Connection aborted by client: %s", self.client_states[client_state.id].client_ip) 180 | break 181 | 182 | except Exception as e: 183 | self.logger.error("An error occurred: %s", str(e)) 184 | break 185 | 186 | await self.disconnect(client_state.id) 187 | 188 | async def disconnect(self, client_id): 189 | # Close the client connection 190 | client_state = self.client_states[client_id] 191 | if client_state.stream_mode == 'live': 192 | # Finish Stream for players! 193 | print("NEED DISCONNECT Players!") 194 | 195 | client_ip = client_state.client_ip 196 | for app in LiveUsers: 197 | if LiveUsers[app]['client_id'] == client_id: 198 | del LiveUsers[app] 199 | break 200 | 201 | client_state['IncomingPackets'].clear() 202 | 203 | del self.client_states[client_id] 204 | try: 205 | client_state.writer.close() 206 | await client_state.writer.wait_closed() 207 | self.logger.info("Client disconnected: %s", client_ip) 208 | except Exception as e: 209 | # Handle the exception here, perform other tasks, or log the error. 210 | self.logger.error(f"Error occurred while disconnecting client: {e}") 211 | 212 | 213 | async def get_chunk_data(self, client_id): 214 | # Read a chunk of data from the client 215 | client_state = self.client_states[client_id] 216 | try: 217 | chunk_data = await client_state.reader.readexactly(1) 218 | if not chunk_data: 219 | raise DisconnectClientException() 220 | 221 | cid = chunk_data[0] & 0b00111111 222 | 223 | # Chunk Basic Header field may be 1, 2, or 3 bytes, depending on the chunk stream ID. 224 | if cid == 0: # ChunkBasicHeader: 2 225 | chunk_data += await client_state.reader.readexactly(1) # Need read 1 more packet 226 | cid = 64 + chunk_data[1] # Chunk stream IDs 64-319 can be encoded in the 2-byte form of the header 227 | elif cid == 1: #ChunkBasicHeader: 3 228 | chunk_data += await client_state.reader.readexactly(2) # Need read 2 more packets 229 | cid = (64 + chunk_data[1] + chunk_data[2]) << 8 # Chunk stream IDs 64-65599 can be encoded in the 3-byte version of this field 230 | 231 | chunk_full = bytearray(chunk_data) 232 | fmt = (chunk_data[0] & 0b11000000) >> 6 233 | 234 | if not cid in client_state.IncomingPackets: 235 | client_state.IncomingPackets[cid] = self.createPacket(cid, fmt) 236 | 237 | # I'm afraid I suffer from memory leaks. :D 238 | client_state.IncomingPackets[cid]['last_received_time'] = time.time() 239 | self.clearPayloadIfTimeout(client_id, 120) 240 | 241 | header_data = bytearray() 242 | # Get Message Timestamp for FMT 0, 1, 2 243 | if fmt <= RTMP_CHUNK_TYPE_2: 244 | timestamp_bytes = await client_state.reader.readexactly(3) 245 | header_data += timestamp_bytes 246 | client_state.IncomingPackets[cid]['timestamp'] = int.from_bytes(timestamp_bytes, byteorder='big') 247 | del timestamp_bytes 248 | 249 | # Get Message Length and Message Type for FMT 0, 1 250 | if fmt <= RTMP_CHUNK_TYPE_1: 251 | length_bytes = await client_state.reader.readexactly(3) 252 | header_data += length_bytes 253 | type_bytes = await client_state.reader.readexactly(1) 254 | header_data += type_bytes 255 | client_state.IncomingPackets[cid]['payload_length'] = int.from_bytes(length_bytes, byteorder='big') 256 | client_state.IncomingPackets[cid]['msg_type_id'] = int.from_bytes(type_bytes, byteorder='big') 257 | client_state.IncomingPackets[cid]['payload'] = bytearray() 258 | del length_bytes 259 | del type_bytes 260 | 261 | # Get Message Stream ID for FMT 0 262 | if fmt == RTMP_CHUNK_TYPE_0: 263 | streamID_bytes = await client_state.reader.readexactly(4) 264 | header_data += streamID_bytes 265 | client_state.IncomingPackets[cid]['msg_stream_id'] = int.from_bytes(streamID_bytes, byteorder='big') 266 | del streamID_bytes 267 | 268 | chunk_full += header_data 269 | 270 | # Set Main Packet Headers and payload_length for FMT 0, 1 271 | if fmt <= RTMP_CHUNK_TYPE_1: 272 | # client_state.IncomingPackets[cid]['basic_header'] = chunk_data 273 | # client_state.IncomingPackets[cid]['header'] = header_data 274 | payload_length = client_state.IncomingPackets[cid]['payload_length'] 275 | 276 | # Calculate Payload Remaining length for FMT 2,3 277 | if fmt > RTMP_CHUNK_TYPE_1: 278 | payload_length = client_state.IncomingPackets[cid]['payload_length'] - len(client_state.IncomingPackets[cid]['payload']) 279 | 280 | # Check message type id 281 | if RTMP_TYPE_METADATA < client_state.IncomingPackets[cid]['msg_type_id']: 282 | self.logger.error("Invalid Packet Type: %s", str(client_state.IncomingPackets[cid]['msg_type_id'])) 283 | raise DisconnectClientException() 284 | 285 | # Messages with type=3 should never have ext timestamp field according to standard. However that's not always the case in real life 286 | if client_state.IncomingPackets[cid]['timestamp'] == 0xffffff: # Max Value check (16777215), Need to read extended timestamp 287 | extended_timestamp_bytes = await client_state.reader.readexactly(4) 288 | chunk_full += extended_timestamp_bytes 289 | client_state.IncomingPackets[cid]['extended_timestamp'] = int.from_bytes(extended_timestamp_bytes, byteorder='big') 290 | del extended_timestamp_bytes 291 | 292 | client_state.inAckSize += len(chunk_full) 293 | 294 | self.logger.debug(f"FMT: {fmt}, CID: {cid}, Message Length: {payload_length}, Timestamp: {client_state.IncomingPackets[cid]['timestamp']}") 295 | 296 | if payload_length > 0: 297 | payload_length = min(client_state.chunk_size, payload_length) 298 | payload = await client_state.reader.readexactly(payload_length) 299 | client_state.inAckSize += len(payload) 300 | client_state.IncomingPackets[cid]['payload'] += payload 301 | del payload 302 | else: 303 | # I'm not sure. In some cases, I may need to disconnect the client, while in other cases, I won't. I will ignore the issue and proceed to the next packet, but I will clear the payload. If invalid data continues, it may result in a disconnection when processing subsequent packets. 304 | self.logger.error(f"Invalid Length (ZERO!), FMT: {fmt}, CID: {cid}, Message Length: {payload_length}, Timestamp: {client_state.IncomingPackets[cid]['timestamp']}") 305 | client_state.IncomingPackets[cid]['payload'] = bytearray() 306 | return 307 | 308 | if client_state.inAckSize >= 0xF0000000: 309 | client_state.inAckSize = 0 310 | client_state.inLastAck = 0 311 | 312 | # Delete some variables for fun! 313 | del chunk_data 314 | del chunk_full 315 | del payload_length 316 | del header_data 317 | 318 | if len(client_state.IncomingPackets[cid]['payload']) >= client_state.IncomingPackets[cid]['payload_length']: 319 | rtmp_packet = { 320 | "header": { 321 | "fmt": client_state.IncomingPackets[cid]["fmt"], 322 | "cid": client_state.IncomingPackets[cid]["cid"], 323 | "timestamp": client_state.IncomingPackets[cid]["timestamp"], 324 | "length": client_state.IncomingPackets[cid]["payload_length"], 325 | "type": client_state.IncomingPackets[cid]["msg_type_id"], 326 | "stream_id": client_state.IncomingPackets[cid]["msg_stream_id"] 327 | }, 328 | "clock": 0, 329 | "payload": client_state.IncomingPackets[cid]['payload'] 330 | } 331 | client_state.IncomingPackets[cid]['payload'] = bytearray() 332 | await self.handle_rtmp_packet(client_id, rtmp_packet) 333 | del rtmp_packet 334 | 335 | # Send ACK If needed! 336 | if(client_state.window_acknowledgement_size > 0 and client_state.inAckSize - client_state.inLastAck >= client_state.window_acknowledgement_size): 337 | client_state.inLastAck = client_state.inAckSize 338 | await self.send_ack(client_id, client_state.inAckSize) 339 | 340 | except Exception as e: 341 | self.logger.error("An error occurred: %s", str(e)) 342 | raise DisconnectClientException() 343 | 344 | # This function is designed to safely stop memory leaks if they exist. It ensures that memory is properly managed and prevents any potential leaks from causing issues. 345 | def clearPayloadIfTimeout(self, client_id, packet_timeout=30): 346 | client_state = self.client_states[client_id] 347 | current_time = time.time() 348 | for cid, packet in client_state.IncomingPackets.items(): 349 | if 'last_received_time' in packet and current_time - packet['last_received_time'] >= packet_timeout: 350 | packet['payload'] = bytearray() # Clear the payload 351 | 352 | def createPacket(self, cid, fmt): 353 | out = {} 354 | out['fmt'] = fmt 355 | out['cid'] = cid 356 | # out['basic_header'] = bytearray() 357 | # out['header'] = bytearray() 358 | 359 | out['timestamp'] = 0 360 | out['extended_timestamp'] = 0 361 | out['payload_length'] = 0 362 | out['msg_type_id'] = 0 363 | out['msg_stream_id'] = 0 364 | out['payload'] = bytearray() 365 | out['last_received_time'] = time.time() 366 | 367 | return out 368 | 369 | async def perform_handshake(self, client_id): 370 | # Perform the RTMP handshake with the client 371 | client_state = self.client_states[client_id] 372 | 373 | c0_data = await client_state.reader.readexactly(1) 374 | if c0_data != bytes([0x03]) and c0_data != bytes([0x06]): 375 | client_state.writer.close() 376 | await client_state.writer.wait_closed() 377 | self.logger.info("Invalid Handshake, Client disconnected: %s", self.client_ip) 378 | 379 | c1_data = await client_state.reader.readexactly(1536) 380 | clientType = bytes([3]) 381 | messageFormat = handshake.detectClientMessageFormat(c1_data) 382 | if messageFormat == handshake.MESSAGE_FORMAT_0: 383 | await self.send(client_id, clientType) 384 | s1_data = c1_data 385 | s2_data = c1_data 386 | await self.send(client_id, c1_data) 387 | await client_state.reader.readexactly(len(s1_data)) 388 | await self.send(client_id, s2_data) 389 | else: 390 | s1_data = handshake.generateS1(messageFormat) 391 | s2_data = handshake.generateS2(messageFormat, c1_data) 392 | data = clientType + s1_data + s2_data 393 | client_state.writer.write(data) 394 | s1_data = await client_state.reader.readexactly(len(s1_data)) 395 | 396 | self.logger.debug("Handshake done!") 397 | 398 | async def handle_rtmp_packet(self, client_id, rtmp_packet): 399 | # Handle an RTMP packet from the client 400 | # client_state = self.client_states[client_id] 401 | 402 | # Extract information from rtmp_packet and process as needed 403 | msg_type_id = rtmp_packet["header"]["type"] 404 | payload = rtmp_packet["payload"] 405 | # self.logger.debug("Received RTMP packet:") 406 | # self.logger.debug(" RTMP Packet Type: %s", msg_type_id) 407 | 408 | if msg_type_id == RTMP_TYPE_SET_CHUNK_SIZE: 409 | self.handle_chunk_size_message(client_id, payload) 410 | elif msg_type_id == RTMP_TYPE_ACKNOWLEDGEMENT: 411 | await self.handle_bytes_read_report(client_id, payload) 412 | # elif msg_type_id == RTMP_PACKET_TYPE_CONTROL: 413 | # self.handle_control_message(payload) 414 | elif msg_type_id == RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: 415 | self.handle_window_acknowledgement_size(client_id, payload) 416 | elif msg_type_id == RTMP_TYPE_SET_PEER_BANDWIDTH: 417 | self.handle_set_peer_bandwidth(client_id, payload) 418 | elif msg_type_id == RTMP_TYPE_AUDIO: 419 | await self.handle_audio_data(client_id, rtmp_packet) 420 | elif msg_type_id == RTMP_TYPE_VIDEO: 421 | await self.handle_video_data(client_id, rtmp_packet) 422 | # elif msg_type_id == RTMP_TYPE_FLEX_STREAM: 423 | # self.handle_flex_stream_message(payload) 424 | # elif msg_type_id == RTMP_TYPE_FLEX_OBJECT: 425 | # self.handle_flex_shared_object_message(payload) 426 | elif msg_type_id == RTMP_TYPE_FLEX_MESSAGE: 427 | invoke_message = self.parse_amf0_invoke_message(rtmp_packet) 428 | await self.handle_invoke_message(client_id, invoke_message) 429 | elif msg_type_id == RTMP_TYPE_DATA: 430 | await self.handle_amf_data(client_id, rtmp_packet) 431 | # elif msg_type_id == RTMP_TYPE_SHARED_OBJECT: 432 | # self.handle_amf0_shared_object_message(payload) 433 | elif msg_type_id == RTMP_TYPE_INVOKE: 434 | invoke_message = self.parse_amf0_invoke_message(rtmp_packet) 435 | await self.handle_invoke_message(client_id, invoke_message) 436 | # elif msg_type_id == RTMP_TYPE_METADATA: 437 | # self.handle_metadata_message(payload) 438 | else: 439 | self.logger.debug("Unsupported RTMP packet type: %s", msg_type_id) 440 | 441 | async def handle_video_data(self, client_id, rtmp_packet): 442 | # Handle video data in an RTMP packet 443 | client_state = self.client_states[client_id] 444 | payload = rtmp_packet['payload'] 445 | isExHeader = (payload[0] >> 4 & 0b1000) != 0 446 | frame_type = payload[0] >> 4 & 0b0111 447 | codec_id = payload[0] & 0x0f 448 | packetType = payload[0] & 0x0f 449 | # Handle Video Data! 450 | if isExHeader: 451 | if packetType == PacketTypeMetadata: 452 | pass 453 | elif packetType == PacketTypeSequenceEnd: 454 | pass 455 | 456 | FourCC = payload[1:5] 457 | if FourCC == FourCC_HEVC: 458 | codec_id = 12 459 | if packetType == PacketTypeSequenceStart: 460 | payload[0] = 0x1c 461 | payload[1:5] = b'\x00\x00\x00\x00' 462 | elif packetType in [PacketTypeCodedFrames, PacketTypeCodedFramesX]: 463 | if packetType == PacketTypeCodedFrames: 464 | payload = payload[3:] 465 | else: 466 | payload[2:5] = b'\x00\x00\x00' 467 | payload[0] = (frame_type << 4) | 0x0c 468 | payload[1] = 1 469 | elif FourCC == FourCC_AV1: 470 | codec_id = 13 471 | if packetType == PacketTypeSequenceStart: 472 | payload[0] = 0x1d 473 | payload[1:5] = b'\x00\x00\x00\x00' 474 | elif packetType == PacketTypeMPEG2TSSequenceStart: 475 | pass 476 | elif packetType == PacketTypeCodedFrames: 477 | payload[0] = (frame_type << 4) | 0x0d 478 | payload[1] = 1 479 | payload[2:5] = b'\x00\x00\x00' 480 | else: 481 | self.logger.debug("unsupported extension header") 482 | return 483 | 484 | if codec_id in [7, 12, 13]: 485 | if frame_type == 1 and payload[1] == 0: 486 | client_state.avcSequenceHeader = bytearray(payload) 487 | info = av.readAVCSpecificConfig(client_state.avcSequenceHeader) 488 | client_state.videoWidth = info['width'] 489 | client_state.videoHeight = info['height'] 490 | client_state.videoProfileName = av.getAVCProfileName(info) 491 | client_state.videoLevel = info['level'] 492 | self.logger.info("CodecID: %d, Video Level: %f, Profile Name: %s, Width: %d, Height: %d, Profile: %d", 493 | codec_id, client_state.videoLevel, client_state.videoProfileName, 494 | client_state.videoWidth, client_state.videoHeight, info['profile']) 495 | 496 | if client_state.videoCodec == 0: 497 | client_state.videoCodec = codec_id 498 | client_state.videoCodecName = common.VIDEO_CODEC_NAME[codec_id] 499 | self.logger.info("Codec Name: %s", client_state.videoCodecName) 500 | 501 | 502 | async def handle_audio_data(self, client_id, rtmp_packet): 503 | client_state = self.client_states[client_id] 504 | payload = rtmp_packet['payload'] 505 | sound_format = (payload[0] >> 4) & 0x0f 506 | sound_type = payload[0] & 0x01 507 | sound_size = (payload[0] >> 1) & 0x01 508 | sound_rate = (payload[0] >> 2) & 0x03 509 | 510 | if client_state.audioCodec == 0: 511 | client_state.audioCodec = sound_format; 512 | client_state.audioCodecName = av.AUDIO_CODEC_NAME[sound_format]; 513 | client_state.audioSampleRate = av.AUDIO_SOUND_RATE[sound_rate]; 514 | client_state.audioChannels = sound_type + 1; 515 | 516 | if sound_format == 4: 517 | # Nellymoser 16 kHz 518 | client_state.audioSampleRate = 16000 519 | elif sound_format in (5, 7, 8): 520 | # Nellymoser 8 kHz | G.711 A-law | G.711 mu-law 521 | client_state.audioSampleRate = 8000 522 | elif sound_format == 11: 523 | # Speex 524 | client_state.audioSampleRate = 16000 525 | elif sound_format == 14: 526 | # MP3 8 kHz 527 | client_state.audioSampleRate = 8000 528 | 529 | if (sound_format == 10 or sound_format == 13) and payload[1] == 0: 530 | # cache AAC sequence header 531 | client_state.isFirstAudioReceived = True 532 | client_state.aacSequenceHeader = payload 533 | 534 | if sound_format == 10: 535 | info = av.read_aac_specific_config(client_state.aacSequenceHeader) 536 | client_state.audioProfileName = av.get_aac_profile_name(info) 537 | client_state.audioSampleRate = info['sample_rate'] 538 | client_state.audioChannels = info['sample_rate'] 539 | else: 540 | client_state.audioSampleRate = 48000 541 | client_state.audioChannels = payload[11] 542 | 543 | #write for players 544 | 545 | 546 | def handle_chunk_size_message(self, client_id, payload): 547 | # Handle Chunk Size message 548 | new_chunk_size = int.from_bytes(payload, byteorder='big') 549 | if(MAX_CHUNK_SIZE < new_chunk_size): 550 | self.logger.debug("Chunk size is too big!", new_chunk_size) 551 | raise DisconnectClientException() 552 | 553 | self.client_states[client_id].chunk_size = new_chunk_size 554 | self.logger.debug("Updated chunk size: %d", self.client_states[client_id].chunk_size) 555 | 556 | def handle_window_acknowledgement_size(self, client_id, payload): 557 | # Handle Window Acknowledgement Size message 558 | client_state = self.client_states[client_id] 559 | new_window_acknowledgement_size = int.from_bytes(payload, byteorder='big') 560 | client_state.window_acknowledgement_size = new_window_acknowledgement_size 561 | self.logger.debug("Updated window acknowledgement size: %d", client_state.window_acknowledgement_size) 562 | 563 | def handle_set_peer_bandwidth(self, client_id, payload): 564 | # Handle Set Peer Bandwidth message 565 | client_state = self.client_states[client_id] 566 | bandwidth = int.from_bytes(payload[:4], byteorder='big') 567 | limit_type = payload[4] 568 | client_state.peer_bandwidth = bandwidth 569 | self.logger.debug("Updated peer bandwidth: %d, Limit type: %d", client_state.peer_bandwidth, limit_type) 570 | 571 | async def handle_invoke_message(self, client_id, invoke): 572 | if invoke['cmd'] == 'connect': 573 | self.logger.debug("Received connect invoke") 574 | await self.handle_connect_command(client_id, invoke) 575 | elif invoke['cmd'] == 'releaseStream' or invoke['cmd'] == 'FCPublish'or invoke['cmd'] == 'FCUnpublish' or invoke['cmd'] == 'getStreamLength': 576 | self.logger.debug("Received %s invoke", invoke['cmd']) 577 | return 578 | elif invoke['cmd'] == 'createStream': 579 | self.logger.debug("Received createStream invoke") 580 | await self.response_createStream(client_id, invoke) 581 | elif invoke['cmd'] == 'publish': 582 | self.logger.debug("Received publish invoke") 583 | await self.handle_publish(client_id, invoke) 584 | elif invoke['cmd'] == 'play': 585 | self.logger.debug("Received play invoke") 586 | await self.handle_onPlay(client_id, invoke) 587 | # Need to add and support other CMDs. 588 | else: 589 | self.logger.info("Unsupported invoke command %s!", invoke['cmd']) 590 | 591 | async def handle_onPlay(self, client_id, invoke): 592 | client_state = self.client_states[client_id] 593 | if not client_state.app in LiveUsers: 594 | self.logger.warning("Stream not exists to play!") 595 | await self.sendStatusMessage(client_id, client_state.publishStreamId, "error", "NetStream.Play.BadName", "Stream not exists") 596 | raise DisconnectClientException() 597 | 598 | publisher_id = LiveUsers[client_state.app]['client_id'] 599 | publisher_client_state = self.client_states[publisher_id] 600 | if publisher_client_state.metaDataPayload != None: 601 | # Sending Publisher Meta Data to Player! 602 | output = amf.AMFBytesIO() 603 | amfWriter = amf.AMF0(output) 604 | amfWriter.write('onMetaData') 605 | amfWriter.write(publisher_client_state.metaData) 606 | output.seek(0) 607 | payload = output.read() 608 | streamId = invoke['packet']['header']['stream_id'] 609 | packet_header = common.Header(RTMP_CHANNEL_DATA, 0, len(payload), RTMP_TYPE_DATA, streamId) 610 | response = common.Message(packet_header, payload) 611 | await self.writeMessage(client_id, response) 612 | 613 | async def handle_publish(self, client_id, invoke): 614 | client_state = self.client_states[client_id] 615 | client_state.stream_mode = 'live' if len(invoke['args']) < 2 else invoke['args'][1] # live, record, append 616 | client_state.streamPath = invoke['args'][0] 617 | client_state.publishStreamId = int(invoke['packet']['header']['stream_id']) 618 | client_state.publishStreamPath = "/" + client_state.app + "/" + client_state.streamPath.split("?")[0] 619 | if(client_state.streamPath == None or client_state.streamPath == ''): 620 | self.logger.warning("Stream key is empty!") 621 | await self.sendStatusMessage(client_id, client_state.publishStreamId, "error", "NetStream.publish.Unauthorized", "Authorization required.") 622 | raise DisconnectClientException() 623 | 624 | if client_state.stream_mode == 'live': 625 | if LiveUsers.get(client_state.app) is not None: 626 | self.logger.warning("Stream already publishing!") 627 | await self.sendStatusMessage(client_id, client_state.publishStreamId, "error", "NetStream.Publish.BadName", "Stream already publishing") 628 | raise DisconnectClientException() 629 | 630 | LiveUsers[client_state.app] = { 631 | 'client_id': client_id, 632 | 'stream_mode': client_state.stream_mode, 633 | 'stream_path': client_state.streamPath, 634 | 'publish_stream_id': client_state.publishStreamId, 635 | 'app': client_state.app, 636 | } 637 | 638 | self.logger.info("Publish Request Mode: %s, App: %s, Path: %s, publishStreamPath: %s, StreamID: %s", client_state.stream_mode, client_state.app, client_state.streamPath, client_state.publishStreamPath, str(client_state.publishStreamId)) 639 | await self.sendStatusMessage(client_id, client_state.publishStreamId, "status", "NetStream.Publish.Start", f"{client_state.publishStreamPath} is now published.") 640 | 641 | async def sendStatusMessage(self, client_id, sid, level, code, description): 642 | response = common.Command( 643 | name='onStatus', 644 | id=sid, 645 | tm=self.relativeTime(client_id), 646 | args=[ 647 | amf.Object( 648 | level=level, 649 | code=code, 650 | description=description, 651 | details=None)]) 652 | 653 | message = response.toMessage() 654 | self.logger.debug("Sending onStatus response!") 655 | await self.writeMessage(client_id, message) 656 | 657 | async def response_createStream(self, client_id, invoke): 658 | client_state = self.client_states[client_id] 659 | client_state.streams = client_state.streams + 1; 660 | response = common.Command( 661 | name='_result', 662 | id=invoke['id'], 663 | tm=self.relativeTime(client_id), 664 | type=common.Message.RPC, 665 | args=[client_state.streams]) 666 | 667 | message = response.toMessage() 668 | self.logger.debug("Sending createStream response!") 669 | await self.writeMessage(client_id, message) 670 | 671 | async def handle_connect_command(self, client_id, invoke): 672 | client_state = self.client_states[client_id] 673 | if hasattr(invoke['cmdData'], 'app'): 674 | client_state.app = invoke['cmdData'].app 675 | 676 | if client_state.app == '': 677 | self.logger.warning("Empty 'app' attribute. Disconnecting client: %s", client_state.client_ip) 678 | raise DisconnectClientException() 679 | 680 | if hasattr(invoke['cmdData'], 'tcUrl'): 681 | client_state.tcUrl = invoke['cmdData'].tcUrl 682 | 683 | if hasattr(invoke['cmdData'], 'swfUrl'): 684 | client_state.swfUrl = invoke['cmdData'].swfUrl 685 | 686 | if hasattr(invoke['cmdData'], 'flashVer'): 687 | client_state.flashVer = invoke['cmdData'].flashVer 688 | 689 | if hasattr(invoke['cmdData'], 'objectEncoding'): 690 | client_state.objectEncoding = invoke['cmdData'].objectEncoding 691 | 692 | self.logger.info("App: %s, tcUrl: %s, swfUrl: %s, flashVer: %s", client_state.app, client_state.tcUrl, client_state.swfUrl, client_state.flashVer) 693 | 694 | await self.send_window_ack(client_id, 5000000) 695 | await self.set_chunk_size(client_id, client_state.out_chunk_size) 696 | await self.set_peer_bandwidth(client_id, 5000000, 2) 697 | await self.respond_connect(client_id, invoke['id']) 698 | 699 | async def send(self, client_id, data): 700 | client_state = self.client_states[client_id] 701 | # Perform asynchronous sending operation 702 | # self.logger.info("Sending data: %s", data) 703 | client_state.writer.write(data) 704 | await client_state.writer.drain() 705 | 706 | 707 | async def send_window_ack(self, client_id, size): 708 | rtmp_buffer = bytes.fromhex("02000000000004050000000000000000") 709 | rtmp_buffer = bytearray(rtmp_buffer) 710 | rtmp_buffer[12:16] = size.to_bytes(4, byteorder='big') 711 | await self.send(client_id, rtmp_buffer) 712 | self.logger.debug("Set ack to %s", size) 713 | 714 | async def send_ack(self, client_id, size): 715 | rtmp_buffer = bytes.fromhex("02000000000004030000000000000000") 716 | rtmp_buffer = bytearray(rtmp_buffer) 717 | rtmp_buffer[12:16] = size.to_bytes(4, byteorder='big') 718 | await self.send(client_id, rtmp_buffer) 719 | self.logger.debug("Send ACK: %s", size) 720 | 721 | async def set_peer_bandwidth(self, client_id, size, bandwidth_type): 722 | rtmp_buffer = bytes.fromhex("0200000000000506000000000000000000") 723 | rtmp_buffer = bytearray(rtmp_buffer) 724 | rtmp_buffer[12:16] = size.to_bytes(4, byteorder='big') 725 | rtmp_buffer[16] = bandwidth_type 726 | await self.send(client_id, rtmp_buffer) 727 | self.logger.debug("Set bandwidth to %s", size) 728 | 729 | async def set_chunk_size(self, client_id, out_chunk_size): 730 | rtmp_buffer = bytearray.fromhex("02000000000004010000000000000000") 731 | struct.pack_into('>I', rtmp_buffer, 12, out_chunk_size) 732 | await self.send(client_id, bytes(rtmp_buffer)) 733 | self.logger.debug("Set out chunk to %s", out_chunk_size) 734 | 735 | async def handle_bytes_read_report(self, client_id, payload): 736 | # bytes_read = int.from_bytes(payload, byteorder='big') 737 | # self.logger.debug("Bytes read: %d", bytes_read) 738 | # # send ACK 739 | # rtmpBuffer = bytearray.fromhex('02000000000004030000000000000000') 740 | # rtmpBuffer[12:16] = bytes_read.to_bytes(4, 'big') 741 | # await self.send(client_id, rtmpBuffer) 742 | # Just Ignore! 743 | return False 744 | 745 | async def respond_connect(self, client_id, tid): 746 | client_state = self.client_states[client_id] 747 | response = common.Command() 748 | response.id, response.name, response.type = tid, '_result', common.Message.RPC 749 | 750 | arg = amf.Object( 751 | level='status', 752 | code='NetConnection.Connect.Success', 753 | description='Connection succeeded.', 754 | fmsVer='MasterStream/8,2', 755 | capabilities = 31, 756 | objectEncoding = client_state.objectEncoding) 757 | 758 | response.setArg(arg) 759 | message = response.toMessage() 760 | self.logger.debug("Sending connect response!") 761 | await self.writeMessage(client_id, message) 762 | 763 | async def writeMessage(self, client_id, message): 764 | client_state = self.client_states[client_id] 765 | if message.streamId in client_state.lastWriteHeaders: 766 | header = client_state.lastWriteHeaders[message.streamId] 767 | else: 768 | if client_state.nextChannelId <= PROTOCOL_CHANNEL_ID: 769 | client_state.nextChannelId = PROTOCOL_CHANNEL_ID + 1 770 | header, client_state.nextChannelId = common.Header( 771 | client_state.nextChannelId), client_state.nextChannelId + 1 772 | client_state.lastWriteHeaders[message.streamId] = header 773 | if message.type < message.AUDIO: 774 | header = common.Header(PROTOCOL_CHANNEL_ID) 775 | 776 | # now figure out the header data bytes 777 | if header.streamId != message.streamId or header.time == 0 or message.time <= header.time: 778 | header.streamId, header.type, header.size, header.time, header.delta = message.streamId, message.type, message.size, message.time, message.time 779 | control = common.Header.FULL 780 | elif header.size != message.size or header.type != message.type: 781 | header.type, header.size, header.time, header.delta = message.type, message.size, message.time, message.time - header.time 782 | control = common.Header.MESSAGE 783 | else: 784 | header.time, header.delta = message.time, message.time - header.time 785 | control = common.Header.TIME 786 | 787 | hdr = common.Header( 788 | channel=header.channel, 789 | time=header.delta if control in ( 790 | common.Header.MESSAGE, 791 | common.Header.TIME) else header.time, 792 | size=header.size, 793 | type=header.type, 794 | streamId=header.streamId) 795 | assert message.size == len(message.data) 796 | data = b'' 797 | while len(message.data) > 0: 798 | data = data + hdr.toBytes(control) # gather header bytes 799 | count = min(client_state.out_chunk_size, len(message.data)) 800 | data = data + message.data[:count] 801 | message.data = message.data[count:] 802 | control = common.Header.SEPARATOR # incomplete message continuation 803 | try: 804 | await self.send(client_id, data) 805 | self.logger.debug("Message sent!") 806 | except: 807 | self.logger.debug("Error on sending message!") 808 | 809 | async def handle_amf_data(self, client_id, rtmp_packet): 810 | client_state = self.client_states[client_id] 811 | offset = 1 if rtmp_packet['header']['type'] == RTMP_TYPE_FLEX_MESSAGE else 0 812 | payload = rtmp_packet['payload'][offset:rtmp_packet['header']['length']] 813 | amfReader = amf.AMF0(payload) 814 | inst = {} 815 | inst['type'] = rtmp_packet['header']['type'] 816 | inst['time'] = rtmp_packet['header']['timestamp'] 817 | inst['packet'] = rtmp_packet 818 | inst['cmd'] = amfReader.read() # first field is command name 819 | if inst['cmd'] == '@setDataFrame': 820 | inst['type'] = amfReader.read() # onMetaData 821 | self.logger.debug("AMF Data type: %s", inst['type']) 822 | if inst['type'] != 'onMetaData': 823 | return 824 | 825 | inst['dataObj'] = amfReader.read() # third is obj data 826 | if(inst['dataObj'] != None): 827 | self.logger.debug("Command Data %s", inst['dataObj']) 828 | else: 829 | self.logger.warning("Unsupported RTMP_TYPE_DATA cmd, CMD: %s", inst['cmd']) 830 | 831 | client_state.metaDataPayload = payload 832 | client_state.metaData = inst['dataObj'] 833 | client_state.audioSampleRate = int(inst['dataObj']['audiosamplerate']); 834 | client_state.audioChannels = 2 if inst['dataObj']['stereo'] else 1 835 | client_state.videoWidth = int(inst['dataObj']['width']); 836 | client_state.videoHeight = int(inst['dataObj']['height']); 837 | client_state.videoFps = int(inst['dataObj']['framerate']); 838 | client_state.Bitrate = int(inst['dataObj']['videodatarate']); 839 | #TODO: handle Meta Data! 840 | 841 | def parse_amf0_invoke_message(self, rtmp_packet): 842 | offset = 1 if rtmp_packet['header']['type'] == RTMP_TYPE_FLEX_MESSAGE else 0 843 | payload = rtmp_packet['payload'][offset:rtmp_packet['header']['length']] 844 | amfReader = amf.AMF0(payload) 845 | inst = {} 846 | inst['type'] = rtmp_packet['header']['type'] 847 | inst['time'] = rtmp_packet['header']['timestamp'] 848 | inst['packet'] = rtmp_packet 849 | 850 | try: 851 | inst['cmd'] = amfReader.read() # first field is command name 852 | if rtmp_packet['header']['type'] == RTMP_TYPE_FLEX_MESSAGE or rtmp_packet['header']['type'] == RTMP_TYPE_INVOKE: 853 | inst['id'] = amfReader.read() # second field *may* be message id 854 | inst['cmdData'] = amfReader.read() # third is command data 855 | if(inst['cmdData'] != None): 856 | self.logger.debug("Command Data %s", vars(inst['cmdData'])) 857 | else: 858 | inst['id'] = 0 859 | inst['args'] = [] # others are optional 860 | while True: 861 | inst['args'].append(amfReader.read()) # amfReader.read() 862 | except EOFError: 863 | pass 864 | 865 | self.logger.debug("Command %s", inst) 866 | return inst 867 | 868 | def relativeTime(self, client_id): 869 | return int(1000 * (time.time() - self.client_states[client_id]._time0)) 870 | 871 | async def start_server(self): 872 | server = await asyncio.start_server( 873 | self.handle_client, self.host, self.port) 874 | 875 | addr = server.sockets[0].getsockname() 876 | self.logger.info("RTMP server started on %s", addr) 877 | 878 | async with server: 879 | await server.serve_forever() 880 | 881 | # Configure logging level and format 882 | logging.basicConfig(level=LogLevel, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 883 | rtmp_server = RTMPServer() 884 | asyncio.run(rtmp_server.start_server()) 885 | --------------------------------------------------------------------------------