فهرست مقاله
پیش از محاسبهٔ جیتر یا تحویل داده به رمزگشا، باید بدانیم کدام بایتها سرآیند و کدام بخش payload هستند. در این درس یک بستهٔ RTP کاملاً ساختگی را آفلاین میخوانیم. تمرکز بر طولها و مرزهاست؛ هیچ بستهای دریافت یا ارسال، هیچ صوتی رمزگشایی و هیچ کیفیت تماس واقعی اندازهگیری نمیشود. نتیجهٔ تابع فقط پذیرش قرارداد ساختاریِ اعلامشده است، نه تأیید همهٔ قواعد RTP.
۱. فیلدهای ثابت را با ترتیب بایت شبکه بخوانیم

ورودی تابع دقیقاً bytes است و باید خودِ بخش RTP باشد؛ سرآیند IP یا UDP را از آن جدا نمیکنیم. بخش سرآیند در RFC 3550 پایهٔ دوازدهبایتی را مشخص میکند. نسخه، پرچمها و تعداد CSRC از بایت اول و marker و عدد payload type از بایت دوم استخراج میشوند. سپس شمارهٔ توالی، timestamp و SSRC خوانده میشوند.
برای خواندن عددهای چندبایتی، از قالب !BBHII در struct استفاده میکنیم. مستندات پایتون علامت تعجب را ترتیب شبکه با اندازههای استاندارد معرفی میکند؛ تکیه بر ترتیب بایت و همترازی بومیِ رایانه مناسب این ورودی نیست. پاسخ باید روی دستگاهی با ترتیب بایت متفاوت نیز معنای همان عددهای شبکه را حفظ کند.
در بستهٔ دستی، شمارهٔ توالی از 0x1234 برابر ۴۶۶۰ و timestamp از 0x01020304 برابر ۱۶۹۰۹۰۶۰ است. این timestamp را میلیثانیه یا ساعت دیواری نمینامیم؛ نرخ ساعت از عدد خام بهتنهایی معلوم نیست. payload type برابر ۹۶ و marker روشن است، ولی بدون قرارداد پروفایل و قالب، از این دو مقدار نوع رمزگذار یا رویداد صوتی مشخصی نتیجه نمیگیریم.
۲. CSRC و افزونه آغاز payload را جابهجا میکنند

چهار بیتِ تعداد CSRC میتواند صفر تا پانزده شناسه را اعلام کند. هر شناسه چهار بایت میگیرد؛ بنابراین پس از پایه، به اندازهٔ چهار ضربدر تعداد جلو میرویم. در نمونه دو شناسه داریم: 0xAABBCCDD و پنج. این عددها فقط دادهٔ ساختگیاند؛ هویت واقعی گوینده یا احراز اصالت منبع را ثابت نمیکنند.
وقتی پرچم X روشن است، پس از فهرست CSRC سرآیند افزونه میآید. طبق بخش افزونهٔ RFC، طول اعلامی تعداد کلمههای چهاربایتیِ بدنه است؛ چهار بایتِ سرآیند افزونه در آن شمرده نمیشود. در نمونه طول یک است، پس بدنه چهار بایت و کل افزونه هشت بایت است. طول صفر نیز همچنان سرآیند چهاربایتی دارد.
محاسبهٔ دستیِ آغاز payload چنین است: دوازده بایت پایه، هشت بایت CSRC، چهار بایت سرآیند افزونه و چهار بایت بدنه؛ جمع بیستوهشت. شناسهٔ پروفایل 0xABCD و بدنهٔ deadbeef را فقط ثبت میکنیم. معنی داخلی افزونه، ترتیب زیرعنصرهای پروفایل یا تفسیر یک افزونهٔ استاندارد دیگر در این کد پیاده نشده است.
۳. padding انتهایی را از دادهٔ مفید جدا کنیم

اگر P روشن باشد، آخرین بایت تعداد بایتهای padding را، همراه خودش، اعلام میکند. این تعداد باید دستکم یک و حداکثر برابر بایتهای باقیمانده پس از سرآیند باشد. اگر فقط سرآیند داشته باشیم یا مقدار صفر بیاید، قرارداد خطا میدهد. شمارش padding نباید وارد ناحیهٔ CSRC یا افزونه شود.
بستهٔ نمونه سیوچهار بایت است. سه بایت انتهایی 00، 00 و 03 هستند؛ پس سه بایت padding کنار میروند. از شش بایتِ پس از سرآیند، سه بایت 10، 20 و 30 برای payload میماند. این مقدار هگزادسیمال، نمونهٔ صوت PCM یا خروجی آزمایش یک رمزگذار نیست؛ فقط محتوای دلخواهِ fixture است.
وقتی P خاموش باشد، برنامه از مقدار آخرین بایت حدس نمیزند که padding داریم. مثلاً پایان با 03 همچنان میتواند بخشی از payload باشد. بررسی ما صرفاً تعداد و مرز padding است؛ محتوای پرکنندهٔ آن را اعتبارسنجی نمیکنیم. همین محدودیت یکی از دلایلی است که خروجی را گواه انطباق کامل بسته با RFC معرفی نمیکنیم.
۴. کد کامل و مرجع مستقل هندسهٔ بسته را اجرا کنیم

فایل rtp_header_parser.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. پیش از هر خواندن متغیر، مرز لازم بررسی میشود: کمبود CSRC، چهار بایت ناقصِ سرآیند افزونه یا بدنهای کوتاهتر از طول اعلامی خطا میدهد. نسخههای غیر از دو نیز پذیرفته نیستند.
"""Bounded offline RTP V2 byte-layout parser; not RTCP, audio or a receiver."""
import json
import platform
import struct
MAX_PACKET_BYTES = 1500
def parse_rtp(packet):
if type(packet) is not bytes or not 12 <= len(packet) <= MAX_PACKET_BYTES:
raise ValueError('packet_bytes')
first, second, sequence, timestamp, ssrc = struct.unpack_from('!BBHII', packet)
if first >> 6 != 2:
raise ValueError('version')
count = first & 15
padding_flag = bool(first & 32)
extension_flag = bool(first & 16)
offset = 12 + 4 * count
if offset > len(packet):
raise ValueError('csrc_truncated')
csrc = [struct.unpack_from('!I', packet, 12 + 4 * index)[0] for index in range(count)]
extension = None
if extension_flag:
if offset + 4 > len(packet):
raise ValueError('extension_header_truncated')
profile, words = struct.unpack_from('!HH', packet, offset)
end = offset + 4 + 4 * words
if end > len(packet):
raise ValueError('extension_body_truncated')
extension = {'profile_identifier': profile, 'length_words': words,
'opaque_body_hex': packet[offset + 4:end].hex()}
offset = end
payload_end, padding_bytes = len(packet), 0
if padding_flag:
remaining = len(packet) - offset
if remaining == 0:
raise ValueError('padding_missing')
padding_bytes = packet[-1]
if not 1 <= padding_bytes <= remaining:
raise ValueError('padding_count')
payload_end -= padding_bytes
payload = packet[offset:payload_end]
return {'version': 2, 'marker': bool(second & 128), 'payload_type_number': second & 127,
'sequence_number': sequence, 'timestamp_number': timestamp, 'ssrc': ssrc,
'csrc': csrc, 'extension': extension, 'header_bytes': offset,
'padding_bytes': padding_bytes, 'payload_bytes': len(payload),
'payload_hex': payload.hex(), 'packet_bytes': len(packet)}
def authored_packet(count, words, padding):
"""Independent fixture assembly with int.to_bytes; no struct/parser helpers."""
first = 128 + count + (16 if words is not None else 0) + (32 if padding else 0)
base = bytes([first, 97]) + (65530).to_bytes(2, 'big') + (42).to_bytes(4, 'big') + (7).to_bytes(4, 'big')
sources = b''.join(value.to_bytes(4, 'big') for value in range(1, count + 1))
ext = b'' if words is None else (0x1010).to_bytes(2, 'big') + words.to_bytes(2, 'big') + b'\x55' * (4 * words)
tail = b'' if not padding else b'\x00' * (padding - 1) + bytes([padding])
return base + sources + ext + b'\x10\x20\x30' + tail
def rejects(call, message):
try:
call()
except ValueError as error:
return str(error) == message
return False
def run_example():
# Entire hand fixture is literal, not generated by authored_packet.
packet = bytes.fromhex('b2 e0 1234 01020304 11223344 aabbccdd 00000005 abcd 0001 deadbeef 102030 000003')
original = bytes(packet)
parsed = parse_rtp(packet)
fixed = bytes.fromhex('80 00 0001 00000002 00000003')
zero_extension = bytes.fromhex('90 60 0001 00000002 00000003 abcd 0000 aa')
maximum_csrc = bytes([143]) + fixed[1:] + b''.join(i.to_bytes(4, 'big') for i in range(1, 16)) + b'\x99'
padding_header_only = bytes([160]) + fixed[1:]
cases, oracle_ok = 0, True
for count in (0, 1, 3):
for words in (None, 0, 2):
for padding in (0, 1, 4):
result = parse_rtp(authored_packet(count, words, padding))
expected_header = 12 + 4 * count + (0 if words is None else 4 + 4 * words)
oracle_ok &= result['header_bytes'] == expected_header
oracle_ok &= result['csrc'] == list(range(1, count + 1))
oracle_ok &= result['padding_bytes'] == padding and result['payload_hex'] == '102030'
oracle_ok &= result['packet_bytes'] == expected_header + 3 + padding
cases += 1
altered = parse_rtp(packet)
altered['csrc'].append(99)
altered['extension']['opaque_body_hex'] = 'changed'
checks = {
'hand_fixed_fields_marker_and_numeric_payload_type': parsed['sequence_number'] == 4660 and parsed['timestamp_number'] == 16909060 and parsed['ssrc'] == 287454020 and parsed['marker'] is True and parsed['payload_type_number'] == 96,
'hand_two_csrc_and_one_word_opaque_extension': parsed['csrc'] == [2864434397, 5] and parsed['extension'] == {'profile_identifier': 43981, 'length_words': 1, 'opaque_body_hex': 'deadbeef'},
'hand_partition_28_header_3_payload_3_padding': parsed['header_bytes'] == 28 and parsed['payload_hex'] == '102030' and parsed['padding_bytes'] == 3 and parsed['packet_bytes'] == 34,
'network_order_independent_integer_read': parsed['sequence_number'] == int.from_bytes(packet[2:4], 'big') and parsed['timestamp_number'] == int.from_bytes(packet[4:8], 'big') and parsed['ssrc'] == int.from_bytes(packet[8:12], 'big'),
'minimal_twelve_byte_header_and_empty_payload': parse_rtp(fixed)['header_bytes'] == 12 and parse_rtp(fixed)['payload_bytes'] == 0,
'plain_payload_without_extension_or_padding': parse_rtp(fixed + b'\x00\x01')['payload_hex'] == '0001' and parse_rtp(fixed)['extension'] is None,
'zero_length_extension_still_has_four_byte_header': parse_rtp(zero_extension)['header_bytes'] == 16 and parse_rtp(zero_extension)['extension']['opaque_body_hex'] == '' and parse_rtp(zero_extension)['payload_hex'] == 'aa',
'maximum_four_bit_csrc_count_fifteen': parse_rtp(maximum_csrc)['csrc'] == list(range(1, 16)) and parse_rtp(maximum_csrc)['header_bytes'] == 72 and parse_rtp(maximum_csrc)['payload_hex'] == '99',
'all_truncated_fixed_headers_reject': all(rejects(lambda length=length: parse_rtp(fixed[:length]), 'packet_bytes') for length in range(12)),
'unsupported_versions_reject': all(rejects(lambda version=version: parse_rtp(bytes([version << 6]) + fixed[1:]), 'version') for version in (0, 1, 3)),
'csrc_count_cannot_read_beyond_packet': rejects(lambda: parse_rtp(bytes([130]) + fixed[1:] + b'\x00' * 4), 'csrc_truncated'),
'extension_flag_requires_complete_extension_header': rejects(lambda: parse_rtp(bytes([144]) + fixed[1:] + b'\x00\x00'), 'extension_header_truncated'),
'extension_word_length_cannot_read_beyond_packet': rejects(lambda: parse_rtp(bytes([144]) + fixed[1:] + bytes.fromhex('abcd0002 deadbeef')), 'extension_body_truncated') and rejects(lambda: parse_rtp(bytes([144]) + fixed[1:] + bytes.fromhex('abcdffff')), 'extension_body_truncated'),
'padding_requires_nonzero_count_within_post_header_bytes': rejects(lambda: parse_rtp(padding_header_only), 'padding_missing') and rejects(lambda: parse_rtp(padding_header_only + b'\x00'), 'padding_count') and rejects(lambda: parse_rtp(padding_header_only + b'\x02'), 'padding_count') and parse_rtp(padding_header_only + b'\x01')['payload_bytes'] == 0,
'last_octet_is_not_padding_count_when_flag_is_clear': parse_rtp(fixed + b'\x00\x03')['payload_hex'] == '0003' and parse_rtp(fixed + b'\x00\x03')['padding_bytes'] == 0,
'exact_bytes_type_and_1500_byte_limit': parse_rtp(fixed + b'\x00' * 1488)['packet_bytes'] == 1500 and rejects(lambda: parse_rtp(fixed + b'\x00' * 1489), 'packet_bytes') and rejects(lambda: parse_rtp(bytearray(fixed)), 'packet_bytes') and rejects(lambda: parse_rtp(memoryview(fixed)), 'packet_bytes'),
'independent_authored_geometry_reference': cases == 27 and bool(oracle_ok),
'input_preserved_and_nested_result_is_fresh': packet == original and parse_rtp(packet) == parsed and altered['csrc'] != parsed['csrc'] and altered['extension'] != parsed['extension'],
}
assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values()), checks
return {'python': platform.python_version(), 'dependencies': 'Python standard library only',
'scope': 'Bounded offline generic RTP V2 geometry only; input is assumed RTP, not RTP/RTCP demultiplexing, codec, SRTP or full RFC conformance',
'results': {'hand_packet_hex': packet.hex(), 'parsed_hand_packet': parsed,
'independent_reference_cases_inside_one_group': cases,
'limits': {'max_packet_bytes': MAX_PACKET_BYTES, 'extension_body': 'opaque bytes, no profile decoding',
'padding': 'count and region bounds only; filler content not validated',
'network_packets_received_or_sent': 0, 'audio_decoded': False}}, 'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
هجده گروه بررسی ثبت شده است. بستهٔ اصلی از رشتهٔ هگز دستی ساخته شده و پاسخ عددی آن از قبل معلوم است. یک مرجع دیگر با int.to_bytes و بدون struct، بستههایی با تعداد CSRC و طول افزونه و padding معلوم میسازد. طول سرآیند و payloadِ مورد انتظار از همان پارامترهای مستقلِ ساخت محاسبه و با خروجی خواننده مقایسه میشود.
سه انتخاب تعداد، سه انتخاب افزونه و سه مقدار padding، بیستوهفت حالت داخل یک گروه مرجع میسازند؛ آنها را بیستوهفت گروه تازه نمیشماریم. دوازده طولِ کوتاهتر از سرآیند نیز یک گروهاند. افزونهٔ دارای طول ۶۵۵۳۵ با ورودی کوچک رد میشود؛ برنامه برای جبران ادعای طول، حافظهای به آن اندازه رزرو نمیکند.
۵. مرز خوانندهٔ ساختاری و چهار پاسخ را حفظ کنیم

سقف ۱۵۰۰ بایت، کران انتخابیِ این تمرین است؛ الزام عمومی RTP یا تنظیم پیشنهادی MTU نیست. بستهٔ با payload تهی در قرارداد هندسی قابل خواندن است، اما دربارهٔ مفیدبودن آن برای یک قالب صوتی تصمیم نمیگیریم. ورودی را RTP فرض کردهایم؛ تشخیص خودکار RTP از RTCP، مدیریت نشست، امنیت SRTP و رمزگشایی خارج از دامنهاند.
{
"python": "3.14.5",
"dependencies": "Python standard library only",
"scope": "Bounded offline generic RTP V2 geometry only; input is assumed RTP, not RTP/RTCP demultiplexing, codec, SRTP or full RFC conformance",
"results": {
"hand_packet_hex": "b2e012340102030411223344aabbccdd00000005abcd0001deadbeef102030000003",
"parsed_hand_packet": {
"version": 2,
"marker": true,
"payload_type_number": 96,
"sequence_number": 4660,
"timestamp_number": 16909060,
"ssrc": 287454020,
"csrc": [
2864434397,
5
],
"extension": {
"profile_identifier": 43981,
"length_words": 1,
"opaque_body_hex": "deadbeef"
},
"header_bytes": 28,
"padding_bytes": 3,
"payload_bytes": 3,
"payload_hex": "102030",
"packet_bytes": 34
},
"independent_reference_cases_inside_one_group": 27,
"limits": {
"max_packet_bytes": 1500,
"extension_body": "opaque bytes, no profile decoding",
"padding": "count and region bounds only; filler content not validated",
"network_packets_received_or_sent": 0,
"audio_decoded": false
}
},
"checks": {
"hand_fixed_fields_marker_and_numeric_payload_type": true,
"hand_two_csrc_and_one_word_opaque_extension": true,
"hand_partition_28_header_3_payload_3_padding": true,
"network_order_independent_integer_read": true,
"minimal_twelve_byte_header_and_empty_payload": true,
"plain_payload_without_extension_or_padding": true,
"zero_length_extension_still_has_four_byte_header": true,
"maximum_four_bit_csrc_count_fifteen": true,
"all_truncated_fixed_headers_reject": true,
"unsupported_versions_reject": true,
"csrc_count_cannot_read_beyond_packet": true,
"extension_flag_requires_complete_extension_header": true,
"extension_word_length_cannot_read_beyond_packet": true,
"padding_requires_nonzero_count_within_post_header_bytes": true,
"last_octet_is_not_padding_count_when_flag_is_clear": true,
"exact_bytes_type_and_1500_byte_limit": true,
"independent_authored_geometry_reference": true,
"input_preserved_and_nested_result_is_fresh": true
}
}
پس از استخراج عدد توالی، میتوان قرارداد جداگانهٔ توالی، تکرار و شکاف مشاهدهشده را آزمود. برای برآورد جیتر RTP، زمان ورود و نرخ ساعت نیز لازم است؛ خواندن timestamp بهتنهایی آن دادهها را تولید نمیکند. این مقاله آن دو محاسبه را دوباره انجام نمیدهد.
پرسش اول: چرا طول یکِ افزونه فقط یک بایت نیست؟ پاسخ: واحد طول، کلمهٔ چهاربایتی است؛ با چهار بایت سرآیند خود افزونه، در نمونه هشت بایت مصرف میشود.
پرسش دوم: اگر طول افزونه صفر باشد، payload از همان محل شروع میشود؟ پاسخ: خیر؛ سرآیند چهاربایتی افزونه همچنان باید خوانده و از آغاز payload عبور داده شود.
پرسش سوم: آیا آخرین بایت 03 همیشه padding است؟ پاسخ: خیر؛ فقط وقتی P روشن است، با بررسی کرانها نقش شمارنده میگیرد.
پرسش چهارم: آیا خروجی موفق یعنی تماس سالم بوده است؟ پاسخ: خیر؛ هیچ تماس یا صوتی نداریم. منابع در ۲۵ مهر ۱۴۰۵ خوانده شدند؛ RFC مربوط به ژوئیهٔ ۲۰۰۳ است و مستندات 3.14.8 از اجرای 3.14.5 جدا ثبت شدهاند. این درس، آزمون ساختگیِ پایه است و گزارش شبکه یا خبر پژوهشی تازه نیست.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.


