192 lines
6.4 KiB
Python
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() |