Files
gitlab-instance-0a899031_do…/stream_stats.py
2026-03-19 03:14:57 -05:00

175 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# stream_stats.py
# 推流状态监控:解析 FFmpeg 输出,统计帧率、码率、网络等,便于调试
"""
推流状态监控模块
解析 FFmpeg stderr 输出,提取:帧率、码率、编码速度、源延迟事件
可选Linux 网络上下行速度
"""
import re
import time
import threading
import platform
class StreamStats:
"""推流状态统计,线程安全"""
def __init__(self, interval_sec=10):
self.interval_sec = interval_sec
self._lock = threading.Lock()
self.frame = 0
self.fps = 0.0
self.bitrate_kbps = 0.0
self.speed = 0.0
self.encode_time = ""
self.size_kb = 0
self.quality_q = 0.0
self.lag_events = 0 # "Resumed reading" 次数,表示源端抖动
self.last_update_ts = 0.0
self.start_ts = time.time()
self._stop = threading.Event()
self._printer_thread = None
# 网络统计 (Linux)
self._net_last = None
self._net_last_ts = 0
self.rx_mbps = 0.0
self.tx_mbps = 0.0
def parse_ffmpeg_progress(self, line: str) -> bool:
"""解析 FFmpeg 进度行,返回是否成功解析"""
# frame= 12345 fps= 30 q=28.0 size= 12345kB time=00:06:51.50 bitrate=2460.0kbits/s speed=1.00x
m = re.search(r'frame=\s*(\d+)', line, re.I)
if not m:
return False
with self._lock:
try:
self.frame = int(m.group(1))
except (ValueError, IndexError):
pass
m2 = re.search(r'fps=\s*([\d.]+)', line, re.I)
if m2:
self.fps = float(m2.group(1))
m3 = re.search(r'bitrate=\s*([\d.]+)\s*kbits/s', line, re.I)
if m3:
self.bitrate_kbps = float(m3.group(1))
m4 = re.search(r'speed=\s*([\d.]+)x', line, re.I)
if m4:
self.speed = float(m4.group(1))
m5 = re.search(r'time=([^\s]+)', line, re.I)
if m5:
self.encode_time = m5.group(1).strip()
m6 = re.search(r'size=\s*(\d+)\s*kB', line, re.I)
if m6:
self.size_kb = int(m6.group(1))
m7 = re.search(r'q=([\d.]+)', line, re.I)
if m7:
self.quality_q = float(m7.group(1))
self.last_update_ts = time.time()
return True
def parse_lag_event(self, line: str) -> bool:
"""检测源端延迟事件 Resumed reading at pts"""
if "resumed reading at pts" in line.lower():
with self._lock:
self.lag_events += 1
return True
return False
def _read_net_bytes(self):
"""读取 /proc/net/dev 总字节数 (Linux)"""
try:
with open("/proc/net/dev", "r") as f:
content = f.read()
except Exception:
return None, None
rx_total = tx_total = 0
for line in content.split("\n")[2:]: # 跳过表头
line = line.strip()
if not line or ":" not in line:
continue
parts = line.split()
if len(parts) >= 10:
try:
rx_total += int(parts[1])
tx_total += int(parts[9])
except (ValueError, IndexError):
pass
return rx_total, tx_total
def update_network(self):
"""更新网络速度 (Linux)"""
if platform.system() != "Linux":
return
rx, tx = self._read_net_bytes()
if rx is None:
return
now = time.time()
with self._lock:
if self._net_last is not None:
dt = now - self._net_last_ts
if dt > 0.5: # 至少 0.5 秒间隔
drx = (rx - self._net_last[0]) / (1024 * 1024) / dt
dtx = (tx - self._net_last[1]) / (1024 * 1024) / dt
self.rx_mbps = round(drx, 2)
self.tx_mbps = round(dtx, 2)
self._net_last = (rx, tx)
self._net_last_ts = now
def format_summary(self) -> str:
"""生成状态摘要字符串"""
self.update_network()
with self._lock:
elapsed = time.time() - self.start_ts
parts = [
f"推流时长 {int(elapsed)}s",
f"帧率 {self.fps:.1f} fps",
f"已推帧 {self.frame}",
f"码率 {self.bitrate_kbps:.0f} kbps",
f"编码速度 {self.speed:.2f}x",
]
if self.lag_events > 0:
parts.append(f"源抖动 {self.lag_events}")
if platform.system() == "Linux":
parts.append(f"下行 {self.rx_mbps:.2f} MB/s 上行 {self.tx_mbps:.2f} MB/s")
return " | ".join(parts)
def start_periodic_printer(self):
"""启动周期性打印线程"""
def _run():
while not self._stop.is_set():
self._stop.wait(self.interval_sec)
if self._stop.is_set():
break
if self.last_update_ts > 0 and (time.time() - self.last_update_ts) < 60:
print(f"[推流状态] {self.format_summary()}")
self._printer_thread = threading.Thread(target=_run, daemon=True)
self._printer_thread.start()
def stop(self):
"""停止周期性打印"""
self._stop.set()
def parse_and_maybe_print(line: str, stats: StreamStats, prefix: str = "[FFmpeg]", verbose=True) -> bool:
"""
解析一行 FFmpeg 输出,更新 stats。
若为进度行则更新不打印;若为 lag 事件则更新并可选打印;其他行原样打印。
verbose: 是否打印非进度行
"""
line_stripped = line.strip() if isinstance(line, str) else line.decode("utf-8", errors="replace").strip()
if not line_stripped:
return False
# 进度行:更新 stats不重复打印由周期摘要代替
if stats.parse_ffmpeg_progress(line_stripped):
return True
# 源端延迟事件
if stats.parse_lag_event(line_stripped):
if verbose:
print(f"{prefix} {line_stripped}")
return True
if verbose:
print(f"{prefix} {line_stripped}")
return False