当前位置: 首页 > news >正文

实现自己的AI视频监控系统-第一章-视频拉流与解码2

文章目录

  • 前言(代码直接参考第二部分第三小节)
  • 一、抓包工具wireshaker的安装与使用
  • 二、第二章:拉流(Streaming)技术深度解析
    • 1. RTSP 协议交互深度剖析
    • 2. RTP 数据包结构与解析
      • 2.1 H.264 在 RTP 中的打包方式 (RFC 6184)
      • 2.2 H.264 与 H.265 (HEVC) 核心技术区别
      • 2.3 RTCP 协议的作用
    • 3. 拉流拆包的完整示例代码展示(不喜欢基础知识直接看这里)
  • 总结
  • 下期预告


前言(代码直接参考第二部分第三小节)

在上一小节的内容中,我们简要介绍了拉流和解码两个基本过程,拉流即建立与网络摄像仪的通讯,获取实时传输的数据,解码则是将传递的数据恢复成人眼观察到的彩色图像,本小节将深入拉流模块,介绍完整的拉流过程。


一、抓包工具wireshaker的安装与使用

在开始之前,本节将分享常用的抓包工具wiresharker。

  • 点击链接进入下载界面下载符合系统版本的工具
    在这里插入图片描述
  • 下载后直接按照要求安装即可
    在这里插入图片描述
  • 按照上一节使用liveNVR的方式,创建好测试的网络视频流
    在这里插入图片描述
  • 打开wiresharker,点击开始捕获分组。而后用vlc打开推送的rtsp流地址,本例地址为rtsp://localhost:5001/stream_1
    在这里插入图片描述
    在这里插入图片描述
  • 运行一段时间之后(推荐3-5秒即可),我们终止wiresharker的记录,便可查询网络通讯的消息记录
    在这里插入图片描述

二、第二章:拉流(Streaming)技术深度解析

在上一章中,我们概述了拉流是一次以RTSP协议为核心的网络会话。本章将深入这个会话的每一个细节,剖析数据包的流转,并探讨其中的关键技术与挑战。

1. RTSP 协议交互深度剖析

RTSP 协议通常运行在 TCP 上(默认端口554),确保了控制命令的可靠传输。让我们逐一分解每个步骤的请求与响应内容。参考的示意图如下所示:

RTSP ClientRTSP ServerRTP/RTCP StreamsPhase 1: Connection SetupOPTIONS requestCSeq: 1200 OKPublic: DESCRIBE, SETUP, PLAY, TEARDOWNPhase 2: Get Stream DescriptionDESCRIBE requestCSeq: 2200 OK with SDPContains media info, codecs, control URLsPhase 3: Setup Transport ChannelsSETUP for videoCSeq: 3Transport: client_port=8000-8001200 OKSession: 123456server_port=9000-9001SETUP for audioCSeq: 4Session: 123456200 OKserver_port=9002-9003Phase 4: Start StreamingPLAY requestCSeq: 5Session: 123456200 OKRTP-Info with seq & timestampPhase 5: Media Data TransmissionVideo RTP packetsAudio RTP packetsRTCP reportsloop[Continuous Streaming]Phase 6: Session TeardownTEARDOWN requestCSeq: 6Session: 123456200 OKRTSP ClientRTSP ServerRTP/RTCP Streams
  1. 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:列出了服务器支持的所有方法。
  1. 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: 500v=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)。

  1. 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。

  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包的时间戳。这两个值对于客户端正确初始化其播放缓冲区至关重要。
  1. 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                   30 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:

  1. Single NAL Unit Mode: 一个RTP包包含一个完整的NAL Unit。适用于较小的NAL单元(如SPS, PPS, SEI等)。

  2. 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。

  3. 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的端口。它主要有两个作用:

  1. QoS反馈:客户端定期向服务器发送Receiver Report (RR),汇报接收质量,包括丢包率、抖动等信息。服务器可以根据这些信息进行码率调整等优化。

  2. 流间同步:RTCP Sender Report (SR) 包含了RTP时间戳和对应的“墙上时钟”(NTP时间戳),客户端利用这个映射关系来实现音频和视频的同步播放。


3. 拉流拆包的完整示例代码展示(不喜欢基础知识直接看这里)

这里不假思索地给出完整的拉流和拆包的示例代码,rtsp视频流以liveNVR和海康IPC的H264编码视频为基准,已经通过验证和测试:

import socket
import re
import struct
import threading
import time
import hashlib
import base64
from collections import deque
from urllib.parse import urlparse, urljoinclass RTSPClient:def __init__(self, rtsp_url):self.rtsp_url = rtsp_urlself.parse_url()# RTSP状态变量self.cseq = 1self.session_id = Noneself.auth_header = Noneself.control_url = Noneself.base_url = Noneself.video_control_url = None  # 新增:视频轨道的控制URL# 网络连接self.rtsp_socket = Noneself.transport_mode = None  # 'tcp' 或 'udp'# 数据存储self.frame_buffer = deque()self.current_frame = b''self.last_rtcp_time = time.time()self.running = Falsedef parse_url(self):"""解析RTSP URL"""parsed = urlparse(self.rtsp_url)self.server_ip = parsed.hostnameself.server_port = parsed.port if parsed.port else 554self.stream_path = parsed.path.lstrip('/')self.username = parsed.username if parsed.username else "admin"self.password = parsed.password if parsed.password else "admin"# 构建基础URLself.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 responsedef build_request_uri(self, method):"""构建请求URI"""# 对于SETUP请求,使用视频轨道控制URLif method == "SETUP" and self.video_control_url:# 如果控制URL是相对路径,则构建完整URLif not self.video_control_url.startswith("rtsp://"):return urljoin(self.base_url, self.video_control_url)return self.video_control_url# 对于PLAY请求,使用会话级控制URLif 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# 其他请求使用基础URLreturn urljoin(self.base_url, self.stream_path)def send_rtsp_request(self, method, extra_headers=None):"""发送RTSP请求"""# 构建请求URIuri = 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 += 1return self.receive_rtsp_response()def receive_rtsp_response(self):"""接收RTSP响应"""response = b''while True:try:chunk = self.rtsp_socket.recv(4096)if not chunk:breakresponse += chunkprint(response)if b'\r\n\r\n' in response:# 检查是否有Content-Lengthheaders_end = response.index(b'\r\n\r\n') + 4header_text = response[:headers_end].decode()# 查找Content-Lengthcontent_length = 0for 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() + bodybreakexcept socket.timeout:breakresponse_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, bodydef 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("成功使用备用路径")breakif status != 200:raise RuntimeError(f"DESCRIBE failed with status {status}")print("成功获取SDP描述")# 新增:解析SPS/PPS参数self.sps = Noneself.pps = Nonesps_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信息中解析控制URLcontrol_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信息中解析视频轨道控制URLvideo_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=1if 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_idtry: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 IDself.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 = Trueself.receive_thread = threading.Thread(target=self.receive_data)self.receive_thread.daemon = Trueself.receive_thread.start()def receive_data(self):"""接收和处理所有数据(RTSP响应和RTP包)"""buffer = b''while self.running:try:chunk = self.rtsp_socket.recv(4096)if not chunk:breakbuffer += 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:continueexcept Exception as e:print(f"接收数据错误: {e}")breakdef 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) & 0x03padding = (rtp_header[0] >> 5) & 0x01extension = (rtp_header[0] >> 4) & 0x01csrc_count = rtp_header[0] & 0x0Fmarker = (rtp_header[1] >> 7) & 0x01payload_type = rtp_header[1] & 0x7Fsequence_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:returnextension_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:returnnal_header = payload[0]nal_type = nal_header & 0x1F# 单NAL单元包 (1-23)if 1 <= nal_type <= 23:nal_unit = payloadself.frame_buffer.append((timestamp, nal_unit))# 分片单元 (FU-A)elif nal_type == 28:  # FU-Aif len(payload) < 2:returnfu_header = payload[1]start_bit = (fu_header & 0x80) >> 7end_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 Nonedef 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 = 0while True:frame = client.get_frame()if frame:frame_count += 1# 这里可以添加帧处理逻辑print(f"接收到帧 #{frame_count}, 大小: {len(frame)} 字节")else:break# passtime.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, RECORDcur 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: 289v=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: 10b'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-1TCP传输模式设置成功
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: 10b'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: GlVt04uHgb'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:拉流和解码都有非常成熟的的三方库,本章内容主要为教学,但是对于底层网络方面的定制性开发,这块内容就需要深入学习了


下期预告

  • 编解码算法 这部分内容暂不考虑,因为涉及的原理和内容比较复杂
  • 多路视频拉流模块设计
http://www.xdnf.cn/news/1334143.html

相关文章:

  • 【网络运维】Linux 文本处理利器:sed 命令
  • Obsidian 1.9.10升级
  • Lecture 6 Kernels, Triton 课程笔记
  • python-使用鼠标对图片进行涂抹自定义绘图
  • React框架超详细入门到实战项目演练【前端】【React】
  • 玳瑁的嵌入式日记D21-08020(数据结构)
  • 河南萌新联赛2025第六场 - 郑州大学
  • 一种数字相机中的自动曝光算法
  • Java 性能优化实战(二):JVM 调优的 5 个核心维度
  • ABAP OOP革命:ALV报表面向对象改造深度实战
  • 基于Python的反诈知识科普平台 Python+Django+Vue.js
  • 49 C++ STL模板库18-类模板-pair
  • 解决前端项目启动时找不到esm文件的问题
  • PostgreSQL 流程---更新
  • 力扣面试150(61/100)
  • 使用安卓平板,通过USB数据线(而不是Wi-Fi)来控制电脑(版本1)
  • 笔试——Day44
  • 使用RealSense相机和YOLO进行实时目标检测
  • 从零开发Java坦克大战Ⅱ(上) -- 从单机到联机(架构演进与设计模式剖析)
  • 01.初识mysql数据库,了解sql语句
  • React-native之组件
  • 《算法导论》第 34 章 - NP 完全性
  • J1939协议
  • C++围绕音视频相关的资料都有哪些?如何进行学习
  • 升级Android系统webview
  • 运维日常工作100条
  • linux内核源码下载
  • Redisson3.14.1及之后连接阿里云redis代理模式,使用分布式锁:ERR unknown command ‘WAIT‘
  • 双模式 RTMP H.265 播放器解析:从国内扩展到 Enhanced RTMP 标准的演进
  • 猫头虎开源AI分享|基于大模型和RAG的一款智能text2sql问答系统:SQLBot(SQL-RAG-QABot),可以帮你用自然语言查询数据库