2025-12-11 13:41:07 +08:00
|
|
|
|
# task_stream_manager_windows.py
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import os
|
|
|
|
|
|
import psutil
|
|
|
|
|
|
from log import logger
|
|
|
|
|
|
from ffmpegStreamer import FFmpegStreamer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WindowsTaskStreamManager:
|
|
|
|
|
|
"""Windows系统专用任务推流管理器"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.task_streams = {}
|
|
|
|
|
|
self.lock = threading.RLock()
|
|
|
|
|
|
self.health_check_thread = None
|
|
|
|
|
|
self.running = True
|
|
|
|
|
|
self.streaming_check_interval = 3
|
|
|
|
|
|
self.ffmpeg_timeout = 20
|
|
|
|
|
|
|
|
|
|
|
|
# Windows特定配置
|
|
|
|
|
|
self.windows_ffmpeg_path = self._find_windows_ffmpeg()
|
|
|
|
|
|
self.windows_ffmpeg_args = [
|
|
|
|
|
|
'-loglevel', 'verbose',
|
|
|
|
|
|
'-hide_banner',
|
|
|
|
|
|
'-f', 'rawvideo',
|
|
|
|
|
|
'-vcodec', 'rawvideo',
|
|
|
|
|
|
'-pix_fmt', 'bgr24',
|
|
|
|
|
|
'-s', '{}x{}',
|
|
|
|
|
|
'-r', '{}',
|
|
|
|
|
|
'-i', '-',
|
|
|
|
|
|
'-c:v', 'libx264',
|
|
|
|
|
|
'-preset', 'ultrafast',
|
|
|
|
|
|
'-tune', 'zerolatency',
|
|
|
|
|
|
'-f', 'flv',
|
|
|
|
|
|
'-g', '10',
|
|
|
|
|
|
'-bf', '0',
|
|
|
|
|
|
'-max_delay', '0',
|
|
|
|
|
|
'-flags', '+global_header',
|
|
|
|
|
|
'-rtbufsize', '100M',
|
|
|
|
|
|
'-b:v', '2000k',
|
|
|
|
|
|
'-bufsize', '2000k',
|
|
|
|
|
|
'{}'
|
|
|
|
|
|
]
|
|
|
|
|
|
self.last_output_time = 0
|
|
|
|
|
|
self.last_restart_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
def _find_windows_ffmpeg(self):
|
|
|
|
|
|
"""查找Windows上的ffmpeg路径"""
|
|
|
|
|
|
# 常见ffmpeg安装位置
|
|
|
|
|
|
possible_paths = [
|
|
|
|
|
|
r'C:\ffmpeg\bin\ffmpeg.exe',
|
|
|
|
|
|
r'C:\Program Files\ffmpeg\bin\ffmpeg.exe',
|
|
|
|
|
|
r'C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe',
|
|
|
|
|
|
os.path.join(os.getcwd(), 'ffmpeg.exe'),
|
|
|
|
|
|
'ffmpeg' # 如果在PATH中
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for path in possible_paths:
|
|
|
|
|
|
if os.path.exists(path) or os.system(f'where {path} >nul 2>nul') == 0:
|
|
|
|
|
|
logger.info(f"找到FFmpeg: {path}")
|
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
|
|
logger.warning("未找到FFmpeg,将使用系统PATH中的ffmpeg")
|
|
|
|
|
|
return 'ffmpeg'
|
|
|
|
|
|
|
|
|
|
|
|
def create_streamer_for_task(self, task_id, config, fps, width, height):
|
|
|
|
|
|
"""为任务创建Windows专用的推流器"""
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
if task_id in self.task_streams:
|
|
|
|
|
|
logger.warning(f"任务 {task_id} 已有推流器,先清理")
|
|
|
|
|
|
self.stop_task_streamer(task_id)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 创建Windows优化配置
|
|
|
|
|
|
win_config = self._create_windows_config(config, width, height, fps)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建推流器
|
|
|
|
|
|
streamer = FFmpegStreamer(win_config, fps, width, height)
|
|
|
|
|
|
streamer.task_id = task_id
|
|
|
|
|
|
|
|
|
|
|
|
# 覆盖build_ffmpeg_command方法为Windows专用版本
|
|
|
|
|
|
streamer.build_ffmpeg_command = self._build_windows_ffmpeg_command(
|
|
|
|
|
|
streamer, win_config, width, height, fps
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 存储任务信息
|
|
|
|
|
|
self.task_streams[task_id] = {
|
|
|
|
|
|
'streamer': streamer,
|
|
|
|
|
|
'config': win_config,
|
|
|
|
|
|
'fps': fps,
|
|
|
|
|
|
'width': width,
|
|
|
|
|
|
'height': height,
|
|
|
|
|
|
'created_at': time.time(),
|
|
|
|
|
|
'last_active': time.time(),
|
|
|
|
|
|
'frame_count': 0,
|
|
|
|
|
|
'status': 'initializing',
|
|
|
|
|
|
'last_ffmpeg_output': '',
|
|
|
|
|
|
'output_lines': []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 启动推流
|
|
|
|
|
|
streamer.start()
|
|
|
|
|
|
|
|
|
|
|
|
# Windows上需要额外时间启动
|
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
if streamer.running and streamer.process:
|
|
|
|
|
|
self.task_streams[task_id]['status'] = 'running'
|
|
|
|
|
|
logger.info(f"Windows任务 {task_id} 推流器创建成功")
|
|
|
|
|
|
|
|
|
|
|
|
# 启动输出监控线程
|
|
|
|
|
|
self._start_output_monitor(task_id)
|
|
|
|
|
|
|
|
|
|
|
|
return streamer
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"Windows任务 {task_id} 推流器启动失败")
|
|
|
|
|
|
self.stop_task_streamer(task_id)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Windows创建任务推流器失败 {task_id}: {str(e)}", exc_info=True)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _create_windows_config(self, config, width, height, fps):
|
|
|
|
|
|
"""创建Windows专用的推流配置"""
|
|
|
|
|
|
win_config = config.copy()
|
|
|
|
|
|
push_config = config.get('push', {}).copy()
|
|
|
|
|
|
|
|
|
|
|
|
# Windows优化参数
|
|
|
|
|
|
push_config.update({
|
|
|
|
|
|
'video_codec': 'libx264', # Windows上软件编码更稳定
|
|
|
|
|
|
'gpu_acceleration': False, # Windows上硬件加速问题多
|
|
|
|
|
|
'preset': 'ultrafast',
|
|
|
|
|
|
'tune': 'zerolatency',
|
|
|
|
|
|
'pixel_format': 'bgr24',
|
|
|
|
|
|
'format': 'flv',
|
|
|
|
|
|
'crf': 23,
|
|
|
|
|
|
'bitrate': '2000k',
|
|
|
|
|
|
'bufsize': '2000k',
|
|
|
|
|
|
'framerate': fps,
|
|
|
|
|
|
'extra_args': [
|
|
|
|
|
|
'-max_delay', '0',
|
|
|
|
|
|
'-flags', '+global_header',
|
|
|
|
|
|
'-rtbufsize', '100M',
|
|
|
|
|
|
'-g', '10',
|
|
|
|
|
|
'-bf', '0'
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
win_config['push'] = push_config
|
|
|
|
|
|
return win_config
|
|
|
|
|
|
|
|
|
|
|
|
def _build_windows_ffmpeg_command(self, streamer, config, width, height, fps):
|
|
|
|
|
|
"""Windows专用的FFmpeg命令构建"""
|
|
|
|
|
|
|
|
|
|
|
|
def build_command():
|
|
|
|
|
|
# 基础命令
|
|
|
|
|
|
cmd = [
|
|
|
|
|
|
self.windows_ffmpeg_path,
|
|
|
|
|
|
'-y',
|
|
|
|
|
|
'-loglevel', 'verbose', # 详细日志便于调试
|
|
|
|
|
|
'-hide_banner',
|
|
|
|
|
|
'-f', 'rawvideo',
|
|
|
|
|
|
'-vcodec', 'rawvideo',
|
|
|
|
|
|
'-pix_fmt', config['push'].get('pixel_format', 'bgr24'),
|
|
|
|
|
|
'-s', f'{width}x{height}',
|
|
|
|
|
|
'-r', str(fps),
|
|
|
|
|
|
'-i', '-',
|
|
|
|
|
|
'-c:v', config['push']['video_codec'],
|
|
|
|
|
|
'-preset', config['push']['preset'],
|
|
|
|
|
|
'-tune', config['push']['tune'],
|
|
|
|
|
|
'-f', config['push']['format'],
|
|
|
|
|
|
'-g', '10',
|
|
|
|
|
|
'-bf', '0',
|
|
|
|
|
|
'-max_delay', '0',
|
|
|
|
|
|
'-flags', '+global_header',
|
|
|
|
|
|
'-rtbufsize', '100M',
|
|
|
|
|
|
'-b:v', config['push']['bitrate'],
|
|
|
|
|
|
'-bufsize', config['push']['bufsize'],
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 添加额外参数
|
|
|
|
|
|
if 'extra_args' in config['push']:
|
|
|
|
|
|
cmd.extend(config['push']['extra_args'])
|
|
|
|
|
|
|
|
|
|
|
|
# 添加输出URL
|
|
|
|
|
|
cmd.append(config['push']['url'])
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Windows FFmpeg命令: {' '.join(cmd)}")
|
|
|
|
|
|
return cmd
|
|
|
|
|
|
|
|
|
|
|
|
return build_command
|
|
|
|
|
|
|
|
|
|
|
|
def _start_output_monitor(self, task_id):
|
|
|
|
|
|
"""启动FFmpeg输出监控线程"""
|
|
|
|
|
|
|
|
|
|
|
|
def monitor():
|
|
|
|
|
|
task_info = self.task_streams.get(task_id)
|
|
|
|
|
|
if not task_info or not task_info.get('streamer'):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
streamer = task_info['streamer']
|
|
|
|
|
|
while self.running and task_id in self.task_streams:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if streamer.process and streamer.process.stderr:
|
|
|
|
|
|
try:
|
|
|
|
|
|
line = streamer.process.stderr.readline()
|
|
|
|
|
|
if line:
|
|
|
|
|
|
line = line.decode('utf-8', errors='ignore').strip()
|
|
|
|
|
|
if line:
|
|
|
|
|
|
task_info['last_ffmpeg_output'] = line
|
|
|
|
|
|
task_info['output_lines'].append(line)
|
|
|
|
|
|
|
|
|
|
|
|
# # 只保留最近100行
|
|
|
|
|
|
# if len(task_info['output_lines']) > 100:
|
|
|
|
|
|
# task_info['output_lines'].pop(0)
|
|
|
|
|
|
|
|
|
|
|
|
# 解析关键信息
|
|
|
|
|
|
self._parse_ffmpeg_output(task_id, line)
|
|
|
|
|
|
self.last_output_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
# 输出关键错误
|
|
|
|
|
|
if any(keyword in line.lower() for keyword in
|
|
|
|
|
|
['error', 'failed', 'invalid', 'cannot']):
|
|
|
|
|
|
logger.error(f"FFmpeg[{task_id}]: {line}")
|
|
|
|
|
|
elif 'frame=' in line and 'fps=' in line:
|
|
|
|
|
|
logger.debug(f"FFmpeg[{task_id}]: {line}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否长时间无输出
|
|
|
|
|
|
if time.time() - self.last_output_time > self.ffmpeg_timeout:
|
|
|
|
|
|
logger.warning(f"FFmpeg[{task_id}] 长时间无输出,可能已崩溃")
|
|
|
|
|
|
self._safe_restart_streamer(task_id)
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=monitor, daemon=True, name=f"FFmpegMonitor-{task_id}").start()
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_ffmpeg_output(self, task_id, line):
|
|
|
|
|
|
"""解析FFmpeg输出,提取关键信息"""
|
|
|
|
|
|
task_info = self.task_streams.get(task_id)
|
|
|
|
|
|
if not task_info:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 提取帧率信息
|
|
|
|
|
|
if 'fps=' in line:
|
|
|
|
|
|
import re
|
|
|
|
|
|
match = re.search(r'fps=\s*(\d+)', line)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
fps = int(match.group(1))
|
|
|
|
|
|
task_info['actual_fps'] = fps
|
|
|
|
|
|
|
|
|
|
|
|
# 检查关键错误
|
|
|
|
|
|
error_keywords = [
|
|
|
|
|
|
'connection refused',
|
|
|
|
|
|
'cannot open',
|
|
|
|
|
|
'invalid data',
|
|
|
|
|
|
'broken pipe',
|
|
|
|
|
|
'timed out',
|
|
|
|
|
|
'access denied'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for keyword in error_keywords:
|
|
|
|
|
|
if keyword in line.lower():
|
|
|
|
|
|
logger.error(f"FFmpeg[{task_id}] 关键错误: {line}")
|
|
|
|
|
|
self._safe_restart_streamer(task_id)
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_restart_streamer(self, task_id):
|
|
|
|
|
|
"""安全重启推流器"""
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
if task_id not in self.task_streams:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
task_info = self.task_streams[task_id]
|
|
|
|
|
|
|
|
|
|
|
|
# 防止频繁重启
|
|
|
|
|
|
if time.time() - self.last_restart_time < 10:
|
|
|
|
|
|
logger.debug(f"跳过频繁重启: {task_id}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.last_restart_time = time.time()
|
|
|
|
|
|
logger.info(f"安全重启推流器: {task_id}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 停止旧推流器
|
|
|
|
|
|
old_streamer = task_info['streamer']
|
|
|
|
|
|
if old_streamer:
|
|
|
|
|
|
old_streamer.stop()
|
|
|
|
|
|
|
|
|
|
|
|
# 短暂延迟
|
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建新推流器
|
|
|
|
|
|
new_streamer = self.create_streamer_for_task(
|
|
|
|
|
|
task_id,
|
|
|
|
|
|
task_info['config'],
|
|
|
|
|
|
task_info['fps'],
|
|
|
|
|
|
task_info['width'],
|
|
|
|
|
|
task_info['height']
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if new_streamer:
|
|
|
|
|
|
logger.info(f"推流器重启成功: {task_id}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"推流器重启失败: {task_id}")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"重启推流器异常 {task_id}: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
def push_frame(self, task_id, frame):
|
|
|
|
|
|
"""为指定任务推送帧(Windows优化版本)"""
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
task_info = self.task_streams.get(task_id)
|
|
|
|
|
|
if not task_info:
|
|
|
|
|
|
logger.warning(f"任务 {task_id} 没有推流器")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
streamer = task_info['streamer']
|
|
|
|
|
|
if not streamer or not streamer.running:
|
|
|
|
|
|
logger.warning(f"任务 {task_id} 推流器未运行")
|
|
|
|
|
|
self._safe_restart_streamer(task_id)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Windows上需要更严格的检查
|
|
|
|
|
|
# if streamer.process and streamer.process.poll() is not None:
|
|
|
|
|
|
# logger.warning(f"FFmpeg进程已退出: {task_id}")
|
|
|
|
|
|
# self._safe_restart_streamer(task_id)
|
|
|
|
|
|
# return False
|
|
|
|
|
|
|
|
|
|
|
|
# 推流帧
|
|
|
|
|
|
success = streamer.add_frame(frame)
|
|
|
|
|
|
|
|
|
|
|
|
if success:
|
|
|
|
|
|
task_info['frame_count'] += 1
|
|
|
|
|
|
task_info['last_active'] = time.time()
|
|
|
|
|
|
task_info['status'] = 'active'
|
|
|
|
|
|
|
|
|
|
|
|
# 每100帧记录一次
|
|
|
|
|
|
if task_info['frame_count'] % 100 == 0:
|
|
|
|
|
|
logger.info(f"任务 {task_id} 已推流 {task_info['frame_count']} 帧")
|
|
|
|
|
|
else:
|
|
|
|
|
|
task_info['status'] = 'error'
|
|
|
|
|
|
logger.warning(f"任务 {task_id} 推流失败")
|
|
|
|
|
|
|
|
|
|
|
|
# 连续失败处理
|
|
|
|
|
|
fail_count = task_info.get('consecutive_failures', 0) + 1
|
|
|
|
|
|
task_info['consecutive_failures'] = fail_count
|
|
|
|
|
|
|
|
|
|
|
|
if fail_count >= 10:
|
|
|
|
|
|
logger.error(f"任务 {task_id} 连续失败 {fail_count} 次,尝试重启")
|
|
|
|
|
|
self._safe_restart_streamer(task_id)
|
|
|
|
|
|
task_info['consecutive_failures'] = 0
|
|
|
|
|
|
|
|
|
|
|
|
return success
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"推流失败 {task_id}: {str(e)}")
|
|
|
|
|
|
task_info['status'] = 'error'
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def stop_task_streamer(self, task_id):
|
|
|
|
|
|
"""停止任务推流器(Windows专用)"""
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
if task_id in self.task_streams:
|
|
|
|
|
|
try:
|
|
|
|
|
|
task_info = self.task_streams[task_id]
|
|
|
|
|
|
streamer = task_info['streamer']
|
|
|
|
|
|
|
|
|
|
|
|
if streamer:
|
|
|
|
|
|
# Windows上需要强制终止
|
|
|
|
|
|
try:
|
|
|
|
|
|
if streamer.process:
|
|
|
|
|
|
import signal
|
|
|
|
|
|
try:
|
|
|
|
|
|
streamer.process.terminate()
|
|
|
|
|
|
streamer.process.wait(timeout=3)
|
|
|
|
|
|
except:
|
|
|
|
|
|
streamer.process.kill()
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
streamer.stop()
|
|
|
|
|
|
|
|
|
|
|
|
del self.task_streams[task_id]
|
|
|
|
|
|
logger.info(f"Windows任务推流器停止成功: {task_id}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"停止任务推流器失败 {task_id}: {str(e)}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_task_stream_info(self, task_id):
|
|
|
|
|
|
"""获取任务推流信息"""
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
if task_id not in self.task_streams:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
task_info = self.task_streams[task_id]
|
|
|
|
|
|
streamer = task_info['streamer']
|
|
|
|
|
|
|
|
|
|
|
|
info = {
|
|
|
|
|
|
'status': task_info['status'],
|
|
|
|
|
|
'fps': task_info['fps'],
|
|
|
|
|
|
'resolution': f"{task_info['width']}x{task_info['height']}",
|
|
|
|
|
|
'frame_count': task_info['frame_count'],
|
|
|
|
|
|
'last_active': time.time() - task_info['last_active'],
|
|
|
|
|
|
'running': streamer.running if streamer else False,
|
|
|
|
|
|
'process_alive': streamer.process.poll() is None if streamer and streamer.process else False,
|
|
|
|
|
|
'output_lines': task_info['output_lines'][-10:], # 最近10行输出
|
|
|
|
|
|
'last_ffmpeg_output': task_info.get('last_ffmpeg_output', '')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
def get_all_task_streams_info(self):
|
|
|
|
|
|
"""获取所有任务推流器信息"""
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
info = {}
|
|
|
|
|
|
for task_id in self.task_streams:
|
|
|
|
|
|
info[task_id] = self.get_task_stream_info(task_id)
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
def start_health_monitor(self):
|
|
|
|
|
|
"""启动Windows健康监控线程"""
|
|
|
|
|
|
|
|
|
|
|
|
def health_monitor_loop():
|
|
|
|
|
|
logger.info("Windows推流健康监控启动")
|
|
|
|
|
|
while self.running:
|
|
|
|
|
|
try:
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
task_ids = list(self.task_streams.keys())
|
|
|
|
|
|
|
|
|
|
|
|
for task_id in task_ids:
|
|
|
|
|
|
task_info = self.task_streams.get(task_id)
|
|
|
|
|
|
if not task_info:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否长时间无活动
|
|
|
|
|
|
inactive_time = time.time() - task_info['last_active']
|
|
|
|
|
|
if inactive_time > 30 and task_info['frame_count'] > 0:
|
|
|
|
|
|
logger.warning(f"任务 {task_id} 已 {inactive_time:.0f} 秒无活动")
|
|
|
|
|
|
self._safe_restart_streamer(task_id)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查FFmpeg进程状态
|
|
|
|
|
|
streamer = task_info['streamer']
|
|
|
|
|
|
if streamer and hasattr(streamer, 'process'):
|
2025-12-11 15:08:28 +08:00
|
|
|
|
if streamer.process is not None:
|
|
|
|
|
|
if streamer.process.poll() is not None:
|
|
|
|
|
|
logger.warning(f"任务 {task_id} FFmpeg进程已退出")
|
|
|
|
|
|
self._safe_restart_streamer(task_id)
|
2025-12-11 13:41:07 +08:00
|
|
|
|
|
|
|
|
|
|
time.sleep(self.streaming_check_interval)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"健康监控异常: {str(e)}")
|
|
|
|
|
|
time.sleep(10)
|
|
|
|
|
|
|
|
|
|
|
|
self.health_check_thread = threading.Thread(
|
|
|
|
|
|
target=health_monitor_loop,
|
|
|
|
|
|
daemon=True,
|
|
|
|
|
|
name="WindowsTaskStreamHealthMonitor"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.health_check_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def stop_health_monitor(self):
|
|
|
|
|
|
"""停止健康监控"""
|
|
|
|
|
|
self.running = False
|
|
|
|
|
|
if self.health_check_thread and self.health_check_thread.is_alive():
|
|
|
|
|
|
self.health_check_thread.join(3.0)
|
|
|
|
|
|
|
|
|
|
|
|
def cleanup_all(self):
|
|
|
|
|
|
"""清理所有推流器"""
|
|
|
|
|
|
logger.info("清理所有Windows任务推流器")
|
|
|
|
|
|
with self.lock:
|
|
|
|
|
|
task_ids = list(self.task_streams.keys())
|
|
|
|
|
|
for task_id in task_ids:
|
|
|
|
|
|
self.stop_task_streamer(task_id)
|
|
|
|
|
|
self.stop_health_monitor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Windows专用全局推流管理器实例
|
|
|
|
|
|
windows_task_stream_manager = WindowsTaskStreamManager()
|