Files
opi-yolo/base_test/receiver.py
2026-06-15 17:10:54 +08:00

192 lines
6.4 KiB
Python

import socket
import cv2
import numpy as np
import threading
import queue
import time
import os
import sys
import logging
# =====================================================================
# 1. 环境初始化:自动挂载本地虚拟环境中的 FFmpeg 7.1 核心
# =====================================================================
try:
import imageio_ffmpeg
ffmpeg_exe_path = imageio_ffmpeg.get_ffmpeg_exe()
ffmpeg_bin_dir = os.path.dirname(ffmpeg_exe_path)
if ffmpeg_bin_dir not in os.environ["PATH"]:
os.environ["PATH"] = ffmpeg_bin_dir + os.pathsep + os.environ["PATH"]
except ImportError:
sys.exit(1)
import av
logging.basicConfig(level=logging.INFO, format='%(asctime)s - [%(levelname)s] - %(message)s')
# =====================================================================
# 2. 极速转码推流引擎(移除所有不必要的队列等待与开销)
# =====================================================================
class JPEG2H264Streamer:
def __init__(self, qgc_ip="127.0.0.1", qgc_port=5600, width=640, height=480, fps=24):
self.logger = logging.getLogger("ProxyStreamer")
self.qgc_ip = qgc_ip
self.qgc_port = qgc_port
self.width = width
self.height = height
self.fps = fps
# 缓冲区缩减到 2,宁可丢帧,也绝不容忍 1 毫秒的累积排队延迟
self.frame_queue = queue.Queue(maxsize=2)
self.is_running = False
self.encoder_thread = None
def start(self):
self.is_running = True
self.encoder_thread = threading.Thread(target=self._encoder_push_worker, daemon=True)
self.encoder_thread.start()
def push_frame(self, frame):
if not self.is_running:
return
try:
self.frame_queue.put_nowait(frame)
except queue.Full:
try:
self.frame_queue.get_nowait() # 顶出老帧
self.frame_queue.put_nowait(frame)
except queue.Empty:
pass
def _encoder_push_worker(self):
output_url = f"udp://{self.qgc_ip}:{self.qgc_port}"
output_container = None
stream = None
pts = 0
while self.is_running:
try:
if output_container is None:
output_container = av.open(output_url, mode='w', format='mpegts')
stream = output_container.add_stream('libx264', rate=self.fps)
stream.width = self.width
stream.height = self.height
stream.pix_fmt = 'yuv420p'
# 强悍的零延迟参数配置
stream.options = {
'preset': 'ultrafast',
'tune': 'zerolatency',
'g': str(self.fps)
}
self.logger.info("⚡ 极致低延迟转码推流内核就绪")
try:
frame = self.frame_queue.get(timeout=1.0)
except queue.Empty:
continue
# 快速色彩空间转换 (BGR -> RGB)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
av_frame = av.VideoFrame.from_ndarray(frame_rgb, format='rgb24')
av_frame.pts = pts
pts += int(90000 / self.fps)
for packet in stream.encode(av_frame):
output_container.mux(packet)
except Exception as e:
output_container = None
time.sleep(1.0)
def stop(self):
self.is_running = False
if self.encoder_thread:
self.encoder_thread.join(timeout=1.0)
# =====================================================================
# 3. 核心全局配置与单套接字全速收包
# =====================================================================
PORT = 9999
RECORD_FPS = 24.0
frame_interval = 1.0 / RECORD_FPS
latest_frame = None
frame_lock = threading.Lock()
is_running = True
def udp_receiver_thread():
global latest_frame, is_running
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 扩大内核网络接收缓冲区,防止超高频数据包在高负载下被系统丢弃
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024)
sock.bind(('0.0.0.0', PORT))
while is_running:
try:
packet, _ = sock.recvfrom(65535)
data = np.frombuffer(packet, dtype=np.uint8)
frame = cv2.imdecode(data, cv2.IMREAD_COLOR)
if frame is not None:
with frame_lock:
latest_frame = frame
except Exception:
time.sleep(0.005)
sock.close()
def main():
global latest_frame, is_running
# 启动全速收包
recv_t = threading.Thread(target=udp_receiver_thread, daemon=True)
recv_t.start()
# 启动极速中继转码
proxy = JPEG2H264Streamer(qgc_ip="127.0.0.1", qgc_port=5600, width=640, height=480, fps=int(RECORD_FPS))
proxy.start()
last_frame_recorded = None
print("🚀 纯净版图传中继代理网关已在后台全速运行...(按 Ctrl+C 退出)")
try:
while is_running:
loop_start = time.time()
current_frame = None
with frame_lock:
if latest_frame is not None:
current_frame = latest_frame
latest_frame = None # 取出即置空,拒绝重复读取
# 无线丢包平滑补偿核心(保留此逻辑可以大幅减少 QGC 绿屏频率)
if current_frame is not None:
last_frame_recorded = current_frame
elif last_frame_recorded is not None:
current_frame = last_frame_recorded
if current_frame is not None:
# 内存直推,直接进入 FFmpeg 7.1 零延迟通道
proxy.push_frame(current_frame)
# 移除所有窗口渲染和 waitKey 阻塞,实行严格的高精时间步长控制
time_used = time.time() - loop_start
sleep_time = frame_interval - time_used
if sleep_time > 0:
time.sleep(sleep_time)
except KeyboardInterrupt:
print("\n👋 正在安全关闭网关...")
finally:
is_running = False
proxy.stop()
print("✅ 网关服务已安全退出。")
if __name__ == "__main__":
main()