FFmpeg实现RTSP推拉流
📅 2026/8/2 19:06:42
👁️ 阅读次数
📝 编程学习
RTSP(Real Time Streaming Protocol)推流与拉流的实现,核心在于建立符合RTSP/RTP/RTCP协议栈的客户端与服务器。以下是基于不同场景和工具的实现方法。
一、 使用现成服务器与工具(快速验证)
这是最快捷的方式,适用于功能验证和简单应用。
1. 推流实现
推流指将本地视频源(文件、摄像头、屏幕)发布到RTSP服务器。
| 工具/组件 | 推流命令/方法示例 | 说明 |
|---|---|---|
| FFmpeg | ffmpeg -re -i input.mp4 -c copy -f rtsp rtsp://server_ip:port/live/stream | 将本地文件以原始编码(-c copy)推流到RTSP服务器。-re表示按输入文件速率读取。 |
| FFmpeg (摄像头) | ffmpeg -f dshow -i video="USB Camera" -vcodec libx264 -f rtsp rtsp://server_ip:port/live/stream(Windows)ffmpeg -f v4l2 -i /dev/video0 -vcodec libx264 -f rtsp rtsp://server_ip:port/live/stream(Linux) | 捕获USB摄像头视频,编码为H.264后推流。 |
| GStreamer | gst-launch-1.0 v4l2src ! videoconvert ! x264enc ! rtspclientsink location=rtsp://server_ip:port/live/stream | 使用GStreamer管道,从V4L2设备采集、编码并推流。 |
| OpenCV VideoWriter | ```python out = cv2.VideoWriter( 'appsrc ! videoconvert ! x264enc ! rtspclientsink location=rtsp://...', cv2.CAP_GSTREAMER, 0, fps, (width, height) ) |
#### 2. 拉流实现 拉流指从RTSP服务器获取并播放或处理视频流。 | 工具/组件 | 拉流命令/方法示例 | 说明 | | :--- | :--- | :--- | | **FFplay (FFmpeg)** | `ffplay rtsp://server_ip:port/live/stream` | 最简单的拉流播放工具。 | | **VLC Media Player** | 媒体 -> 打开网络串流 -> 输入RTSP URL | 图形化工具,支持拉流播放。 | | **OpenCV VideoCapture** | ```python<br>cap = cv2.VideoCapture('rtsp://server_ip:port/live/stream')<br>while cap.isOpened():<br> ret, frame = cap.read()<br> # 处理frame<br> ``` | 在Python中拉流并获取每一帧图像进行处理。 | | **GStreamer** | `gst-launch-1.0 rtspsrc location=rtsp://server_ip:port/live/stream ! decodebin ! autovideosink` | 使用GStreamer管道拉流并解码显示。 | #### 3. 轻量级RTSP服务器 进行推拉流测试需要一个RTSP服务器。**mediamtx**(原rtsp-simple-server)是一个极简的选择。 1. **下载并启动服务器**: ```bash # 从 https://github.com/bluenviron/mediamtx/releases 下载对应平台版本 ./mediamtx ``` 服务器默认在 `:8554` 端口监听。 2. **推流到服务器**: ```bash ffmpeg -re -i input.mp4 -c copy -f rtsp rtsp://localhost:8554/mystream ``` 3. **从服务器拉流**: ```bash ffplay rtsp://localhost:8554/mystream ``` ### 二、 编程实现(C/C++示例) 对于嵌入式或需要深度集成的场景,需要编程实现RTSP客户端/服务器。 #### 1. RTSP推流客户端实现要点 一个基本的RTSP推流客户端需要完成以下步骤: ```c // 伪代码流程1. 建立TCP连接至RTSP服务器(默认端口554)。 2. 发送DESCRIBE请求,获取媒体描述(SDP)。 3. 发送SETUP请求,为音视频流建立传输通道(RTP/RTCP端口)。 4. 发送PLAY请求,开始播放。 // 对于推流(ANNOUNCE, RECORD),服务器需支持相关方法。 5. 在SETUP建立的UDP或TCP套接字上,按照RTP协议封装并发送音视频数据。 将H.264 NALU封装为RTP包。一个NALU可能被分片(FU-A)或打包成单个RTP包。 RTP头需设置序列号、时间戳、负载类型等。 6. 周期性发送RTCP接收报告(RR)或发送报告(SR)。关键代码片段(RTP封装H.264 NALU):
// 假设有一个H.264 NALU (nal_data, nal_len),需要封装为RTP包 void send_rtp_h264_packet(int rtp_sock, struct sockaddr_in *dest_addr, const uint8_t *nal_data, int nal_len, uint16_t *seq_num, uint32_t *timestamp) { uint8_t rtp_packet[1500]; // MTU大小 int rtp_header_len = 12; // 1. 构建RTP头 (RFC 3550) rtp_packet[0] = 0x80; // V=2, P=0, X=0, CC=0 rtp_packet[1] = 96; // PT=96 (动态负载类型,H.264常用) rtp_packet[2] = (*seq_num >> 8) & 0xFF; rtp_packet[3] = *seq_num & 0xFF; (*seq_num)++; // 设置时间戳 (根据时钟频率递增,H.264视频通常为90000 Hz) rtp_packet[4] = (*timestamp >> 24) & 0xFF; rtp_packet[5] = (*timestamp >> 16) & 0xFF; rtp_packet[6] = (*timestamp >> 8) & 0xFF; rtp_packet[7] = *timestamp & 0xFF; // SSRC等字段省略... // 2. 封装H.264负载 (根据RFC 6184) if (nal_len <= 1400) { // 单包模式 (Single NALU) memcpy(rtp_packet + rtp_header_len, nal_data, nal_len); send_packet(rtp_sock, dest_addr, rtp_packet, rtp_header_len + nal_len); } else { // 分片模式 (FU-A) // FU indicator rtp_packet[rtp_header_len] = (nal_data[0] & 0xE0) | 28; // FU-A type=28 // FU header (S|E|R) + NALU type int offset = 1; // 跳过NALU头 int remaining = nal_len - offset; int max_payload_size = 1400; int start = 1; while (remaining > 0) { int payload_size = (remaining > max_payload_size) ? max_payload_size : remaining; rtp_packet[rtp_header_len + 1] = (start ? 0x80 : 0x00) | // Start bit ((remaining payload_size == 0) ? 0x40 : 0x00) | // End bit (nal_data[0] & 0x1F); // NALU type memcpy(rtp_packet + rtp_header_len + 2, nal_data + offset, payload_size); send_packet(rtp_sock, dest_addr, rtp_packet, rtp_header_len + 2 + payload_size); offset += payload_size; remaining -= payload_size; start = 0; } } }2. RTSP拉流客户端实现要点
拉流客户端与推流客户端步骤1-4类似,区别在于第5步是接收并解析RTP包,重组H.264 NALU。
// 伪代码流程 (接收端) 14. 同推流客户端,建立RTSP会话。 5. 在SETUP建立的UDP或TCP套接字上,循环接收RTP包。 6. 解析RTP头,获取序列号、时间戳、负载类型。 7. 根据RFC 6184解析H.264负载: 单包模式:直接提取NALU。 - FU-A分片模式:缓存分片,根据起始和结束标记重组完整的NALU。 8. 将重组后的NALU送入解码器(如FFmpeg、硬件解码器)或进行其他处理。关键代码片段(解析RTP并重组H.264):
typedef struct { uint8_t buffer[MAX_NALU_SIZE]; int size; uint16_t seq; // 期望的下一个序列号 uint32_t timestamp; int fu_a_started; } NaluAssembler; void process_rtp_packet(NaluAssembler *assembler, const uint8_t *rtp_packet, int packet_len) { // 解析RTP头 int payload_type = rtp_packet[1] & 0x7F; uint16_t seq_num = (rtp_packet[2] << 8) | rtp_packet[3]; uint32_t timestamp = (rtp_packet[4] << 24) | (rtp_packet[5] << 16) | (rtp_packet[6] << 8) | rtp_packet[7]; const uint8_t *payload = rtp_packet + 12; // 假设无CSRC int payload_size = packet_len - 12; if (payload_type != 96) return; // 非H.264负载 uint8_t nal_unit_type = payload[0] & 0x1F; if (nal_unit_type >= 1 && nal_unit_type <= 23) { // 单包NALU deliver_nalu(payload, payload_size, timestamp); } else if (nal_unit_type == 28) { // FU-A uint8_t fu_header = payload[1]; int start_bit = (fu_header >> 7) & 1; int end_bit = (fu_header >> 6) & 1; uint8_t reconstructed_nal_type = fu_header & 0x1F; if (start_bit) { // 开始分片 assembler->size = 0; assembler->timestamp = timestamp; assembler->fu_a_started = 1; // 重组NALU头 assembler->buffer[0] = (payload[0] & 0xE0) | reconstructed_nal_type; assembler->size = 1; } if (assembler->fu_a_started && assembler->timestamp == timestamp) { // 追加分片数据 memcpy(assembler->buffer + assembler->size, payload + 2, payload_size2); assembler->size += (payload_size - 2); } if (end_bit && assembler->fu_a_started && assembler->timestamp == timestamp) { // 分片结束,交付完整NALU deliver_nalu(assembler->buffer, assembler->size, timestamp); assembler->fu_a_started = 0; } } // 处理其他类型,如STAP-A, MTAP16等 }三、 特定平台实现(Android屏幕推流)
在Android上实现屏幕推流到RTSP服务器,涉及屏幕捕获、编码和网络传输。
// 核心步骤摘要 // 1. 获取MediaProjection权限,创建VirtualDisplay捕获屏幕 MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); Intent captureIntent = projectionManager.createScreenCaptureIntent(); startActivityForResult(captureIntent, REQUEST_CODE); // 在onActivityResult中 MediaProjection mediaProjection = projectionManager.getMediaProjection(resultCode, data); // 配置视频尺寸、DPI等 VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null); // 2. 配置MediaCodec进行硬件编码(H.264) MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface inputSurface = encoder.createInputSurface(); encoder.start(); // 将VirtualDisplay的surface设置为编码器的inputSurface // virtualDisplay = mediaProjection.createVirtualDisplay(..., inputSurface, ...); // 3. 从MediaCodec输出缓冲区获取编码后的H.264数据 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferId = encoder.dequeueOutputBuffer(bufferInfo, timeoutUs); if (outputBufferId >= 0) { ByteBuffer outputBuffer = encoder.getOutputBuffer(outputBufferId); byte[] encodedData = new byte[bufferInfo.size]; outputBuffer.get(encodedData); // 4. 将encodedData(H.264 NALU)通过RTP协议发送至RTSP服务器 //需要实现RTSP客户端协议栈及RTP打包逻辑(如第二部分所述) sendRtpPacket(encodedData, bufferInfo.presentationTimeUs); encoder.releaseOutputBuffer(outputBufferId, false); }四、 推拉流与AI处理结合
常见场景是拉流->AI推理->推流。
import cv2 import subprocess import threading # 1. RTSP拉流 rtsp_pull_url = "rtsp://server_ip:port/input_stream" cap = cv2.VideoCapture(rtsp_pull_url) # 2. 准备RTSP推流管道 (使用FFmpeg) rtsp_push_url = "rtsp://server_ip:port/output_stream" command = ['ffmpeg', '-y', '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', 'bgr24', '-s', '{}x{}'.format(width, height), '-r', str(fps), '-i', '-', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'ultrafast', '-f', 'rtsp', rtsp_push_url] p = subprocess.Popen(command, stdin=subprocess.PIPE) while cap.isOpened(): ret, frame = cap.read() if not ret: break # 3. AI推理处理 (例如YOLOv8) # results = model(frame) # processed_frame = plot_boxes(frame, results) processed_frame = frame # 此处省略推理步骤 # 4.将处理后的帧推流 p.stdin.write(processed_frame.tobytes()) cap.release() p.stdin.close() p.wait()实现关键点总结:
- 协议理解:必须理解RTSP(信令控制)、RTP(数据传输)、RTCP(传输控制)协议交互过程。
- 数据封装:视频编码数据(如H.264 NALU)需要按照RFC 6184规范封装成RTP包。注意NALU分片(FU-A)与组合。
- 格式兼容:确保推流端输出的H.264码流格式(AVCC或Annex-B)与接收端或服务器期望的格式匹配。例如,某些解码器可能只支持Annex-B格式。
- 网络适应:处理网络抖动、丢包。使用RTCP进行QoS反馈,必要时实现丢包重传或前向纠错。
- 性能优化:对于高帧率、高分辨率视频,采用硬件编解码、多线程处理、零拷贝等技术降低延迟和CPU占用。
参考来源
- h264 NAL解析
- 代码实现rtsp推流拉流的方法(自己总结)
- Windows、linux一台电脑rtsp推流另一台拉流显示
- RTMP推流组件EasyRTMP实现内网摄像头RTSP拉流转码RTMP推流到RTMP服务器之Android版屏幕推流方法及其注意点
- RTMP推流组件EasyRTMP实现内网摄像头RTSP拉流转码RTMP推流到RTMP服务器之Android版屏幕推流方法及其注意点
- rtsp拉流+yolov8推理+rtsp推流
编程学习
技术分享
实战经验