RK3588——网口实时传输视频

由于通过流媒体服务器传输画面延迟太高的问题,不知道是没有调试到合适的参数还是其他什么问题。诞生了这篇博客。

RK3588板端上接摄像头,采集画面,通过网口实时传输给上位机并显示。

第一代版本

RK3588代码
import cv2
import socket
import struct

# 配置
SERVER_IP = '192.168.137.1'  # 上位机的IP地址
PORT = 5000  # 端口号

# 创建一个socket对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 打开摄像头
cap = cv2.VideoCapture(23)

if not cap.isOpened():
    print("无法打开摄像头")
    exit()

while True:
    # 读取摄像头帧
    ret, frame = cap.read()
    if not ret:
        print("无法读取帧")
        break
    frame = cv2.resize(frame,(320,240))
    # 对帧进行编码
    encoded, buffer = cv2.imencode('.jpg', frame)

    if not encoded:
        print("编码帧失败")
        break

    # 发送数据
    data = buffer.tobytes()
    print(len(data))
    print(len(struct.pack('L', len(data))))
    print(struct.pack('L', len(data)))
    # 发送数据大小
    client_socket.sendto(struct.pack('L', len(data)), (SERVER_IP, PORT))
    # 发送数据
    client_socket.sendto(data, (SERVER_IP, PORT))

# 释放资源
cap.release()
client_socket.close()
上位机代码(windows系统)
import cv2
import socket
import numpy as np

# 配置
PORT = 5000  # 端口号

# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('0.0.0.0', PORT))

# 创建窗口并设置为全屏模式
cv2.namedWindow('Video Stream', cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty('Video Stream', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)

while True:
    # 接收数据大小
    data_size, _ = server_socket.recvfrom(8)

    # 接收的视频数据长度
    data_size = int.from_bytes(data_size, byteorder='little')

    # 接收视频数据
    data, _ = server_socket.recvfrom(data_size)

    # 解码
    np_data = np.frombuffer(data, dtype=np.uint8)
    frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)

    if frame is None:
        print("解码帧失败")
        continue

    # 显示帧
    cv2.imshow('Video Stream', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放资源
server_socket.close()
cv2.destroyAllWindows()

测试视频

遗留问题:视频每帧只能一次传输完毕,且每帧的大小不能过大,不然会报错。

第二代版本

通过分组发送帧图像的方式,即segment_size,优化了帧图像必须要一次传输的问题。

并且把b'\xff\xff'当作一帧画面传输完成的表中,暂时还没发现什么问题。

RK3588代码
import cv2
import socket

# 配置
SERVER_IP = '192.168.137.1'  # 上位机的IP地址
PORT = 5000  # 端口号

# 创建一个socket对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 打开摄像头
cap = cv2.VideoCapture(23)

if not cap.isOpened():
    print("无法打开摄像头")
    exit()

frame_count = 0
segment_size = 65000 #32678 # 一组数据的大小
while True:
    # 读取摄像头帧
    ret, frame = cap.read()
    if not ret:
        print("无法读取帧")
        break

    # 对帧进行编码
    encoded, buffer = cv2.imencode('.jpg', frame)

    if not encoded:
        print("编码帧失败")
        break

    # 发送数据
    data = buffer.tobytes()
    data_len = len(data) # 一帧图像的数据大小
    print(data_len)
    # 发送数据

    time = data_len // segment_size # 需要发多少组
    left = data_len % segment_size # 剩下多少个
    if time > 0:
        for i in range(time): # 遍历发送所有的组
            start_pos = i*segment_size 
            end_pos = (i+1)*segment_size
            client_socket.sendto(data[start_pos:end_pos], (SERVER_IP, PORT))
    # 发送剩下的,并且加上结束帧标识符
    client_socket.sendto(data[time*segment_size:data_len] + b'\xff\xff', (SERVER_IP, PORT))

    frame_count +=1
# 释放资源
cap.release()
client_socket.close()
上位机代码(windows系统)
import cv2
import socket
import numpy as np

# 配置
PORT = 5000  # 端口号

# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('0.0.0.0', PORT))
# 一组数据的大小
segment_size = 65000 #32678

while True:
    data_byte = b''
    # 接收视频数据
    data, _ = server_socket.recvfrom(segment_size)
    while (b'\xff\xff' not in data):
        data_byte += data
        data, _ = server_socket.recvfrom(segment_size)
    # 把分组发送的字节相加
    data_byte += data
    #去除最后两个结束帧标识符
    rev_data = data_byte[:-2]

    # 解码
    np_data = np.frombuffer(rev_data, dtype=np.uint8)
    frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)

    if frame is None:
        print("解码帧失败")
        continue

    # 显示帧
    cv2.imshow('Video Stream', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放资源
server_socket.close()
cv2.destroyAllWindows()

后续优化方向:可以通过多线程的方式去发送每组的数据,这样能进一步增加实时性。

第三代版本

增加了crc校验,对于显示不完整的帧不显示,解决了上一代版本上位机画面闪烁的问题。

RK3588代码
import cv2
import socket
import time
import struct
import zlib
 
# 配置
SERVER_IP = '192.168.1.27'  # 上位机的IP地址
PORT = 5001  # 端口号
FPS = 20  # 降低帧率以减轻网络压力
COMPRESSION_QUALITY = 70  # 降低JPEG压缩质量,减小数据量
 
# 创建一个socket对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4194304)  # 4MB发送缓冲区
 
# 相机连接参数
CAMERA_CONFIG = {
    'ip': '192.168.10.101',
    'port': 80,
    'username': 'admin',
    'password': 'a1234567'
}

# RTSP流地址
RTSP_URL = f"rtsp://{CAMERA_CONFIG['username']}:{CAMERA_CONFIG['password']}@{CAMERA_CONFIG['ip']}:554/h264/ch1/sub/av_stream"

print("正在连接摄像头...")
# 打开摄像头
cap = cv2.VideoCapture(RTSP_URL)
 
if not cap.isOpened():
    print("无法打开摄像头")
    exit()

print("摄像头连接成功")

# 尝试设置摄像头参数
cap.set(cv2.CAP_PROP_FPS, FPS)  # 设置帧率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)  # 设置宽度
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)  # 设置高度
 
frame_count = 0
segment_size = 8192  # 减小分片大小,降低单包丢失影响
last_frame_time = time.time()
start_time = time.time()

def send_frame(data, frame_id):
    """发送一帧数据,使用可靠的分片方法"""
    # 计算分片数量
    total_segments = (len(data) + segment_size - 1) // segment_size
    
    # 如果分片过多,跳过此帧
    if total_segments > 100:
        print(f"帧太大,分片数量: {total_segments},跳过")
        return False
    
    # 为每个分片添加头信息:帧ID(4字节) + 分片索引(2字节) + 总分片数(2字节) + CRC32(4字节)
    for i in range(total_segments):
        # 计算当前分片数据
        start_pos = i * segment_size
        end_pos = min(start_pos + segment_size, len(data))
        segment_data = data[start_pos:end_pos]
        
        # 计算数据校验和
        checksum = zlib.crc32(segment_data)
        
        # 创建头信息
        header = struct.pack(">IHH", frame_id, i, total_segments)
        
        # 如果是最后一个分片,添加结束标记
        if i == total_segments - 1:
            # 发送分片 (头信息 + 数据 + 校验和 + 结束标记)
            packet = header + segment_data + struct.pack(">I", checksum) + b'\xff\xff'
        else:
            # 发送分片 (头信息 + 数据 + 校验和)
            packet = header + segment_data + struct.pack(">I", checksum)
        
        # 发送数据
        client_socket.sendto(packet, (SERVER_IP, PORT))
        
        # 控制发送速率,避免网络拥塞
        if total_segments > 10:
            time.sleep(0.001)
    
    return True

try:
    print("开始发送视频流...")
    while True:
        # 控制帧率
        current_time = time.time()
        if current_time - last_frame_time < 1.0/FPS:
            time.sleep(0.001)  # 短暂休眠避免CPU占用过高
            continue
            
        # 读取摄像头帧
        ret, frame = cap.read()
        if not ret:
            print("无法读取帧,尝试重新连接...")
            # 尝试重新连接摄像头
            cap.release()
            time.sleep(1)
            cap = cv2.VideoCapture(RTSP_URL)
            if not cap.isOpened():
                print("重新连接失败,退出程序")
                break
            continue
        
        last_frame_time = current_time
        
        # 调整图像尺寸,减小传输数据量
        frame = cv2.resize(frame, (640, 480))
     
        # 对帧进行编码(控制压缩质量)
        encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), COMPRESSION_QUALITY]
        encoded, buffer = cv2.imencode('.jpg', frame, encode_param)
     
        if not encoded:
            print("编码帧失败")
            continue
     
        # 获取编码后的数据
        data = buffer.tobytes()
        data_len = len(data)
        
        # 发送当前帧
        frame_id = frame_count % 10000  # 循环使用的帧ID
        success = send_frame(data, frame_id)
        
        if success:
            frame_count += 1
            
            # 每5秒显示一次统计信息
            if frame_count % 100 == 0:
                elapsed = time.time() - start_time
                fps = frame_count / elapsed
                print(f"已发送 {frame_count} 帧,平均发送速率: {fps:.1f} fps,平均帧大小: {data_len/1024:.1f} KB")
        
except KeyboardInterrupt:
    print("程序被用户中断")
except Exception as e:
    print(f"发生错误: {e}")
finally:
    # 释放资源
    print(f"程序结束,共发送 {frame_count} 帧")
    cap.release()
    client_socket.close()
上位机代码(windows系统):
import cv2
import socket
import numpy as np
import time
import struct
import zlib
from collections import deque, defaultdict
 
# 配置
PORT = 5001  # 端口号
BUFFER_SIZE = 15  # 帧缓冲区大小
TARGET_FPS = 25  # 目标帧率
DISPLAY_WIDTH = 640  # 显示窗口宽度
DISPLAY_HEIGHT = 480  # 显示窗口高度
 
# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('0.0.0.0', PORT))
# 设置socket更大的接收缓冲区
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8388608)  # 8MB缓冲区
# 设置socket超时,避免一直阻塞
server_socket.settimeout(0.01)

# 创建帧缓冲队列
frame_buffer = deque(maxlen=BUFFER_SIZE)
last_display_time = time.time()
last_frame = None

# 数据接收缓冲区,格式: {frame_id: {segment_idx: segment_data, ...}}
frame_segments = defaultdict(dict)
# 帧片段计数,格式: {frame_id: (received_segments, total_segments)}
frame_segment_counts = {}

# 创建固定大小的显示窗口
cv2.namedWindow('Video Stream', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Video Stream', DISPLAY_WIDTH, DISPLAY_HEIGHT)

# 统计信息变量
frame_received = 0
frame_displayed = 0
corrupted_frames = 0
start_time = time.time()

print("开始接收视频流...")

def process_packet(packet):
    """处理接收到的数据包,返回完整的帧或None"""
    # 数据包太小,无法包含头信息
    if len(packet) < 12:  # 4(frame_id) + 2(segment_idx) + 2(total_segments) + 4(crc32)
        return None
        
    try:
        # 提取头信息:帧ID + 分片索引 + 总分片数
        header = packet[:8]
        frame_id, segment_idx, total_segments = struct.unpack(">IHH", header)
        
        # 检查是否是结束标记
        has_end_marker = False
        if packet[-2:] == b'\xff\xff':
            # 去掉结束标记
            data = packet[8:-6]  # 去掉头部8字节和尾部6字节(4字节CRC + 2字节结束标记)
            checksum_bytes = packet[-6:-2]  # 取出校验和
            has_end_marker = True
        else:
            # 没有结束标记
            data = packet[8:-4]  # 去掉头部8字节和尾部4字节校验和
            checksum_bytes = packet[-4:]  # 取出校验和
        
        # 验证校验和
        received_checksum = struct.unpack(">I", checksum_bytes)[0]
        calculated_checksum = zlib.crc32(data)
        
        if received_checksum != calculated_checksum:
            print(f"校验和错误: 帧ID={frame_id}, 分片={segment_idx}, 收到={received_checksum}, 计算={calculated_checksum}")
            return None
            
        # 更新帧分片信息
        frame_segments[frame_id][segment_idx] = data
        if frame_id not in frame_segment_counts:
            frame_segment_counts[frame_id] = [1, total_segments]
        else:
            frame_segment_counts[frame_id][0] += 1
            
        # 检查帧是否完整
        if frame_segment_counts[frame_id][0] == total_segments:
            # 按顺序拼接所有分片
            full_data = b''
            for i in range(total_segments):
                if i not in frame_segments[frame_id]:
                    print(f"错误: 帧ID={frame_id}缺少分片{i}")
                    return None
                full_data += frame_segments[frame_id][i]
            
            # 清理缓存
            del frame_segments[frame_id]
            del frame_segment_counts[frame_id]
            
            # 清理过期的帧数据(超过100个不同的帧ID时)
            if len(frame_segments) > 100:
                oldest_frame_id = min(frame_segments.keys())
                del frame_segments[oldest_frame_id]
                if oldest_frame_id in frame_segment_counts:
                    del frame_segment_counts[oldest_frame_id]
            
            return full_data
        return None
    except Exception as e:
        print(f"处理数据包错误: {e}")
        return None

while True:
    try:
        # 接收和处理帧的循环
        for _ in range(30):  # 每次显示前尝试接收更多包以填充缓冲区
            try:
                # 接收数据包
                packet, _ = server_socket.recvfrom(65535)
                full_data = process_packet(packet)
                
                if full_data is not None:
                    # 解码
                    np_data = np.frombuffer(full_data, dtype=np.uint8)
                    frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)
                    
                    if frame is not None:
                        # 调整帧大小以确保一致性
                        frame = cv2.resize(frame, (DISPLAY_WIDTH, DISPLAY_HEIGHT))
                        # 将解码成功的帧添加到缓冲区
                        frame_buffer.append(frame)
                        frame_received += 1
                    else:
                        corrupted_frames += 1
                        print("解码帧失败")
            except socket.timeout:
                # 超时继续循环
                break
            except Exception as e:
                print(f"接收/解码错误: {e}")
                break
        
        # 控制显示帧率并从缓冲区取帧显示
        current_time = time.time()
        time_elapsed = current_time - last_display_time
        
        # 按照目标帧率显示
        if time_elapsed >= 1.0/TARGET_FPS:
            # 缓冲区有足够帧时才消耗
            if len(frame_buffer) > BUFFER_SIZE // 3:
                display_frame = frame_buffer.popleft()
                last_frame = display_frame.copy()  # 保存该帧用于后续可能的填充
                cv2.imshow('Video Stream', display_frame)
                frame_displayed += 1
            # 缓冲区不足但有上一帧时显示上一帧
            elif last_frame is not None:
                cv2.imshow('Video Stream', last_frame)
            
            last_display_time = current_time
            
            # 每5秒显示一次统计信息
            if current_time - start_time > 5:
                fps_received = frame_received / (current_time - start_time)
                fps_displayed = frame_displayed / (current_time - start_time)
                buffer_status = len(frame_buffer)
                print(f"接收帧率: {fps_received:.1f} fps, 显示帧率: {fps_displayed:.1f} fps, 缓冲区: {buffer_status}/{BUFFER_SIZE}, 损坏帧: {corrupted_frames}")
                # 重置统计
                frame_received = 0
                frame_displayed = 0
                corrupted_frames = 0
                start_time = current_time
     
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    except Exception as e:
        print(f"主循环错误: {e}")
        continue
 
# 释放资源
server_socket.close()
cv2.destroyAllWindows()

### RK3588 视频推流 Python 示例代码 对于RK3588硬件平台上进行视频推流的任务,可以采用Python结合GStreamer框架来完成。这种方法不仅能够高效处理视频数据,还支持多种络协议传输视频流。下面是一个基于Python和GStreamer的简单示例程序,用于从USB摄像头捕获图像并通过RTSP协议推送出去。 #### 安装依赖库 为了运行此脚本,需先安装必要的软件包: ```bash sudo apt-get install gstreamer1.0-1.0-dev pip3 install pygst ``` #### Python代码实现 以下是完整的Python代码片段,展示如何设置并启动一个简单的RTSP服务器来进行视频推流: ```python import sys from gi.repository import Gst, GObject, GLib, GstRtspServer def main(args): # 初始化GStreamer Gst.init(None) # 创建RTSP服务器实例 server = GstRtspServer.RTSPServer.new() mounts = server.get_mount_points() factory = GstRtspServer.RTSPMediaFactory.new() factory.set_launch( "( v4l2src device=/dev/video0 ! image/jpeg,width=640,height=480,framerate=30/1 " "! jpegparse ! rtph264pay name=pay0 pt=96 )" ) mounts.add_factory("/test", factory) server.attach(None) print(f"Stream ready at rtsp://<your-ip>:8554/test") loop = GLib.MainLoop() try: loop.run() except KeyboardInterrupt: pass if __name__ == '__main__': sys.exit(main(sys.argv)) ``` 这段代码创建了一个RTSP服务端口监听于`rtsp://<your-ip>:8554/test`地址上,并通过指定路径中的USB摄像机设备(通常是`/dev/video0`)获取JPEG格式的画面帧作为输入源[^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张飞飞飞飞飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值