文章目录
- 前言(代码直接参考第二部分第三小节)
- 一、抓包工具wireshaker的安装与使用
- 二、第二章:拉流(Streaming)技术深度解析
-
- [1. RTSP 协议交互深度剖析](#1. RTSP 协议交互深度剖析)
- [2. RTP 数据包结构与解析](#2. RTP 数据包结构与解析)
-
- [2.1 H.264 在 RTP 中的打包方式 (RFC 6184)](#2.1 H.264 在 RTP 中的打包方式 (RFC 6184))
- [2.2 H.264 与 H.265 (HEVC) 核心技术区别](#2.2 H.264 与 H.265 (HEVC) 核心技术区别)
- [2.3 RTCP 协议的作用](#2.3 RTCP 协议的作用)
- [3. 拉流拆包的完整示例代码展示(不喜欢基础知识直接看这里)](#3. 拉流拆包的完整示例代码展示(不喜欢基础知识直接看这里))
- 总结
- 下期预告
前言(代码直接参考第二部分第三小节)
在上一小节的内容中,我们简要介绍了拉流和解码两个基本过程,拉流即建立与网络摄像仪的通讯,获取实时传输的数据,解码则是将传递的数据恢复成人眼观察到的彩色图像,本小节将深入拉流模块,介绍完整的拉流过程。
一、抓包工具wireshaker的安装与使用
在开始之前,本节将分享常用的抓包工具wiresharker。
- 点击链接进入下载界面下载符合系统版本的工具
- 下载后直接按照要求安装即可
- 按照上一节使用liveNVR的方式,创建好测试的网络视频流
- 打开wiresharker,点击开始捕获分组。而后用vlc打开推送的rtsp流地址,本例地址为rtsp://localhost:5001/stream_1
- 运行一段时间之后(推荐3-5秒即可),我们终止wiresharker的记录,便可查询网络通讯的消息记录
二、第二章:拉流(Streaming)技术深度解析
在上一章中,我们概述了拉流是一次以RTSP协议为核心的网络会话。本章将深入这个会话的每一个细节,剖析数据包的流转,并探讨其中的关键技术与挑战。
1. RTSP 协议交互深度剖析
RTSP 协议通常运行在 TCP 上(默认端口554),确保了控制命令的可靠传输。让我们逐一分解每个步骤的请求与响应内容。参考的示意图如下所示:
RTSP Client RTSP Server RTP/RTCP Streams Phase 1: Connection Setup OPTIONS request CSeq: 1 200 OK Public: DESCRIBE, SETUP, PLAY, TEARDOWN Phase 2: Get Stream Description DESCRIBE request CSeq: 2 200 OK with SDP Contains media info, codecs, control URLs Phase 3: Setup Transport Channels SETUP for video CSeq: 3 Transport: client_port=8000-8001 200 OK Session: 123456 server_port=9000-9001 SETUP for audio CSeq: 4 Session: 123456 200 OK server_port=9002-9003 Phase 4: Start Streaming PLAY request CSeq: 5 Session: 123456 200 OK RTP-Info with seq & timestamp Phase 5: Media Data Transmission Video RTP packets Audio RTP packets RTCP reports loop [Continuous Streaming] Phase 6: Session Teardown TEARDOWN request CSeq: 6 Session: 123456 200 OK RTSP Client RTSP Server RTP/RTCP Streams
- OPTIONS
目的:握手的第一步,并非必须,但用于确认服务器是否存活以及支持哪些RTSP方法(如DESCRIBE, SETUP, PLAY, PAUSE, TEARDOWN)。
客户端请求示例:
OPTIONS rtsp://192.168.1.100:554/live/stream RTSP/1.0
CSeq: 1
User-Agent: MyStreamingClient
- CSeq:序列号,用于匹配请求和响应,每次请求必须递增。
服务器响应示例:
http
RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, PLAY, PAUSE, TEARDOWN
- Public:列出了服务器支持的所有方法。
- DESCRIBE
目的:获取媒体流的元数据描述,核心是SDP(Session Description Protocol)文件。
客户端请求示例:
http
DESCRIBE rtsp://192.168.1.100:554/live/stream RTSP/1.0
CSeq: 2
Accept: application/sdp
User-Agent: MyStreamingClient
- Accept: 指明客户端希望接收的描述格式,通常是SDP。
服务器响应示例:
http
RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: 500
v=0
o=- 123456789 1 IN IP4 192.168.1.100
s=Live Stream
c=IN IP4 0.0.0.0
t=0 0
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; profile-level-id=4D0029; sprop-parameter-sets=Z00AKZpkA8ARPy4C3AQEBQAAAwPAAAPBAPPiwIA,aL48gA==
a=control:track0
m=audio 0 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/44100/2
a=fmtp:97 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=121056E500
a=control:track1
-
Content-Type 和 Content-Length 描述了消息体的格式和大小。
-
SDP关键字段解析:
m=:媒体行。m=video 0 RTP/AVP 96 表示这是一个视频流,端口号暂时为0(将在SETUP中确定),使用RTP/AVP(UDP)负载格式,负载类型号为96。
a=rtpmap:将负载类型号映射到具体的编解码器及其参数。96 H264/90000 表示负载类型96是H.264编码,时钟频率为90000Hz(用于视频时间戳)。
a=fmtp:提供编解码器的具体格式参数。对于H.264,它可能包含profile-level-id和至关重要的 sprop-parameter-sets,这通常包含了SPS和PPS,是解码H.264流的基石。对于AAC音频,config字段同样包含了解码所需的音频特定配置信息。
a=control:指定该媒体流的控制URL。track0和track1是相对路径,后续的SETUP请求需要拼接到RTSP URL后面(如 rtsp://.../live/stream/track0)。
- SETUP
目的:为每一路媒体流建立传输通道,协商RTP/RTCP的传输方式和端口。
客户端请求示例(视频流):
http
SETUP rtsp://192.168.1.100:554/live/stream/track0 RTSP/1.0
CSeq: 3
Transport: RTP/AVP;unicast;client_port=8000-8001
User-Agent: MyStreamingClient
-
Transport:这是SETUP请求的核心。
-
RTP/AVP:表示使用RTP over UDP。
-
unicast:单播。
-
client_port=8000-8001:客户端宣布它将在端口8000上接收RTP数据包,在8001上接收RTCP控制包。
-
服务器响应示例:
http
RTSP/1.0 200 OK
CSeq: 3
Transport: RTP/AVP;unicast;client_port=8000-8001;server_port=6000-6001
Session: 12345678
-
Transport:服务器确认传输参数,并告知客户端服务器端的RTP和RTCP端口(server_port=6000-6001)。
-
Session:服务器为此会话创建的唯一ID。后续所有请求(PLAY, PAUSE, TEARDOWN)都必须携带此Session ID,以便服务器识别会话。
传输方式的选择:
-
UDP(RTP/AVP):高效,延迟低,但可能丢包。client_port和server_port由双方明确指定。
-
TCP(RTP/AVP/TCP):可靠传输,能穿透更多防火墙。在TCP模式下,RTP/RTCP数据会通过现有的RTSP TCP连接进行交织传输,不再需要单独协商端口。此时Transport头可能为:Transport: RTP/AVP/TCP;interleaved=0-1,表示视频流的RTP数据在通道0,RTCP在通道1。
- PLAY
目的:启动数据传输。可以指定播放范围(如从第10秒开始)。
客户端请求示例:
http
PLAY rtsp://192.168.1.100:554/live/stream RTSP/1.0
CSeq: 4
Session: 12345678
Range: npt=0.000-
- Range:指定播放的时间范围。npt=0.000- 表示从0秒开始播放到结束。
服务器响应示例:
http
RTSP/1.0 200 OK
CSeq: 4
Session: 12345678
RTP-Info: url=rtsp://192.168.1.100:554/live/stream/track0;seq=12345;rtptime=789001
- RTP-Info:提供了关键的起始信息。seq是第一个RTP包的序列号,rtptime是第一个RTP包的时间戳。这两个值对于客户端正确初始化其播放缓冲区至关重要。
- TEARDOWN
目的:优雅地终止会话,释放服务器资源。
客户端请求示例:
http
TEARDOWN rtsp://192.168.1.100:554/live/stream RTSP/1.0
CSeq: 5
Session: 12345678
- 服务器响应:通常是一个简单的200 OK,确认会话已结束。
2. RTP 数据包结构与解析
当PLAY命令成功后,服务器会通过协商好的通道(UDP端口或TCP交织通道)持续发送RTP包。
一个RTP包的结构如下所示:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| payload |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-
V (Version): 版本,总是2。
-
P (Padding) / X (Extension): 填充和扩展位,通常为0。
-
M (Marker): 标记位。对于视频,通常标志一帧的结束。这对于判断帧边界非常重要。
-
PT (Payload Type): 负载类型,对应SDP中定义的rtpmap值(如96)。
-
Sequence Number: 序列号,每个包递增1,用于检测丢包和乱序。
-
Timestamp: 时间戳,基于时钟频率(如视频90000Hz)的采样值,用于音视频同步。
-
SSRC: 同步源标识符,唯一标识一个流。
-
Payload: 实际的负载数据,对于H.264,这里就是NAL Unit(或其片段)。
2.1 H.264 在 RTP 中的打包方式 (RFC 6184)
一个H.264的NAL Unit(网络抽象层单元)可能大于网络的MTU(最大传输单元)。RTP协议定义了三种模式来拆分或组合NAL Unit:
-
Single NAL Unit Mode: 一个RTP包包含一个完整的NAL Unit。适用于较小的NAL单元(如SPS, PPS, SEI等)。
-
Fragmentation Unit (FU-A): 用于将一个大的NAL Unit分割成多个RTP包。
-
第一个包有FU indicator和FU header,其中S(Start)位为1。
-
中间包只有S和E位都为0。
-
最后一个包E(End)位为1。
-
所有分片包的FU header中的NAL Unit Type字段都是相同的,来自原始的NAL Unit。
-
-
Aggregation Packet (STAP-A): 用于将多个小的NAL Unit(如SPS+PPS+IDR帧的Slice)组合在一个RTP包中发送,减少包头开销,提高效率。
客户端处理流程:接收RTP包 -> 根据序列号排序 -> 根据Marker位和FU-A的S/E位判断帧边界 -> 重组完整的NAL Units -> 将NAL Units送入解码器。
2.2 H.264 与 H.265 (HEVC) 核心技术区别
在拉流和解码过程中,客户端从 SDP 中获取的 fmtp 参数和 RTP 负载中的编码数据,其结构完全取决于所使用的编解码器。H.265 (HEVC) 作为 H.264 (AVC) 的继任者,旨在在同等主观画质下,将压缩效率提高一倍。
下表清晰地概括了两代标准的核心差异:
特性维度 | H.264 / AVC (Advanced Video Coding) | H.265 / HEVC (High Efficiency Video Coding) | 区别带来的影响 |
---|---|---|---|
核心目标 | 高清视频(1080p)的高效编码 | 超高清视频(4K/8K)、HDR 的高效编码 | HEVC 为更高分辨率和动态范围内容而生 |
压缩效率 | 基准。例如,一段 4Mbps 的 1080p 视频 | 提升约 50%。同等画质下,码率仅需 ~2Mbps | 大幅节省带宽和存储成本,使 4K 流媒体成为可能 |
宏块 vs. 编码树单元(CTU) | 使用 宏块 (Macroblock),最大 16x16 像素 | 使用 编码树单元 (Coding Tree Unit - CTU),最大支持 64x64 像素 | CTU 可以更好地处理大块平坦区域,效率更高 |
帧内预测模式 | 提供 9 种 方向性预测模式 | 提供 多达 35 种 方向性预测模式 | 更精确的预测,减少冗余信息,提升压缩率 |
并行处理工具 | 主要依赖 片 (Slices) | 引入了 Tile(波瓦) 和 WPP(波前并行处理) | 更好地利用多核CPU进行并行编解码,提升速度 |
关键参数集 | SPS (序列参数集) PPS (图像参数集) | 额外引入VPS (视频参数集) | HEVC 的码流结构更复杂,VPS 包含了整体视频层的配置信息 |
NAL Unit 类型 | 常用的如:7 (SPS), 8 (PPS), 5 (IDR帧), 1 (非IDR帧) | 常用的如:32 (VPS), 33 (SPS), 34 (PPS), 19 (IDR帧), 1 (非IDR帧) | 解码器需要识别新的 NAL 单元类型才能正确解析 |
专利与费用 | 专利授权模式相对清晰、成熟 | 专利池复杂,授权费用曾一度高昂且不清晰 | 曾是阻碍 H.265 普及的重要因素,如今已大幅改善 |
应用场景 | 当前最主流、兼容性最好的格式,广泛应用于视频会议、监控、在线视频(720p/1080p) | 4K 超高清电视频道、高端安防监控、主流视频平台的 4K/HDR 内容 | H.264 是安全牌,H.265 是面向未来的高效选择 |
2.3 RTCP 协议的作用
RTCP是RTP的伴生协议,通常使用RTP端口号+1的端口。它主要有两个作用:
-
QoS反馈:客户端定期向服务器发送Receiver Report (RR),汇报接收质量,包括丢包率、抖动等信息。服务器可以根据这些信息进行码率调整等优化。
-
流间同步:RTCP Sender Report (SR) 包含了RTP时间戳和对应的"墙上时钟"(NTP时间戳),客户端利用这个映射关系来实现音频和视频的同步播放。
3. 拉流拆包的完整示例代码展示(不喜欢基础知识直接看这里)
这里不假思索地给出完整的拉流和拆包的示例代码,rtsp视频流以liveNVR和海康IPC的H264编码视频为基准,已经通过验证和测试:
python
import socket
import re
import struct
import threading
import time
import hashlib
import base64
from collections import deque
from urllib.parse import urlparse, urljoin
class RTSPClient:
def __init__(self, rtsp_url):
self.rtsp_url = rtsp_url
self.parse_url()
# RTSP状态变量
self.cseq = 1
self.session_id = None
self.auth_header = None
self.control_url = None
self.base_url = None
self.video_control_url = None # 新增:视频轨道的控制URL
# 网络连接
self.rtsp_socket = None
self.transport_mode = None # 'tcp' 或 'udp'
# 数据存储
self.frame_buffer = deque()
self.current_frame = b''
self.last_rtcp_time = time.time()
self.running = False
def parse_url(self):
"""解析RTSP URL"""
parsed = urlparse(self.rtsp_url)
self.server_ip = parsed.hostname
self.server_port = parsed.port if parsed.port else 554
self.stream_path = parsed.path.lstrip('/')
self.username = parsed.username if parsed.username else "admin"
self.password = parsed.password if parsed.password else "admin"
# 构建基础URL
self.base_url = f"rtsp://{self.server_ip}:{self.server_port}/"
print(f"设备地址: {self.server_ip}:{self.server_port}")
print(f"流路径: {self.stream_path}")
if self.username:
print(f"用户名: {self.username}")
if self.password:
print(f"密码: {self.password}")
def connect(self):
"""建立RTSP连接"""
self.rtsp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.rtsp_socket.connect((self.server_ip, self.server_port))
self.rtsp_socket.settimeout(5.0)
def calculate_digest_auth(self, method, uri, realm, nonce):
"""计算Digest认证的响应值"""
if not self.username or not self.password:
return None
# 计算HA1 = MD5(username:realm:password)
ha1_str = f"{self.username}:{realm}:{self.password}"
ha1 = hashlib.md5(ha1_str.encode()).hexdigest()
# 计算HA2 = MD5(method:uri)
ha2_str = f"{method}:{uri}"
ha2 = hashlib.md5(ha2_str.encode()).hexdigest()
# 计算response = MD5(HA1:nonce:HA2)
response_str = f"{ha1}:{nonce}:{ha2}"
response = hashlib.md5(response_str.encode()).hexdigest()
return response
def build_request_uri(self, method):
"""构建请求URI"""
# 对于SETUP请求,使用视频轨道控制URL
if method == "SETUP" and self.video_control_url:
# 如果控制URL是相对路径,则构建完整URL
if not self.video_control_url.startswith("rtsp://"):
return urljoin(self.base_url, self.video_control_url)
return self.video_control_url
# 对于PLAY请求,使用会话级控制URL
if method == "PLAY" and self.control_url:
if not self.control_url.startswith("rtsp://"):
return urljoin(self.base_url, self.control_url)
return self.control_url
# 其他请求使用基础URL
return urljoin(self.base_url, self.stream_path)
def send_rtsp_request(self, method, extra_headers=None):
"""发送RTSP请求"""
# 构建请求URI
uri = self.build_request_uri(method)
print("cur uri : ",uri)
if method == "DESCRIBE":
if not uri.startswith("rtsp"):
uri = "rtsp://" + str(self.server_ip) + ":" + str(self.server_port) + "/" + str(self.stream_path)
headers = {
"CSeq": self.cseq,
"User-Agent": "实现自己的AI视频监控系统"
}
# 添加认证头(如果可用)
if self.auth_header:
headers["Authorization"] = self.auth_header
# 添加额外头部
if extra_headers:
headers.update(extra_headers)
request = f"{method} {uri} RTSP/1.0\r\n"
# 添加头部
for key, value in headers.items():
request += f"{key}: {value}\r\n"
request += "\r\n"
print(f"发送请求:\n{request}")
self.rtsp_socket.send(request.encode())
self.cseq += 1
return self.receive_rtsp_response()
def receive_rtsp_response(self):
"""接收RTSP响应"""
response = b''
while True:
try:
chunk = self.rtsp_socket.recv(4096)
if not chunk:
break
response += chunk
print(response)
if b'\r\n\r\n' in response:
# 检查是否有Content-Length
headers_end = response.index(b'\r\n\r\n') + 4
header_text = response[:headers_end].decode()
# 查找Content-Length
content_length = 0
for line in header_text.split('\r\n'):
if line.lower().startswith('content-length:'):
content_length = int(line.split(':')[1].strip())
print("content_length : ", content_length)
break
# 如果存在消息体,读取完整消息体
if content_length > 0:
body = response[headers_end:]
while len(body) < content_length:
body += self.rtsp_socket.recv(content_length - len(body))
response = header_text.encode() + body
break
except socket.timeout:
break
response_str = response.decode(errors='ignore')
print(f"收到响应:\n{response_str}")
if not response_str:
return 0, {}, ""
status_line = response_str.split('\r\n')[0]
if ' ' in status_line:
status_code = int(status_line.split(' ')[1])
else:
status_code = 0
# 解析头部
headers = {}
for line in response_str.split('\r\n')[1:]:
if ': ' in line:
key, value = line.split(': ', 1)
headers[key.strip()] = value.strip()
# 提取消息体
body = ""
if '\r\n\r\n' in response_str:
body = response_str.split('\r\n\r\n', 1)[1]
return status_code, headers, body
def setup_stream(self):
"""设置流媒体会话"""
# OPTIONS请求
status, headers, _ = self.send_rtsp_request("OPTIONS")
if status != 200:
raise RuntimeError(f"OPTIONS failed with status {status}")
# DESCRIBE请求
status, headers, body = self.send_rtsp_request("DESCRIBE")
# 处理401认证要求
if status == 401:
print("服务器要求认证")
auth_header = headers.get("WWW-Authenticate", "")
print(f"认证头部: {auth_header}")
# 解析认证参数
auth_params = {}
if "Digest" in auth_header:
# 解析Digest认证参数
auth_str = auth_header.replace("Digest ", "")
parts = [p.strip() for p in auth_str.split(",")]
for part in parts:
if "=" in part:
key, value = part.split("=", 1)
auth_params[key.strip()] = value.strip().strip('"')
print(f"Digest认证参数: {auth_params}")
# 计算认证响应
realm = auth_params.get("realm", "")
nonce = auth_params.get("nonce", "")
response = self.calculate_digest_auth("DESCRIBE",
f"rtsp://{self.server_ip}/{self.stream_path}",
realm, nonce)
if response:
print("header response : ", response)
# 构造认证头
self.auth_header = (
f'Digest username="{self.username}", realm="{realm}", '
f'nonce="{nonce}", uri="rtsp://{self.server_ip}/{self.stream_path}", '
f'response="{response}"'
)
# 重试DESCRIBE请求
status, headers, body = self.send_rtsp_request("DESCRIBE")
if status != 200:
# 尝试备用路径
print("认证失败,尝试备用路径")
for alt_path in ["/Streaming/Channels/101", "/Streaming/Channels/1", "/cam/realmonitor", "ch0_0.h264"]:
print(f"尝试路径: {alt_path}")
self.stream_path = alt_path.lstrip('/')
# 重新计算认证响应(如果使用认证)
if self.auth_header and self.username and self.password:
realm = re.search(r'realm="([^"]+)"', self.auth_header).group(1)
nonce = re.search(r'nonce="([^"]+)"', self.auth_header).group(1)
response = self.calculate_digest_auth("DESCRIBE",
f"rtsp://{self.server_ip}/{self.stream_path}",
realm, nonce)
if response:
self.auth_header = (
f'Digest username="{self.username}", realm="{realm}", '
f'nonce="{nonce}", uri="rtsp://{self.server_ip}/{self.stream_path}", '
f'response="{response}"'
)
status, headers, body = self.send_rtsp_request("DESCRIBE")
if status == 200:
print("成功使用备用路径")
break
if status != 200:
raise RuntimeError(f"DESCRIBE failed with status {status}")
print("成功获取SDP描述")
# 新增:解析SPS/PPS参数
self.sps = None
self.pps = None
sps_pps_match = re.search(r'sprop-parameter-sets=([^,\s]+),([^\s]+)', body)
if sps_pps_match:
try:
self.sps = base64.b64decode(sps_pps_match.group(1))
self.pps = base64.b64decode(sps_pps_match.group(2))
print(f"提取到SPS: {len(self.sps)}字节, PPS: {len(self.pps)}字节")
except Exception as e:
print(f"解析SPS/PPS失败: {e}")
print(f"SDP内容:\n{body}")
# 从SDP信息中解析控制URL
control_match = re.search(r'a=control:(.+)\r\n', body)
if control_match:
self.control_url = control_match.group(1).strip()
print(f"解析到会话级控制URL: {self.control_url}")
# 从SDP信息中解析视频轨道控制URL
video_control_match = re.search(r'm=video.*\r\na=control:(.+)\r\n', body)
if video_control_match:
self.video_control_url = video_control_match.group(1).strip()
print(f"解析到视频轨道控制URL: {self.video_control_url}")
else:
# 如果找不到视频轨道URL,使用会话级URL加上trackID=1
if self.control_url.startswith("rtsp"):
self.video_control_url = f"{self.control_url}/trackID=1"
else:
self.video_control_url = "rtsp://" + str(self.server_ip) + ":" + str(self.server_port) + "/" + str(self.stream_path)+"/"+str(self.control_url)
print(f"使用默认视频轨道URL: {self.video_control_url}")
# 尝试TCP传输方式(海康威视推荐方式)
print("尝试TCP传输模式")
transport = "RTP/AVP/TCP;unicast;interleaved=0-1"
setup_headers = {
"Transport": transport,
"Timeout": 10
}
if self.session_id:
setup_headers["Session"] = self.session_id
try:
status, headers, _ = self.send_rtsp_request("SETUP", extra_headers=setup_headers)
if status == 200:
print("TCP传输模式设置成功")
self.transport_mode = "tcp"
else:
raise RuntimeError(f"SETUP failed with status {status}")
except Exception as e:
print(f"SETUP请求错误: {e}")
raise RuntimeError("TCP传输模式设置失败")
# 保存Session ID
self.session_id = headers.get("Session", "").split(';')[0]
print(f"Session ID: {self.session_id}")
# 解析传输参数
transport = headers.get("Transport", "")
print(f"传输参数: {transport}")
def start_stream(self):
"""开始播放流"""
play_headers = {
"Session": self.session_id,
"Range": "npt=0.000-",
"Timeout": 10
}
try:
status, headers, _ = self.send_rtsp_request("PLAY", extra_headers=play_headers)
if status != 200:
raise RuntimeError(f"PLAY failed with status {status}")
except Exception as e:
print(f"PLAY请求错误: {e}")
return
# 启动接收线程
self.running = True
self.receive_thread = threading.Thread(target=self.receive_data)
self.receive_thread.daemon = True
self.receive_thread.start()
def receive_data(self):
"""接收和处理所有数据(RTSP响应和RTP包)"""
buffer = b''
while self.running:
try:
chunk = self.rtsp_socket.recv(4096)
if not chunk:
break
buffer += chunk
# 处理缓冲区中的所有数据
while len(buffer) > 0:
# 检查是否是RTP包 (以'$'开头)
if buffer[0] == 0x24: # '$'符号
if len(buffer) < 4:
break # 等待更多数据
# 解析RTP头
channel = buffer[1]
packet_length = struct.unpack('>H', buffer[2:4])[0]
# 检查是否收到完整包
if len(buffer) < 4 + packet_length:
break # 等待更多数据
# 提取RTP包
rtp_packet = buffer[4:4 + packet_length]
buffer = buffer[4 + packet_length:]
# 处理RTP包
self.process_rtp_packet(rtp_packet, channel)
else:
# 处理RTSP响应
end_pos = buffer.find(b'\r\n\r\n')
if end_pos == -1:
break # 等待更多数据
rtsp_response = buffer[:end_pos + 4]
buffer = buffer[end_pos + 4:]
# 解析RTSP响应
self.process_rtsp_response(rtsp_response)
except socket.timeout:
continue
except Exception as e:
print(f"接收数据错误: {e}")
break
def process_rtsp_response(self, response_data):
"""处理RTSP响应(用于TCP模式)"""
response_str = response_data.decode(errors='ignore')
print(f"收到RTSP响应:\n{response_str}")
def process_rtp_packet(self, packet, channel):
"""处理RTP包(TCP模式)"""
if len(packet) < 12:
return
# 解析RTP头部
rtp_header = packet[0:12]
version = (rtp_header[0] >> 6) & 0x03
padding = (rtp_header[0] >> 5) & 0x01
extension = (rtp_header[0] >> 4) & 0x01
csrc_count = rtp_header[0] & 0x0F
marker = (rtp_header[1] >> 7) & 0x01
payload_type = rtp_header[1] & 0x7F
sequence_number = struct.unpack('>H', rtp_header[2:4])[0]
timestamp = struct.unpack('>I', rtp_header[4:8])[0]
ssrc = struct.unpack('>I', rtp_header[8:12])[0]
# 计算头部长度
header_length = 12 + 4 * csrc_count
# 处理扩展头
if extension:
if len(packet) < header_length + 4:
return
extension_header = packet[header_length:header_length + 4]
extension_length = struct.unpack('>H', extension_header[2:4])[0]
header_length += 4 + 4 * extension_length
# 处理填充
payload = packet[header_length:]
if padding:
padding_length = payload[-1]
payload = payload[:-padding_length]
# 处理H.264负载
self.process_h264_payload(payload, timestamp)
def process_h264_payload(self, payload, timestamp):
"""处理H.264负载"""
if len(payload) < 1:
return
nal_header = payload[0]
nal_type = nal_header & 0x1F
# 单NAL单元包 (1-23)
if 1 <= nal_type <= 23:
nal_unit = payload
self.frame_buffer.append((timestamp, nal_unit))
# 分片单元 (FU-A)
elif nal_type == 28: # FU-A
if len(payload) < 2:
return
fu_header = payload[1]
start_bit = (fu_header & 0x80) >> 7
end_bit = (fu_header & 0x40) >> 6
# 重构NAL单元头
reconstructed_nal_header = (nal_header & 0xE0) | (fu_header & 0x1F)
if start_bit:
# 开始分片
self.current_frame = bytes([reconstructed_nal_header]) + payload[2:]
else:
# 继续分片
self.current_frame += payload[2:]
if end_bit:
# 分片结束
self.frame_buffer.append((timestamp, self.current_frame))
self.current_frame = b''
def get_frame(self):
"""从缓冲区获取一帧"""
if self.frame_buffer:
return self.frame_buffer.popleft()[1]
return None
def stop(self):
"""停止流并清理"""
self.running = False
# 发送TEARDOWN请求
if self.rtsp_socket and self.session_id:
teardown_headers = {
"Session": self.session_id
}
try:
self.send_rtsp_request("TEARDOWN", extra_headers=teardown_headers)
except:
pass
# 关闭套接字
if self.rtsp_socket:
try:
self.rtsp_socket.close()
except:
pass
# 使用示例
if __name__ == "__main__":
# 替换为你的摄像头RTSP URL
# CAMERA_URL = "rtsp://username:password@ip:554/h264/ch0/main/av_stream"
CAMERA_URL = r"rtsp://localhost:5001/stream_1"
client = RTSPClient(CAMERA_URL)
try:
print("连接摄像头...")
client.connect()
print("设置流...")
client.setup_stream()
print("开始播放...")
client.start_stream()
print("接收帧数据 (按Ctrl+C停止)...")
frame_count = 0
while True:
frame = client.get_frame()
if frame:
frame_count += 1
# 这里可以添加帧处理逻辑
print(f"接收到帧 #{frame_count}, 大小: {len(frame)} 字节")
else:
break
# pass
time.sleep(0.01)
except KeyboardInterrupt:
print("用户中断")
except Exception as e:
print(f"错误: {e}")
finally:
client.stop()
print("连接关闭")
"""
设备地址: localhost:5001
流路径: stream_1
用户名: admin
密码: admin
连接摄像头...
设置流...
cur uri : stream_1
发送请求:
OPTIONS stream_1 RTSP/1.0
CSeq: 1
User-Agent: 实现自己的AI视频监控系统
b'RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: GlVt04uHg\r\nPublic: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD\r\n\r\n'
收到响应:
RTSP/1.0 200 OK
CSeq: 1
Session: GlVt04uHg
Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD
cur uri : stream_1
发送请求:
DESCRIBE rtsp://localhost:5001/stream_1 RTSP/1.0
CSeq: 2
User-Agent: 实现自己的AI视频监控系统
b'RTSP/1.0 200 OK\r\nCSeq: 2\r\nSession: GlVt04uHg\r\nContent-Type: application/sdp\r\nContent-Length: 289\r\n\r\nv=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=No Name\r\nc=IN IP4 127.0.0.1\r\nt=0 0\r\na=tool:liveqing 58.20.100\r\nm=video 0 RTP/AVP 96\r\na=rtpmap:96 H264/90000\r\na=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAKY2NQDwBE/LNwEBAUAAAcIAAFfkAQA==,aM48gA==; profile-level-id=420029\r\na=control:streamid=0\r\n'
content_length : 289
收到响应:
RTSP/1.0 200 OK
CSeq: 2
Session: GlVt04uHg
Content-Type: application/sdp
Content-Length: 289
v=0
o=- 0 0 IN IP4 127.0.0.1
s=No Name
c=IN IP4 127.0.0.1
t=0 0
a=tool:liveqing 58.20.100
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAKY2NQDwBE/LNwEBAUAAAcIAAFfkAQA==,aM48gA==; profile-level-id=420029
a=control:streamid=0
成功获取SDP描述
提取到SPS: 25字节, PPS: 4字节
SDP内容:
v=0
o=- 0 0 IN IP4 127.0.0.1
s=No Name
c=IN IP4 127.0.0.1
t=0 0
a=tool:liveqing 58.20.100
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAKY2NQDwBE/LNwEBAUAAAcIAAFfkAQA==,aM48gA==; profile-level-id=420029
a=control:streamid=0
解析到会话级控制URL: streamid=0
使用默认视频轨道URL: rtsp://localhost:5001/stream_1/streamid=0
尝试TCP传输模式
cur uri : rtsp://localhost:5001/stream_1/streamid=0
发送请求:
SETUP rtsp://localhost:5001/stream_1/streamid=0 RTSP/1.0
CSeq: 3
User-Agent: 实现自己的AI视频监控系统
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
Timeout: 10
b'RTSP/1.0 200 OK\r\nCSeq: 3\r\nSession: GlVt04uHg\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n'
收到响应:
RTSP/1.0 200 OK
CSeq: 3
Session: GlVt04uHg
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
TCP传输模式设置成功
Session ID: GlVt04uHg
传输参数: RTP/AVP/TCP;unicast;interleaved=0-1
开始播放...
cur uri : streamid=0
发送请求:
PLAY streamid=0 RTSP/1.0
CSeq: 4
User-Agent: 实现自己的AI视频监控系统
Session: GlVt04uHg
Range: npt=0.000-
Timeout: 10
b'RTSP/1.0 200 OK\r\nCSeq: 4\r\nSession: GlVt04uHg\r\nRange: npt=0.000-\r\n\r\n'
收到响应:
RTSP/1.0 200 OK
CSeq: 4
Session: GlVt04uHg
Range: npt=0.000-
接收帧数据 (按Ctrl+C停止)...
接收到帧 #1, 大小: 25 字节
接收到帧 #2, 大小: 4 字节
接收到帧 #3, 大小: 79070 字节
接收到帧 #4, 大小: 7155 字节
接收到帧 #5, 大小: 6007 字节
接收到帧 #6, 大小: 6022 字节
接收到帧 #7, 大小: 5322 字节
接收到帧 #8, 大小: 5311 字节
接收到帧 #9, 大小: 5443 字节
....
用户中断
cur uri : stream_1
发送请求:
TEARDOWN stream_1 RTSP/1.0
CSeq: 5
User-Agent: 实现自己的AI视频监控系统
Session: GlVt04uHg
b'RTSP/1.0 200 OK\r\nCSeq: 5\r\nSession: GlVt04uHg\r\n\r\n'
收到响应:
RTSP/1.0 200 OK
CSeq: 5
Session: GlVt04uHg
接收数据错误: [WinError 10038] 在一个非套接字上尝试了一个操作。
连接关闭
进程已结束,退出代码0
"""
总结
深入原理或协议的专业知识一般都是烦躁且无味的,不过我十分推荐大家伙有空的时候可以静下心来好好学习一下该部分内容。示例代码有误或有其它问题可以及时联系我
ps:拉流和解码都有非常成熟的的三方库,本章内容主要为教学,但是对于底层网络方面的定制性开发,这块内容就需要深入学习了
下期预告
- 编解码算法 这部分内容暂不考虑,因为涉及的原理和内容比较复杂
- 多路视频拉流模块设计