更新摄像头时Popen调用ffmpeg进程ID未更新,无法终止进程
问题描述
我用Python+Django开发视频管理系统,用来展示摄像头直播流和实现录像功能,新增摄像头的POST请求流程正常。但更新摄像头信息(比如IP、密码)时,我会先删除旧摄像头实例,再创建同名新实例。这时候发现start()能拿到新的ffmpeg进程ID,但terminate_process()还是用旧的进程ID,导致没法正确终止旧进程。试了多种终止子进程的方法都没用,怀疑是用del只删了对象引用,没真正销毁对象。
核心问题分析
- 更新逻辑是先删旧实例再建新实例,但旧实例的进程没被提前终止,且
del只是减少对象引用计数,Python垃圾回收不会自动帮你终止子进程 - 额外存储的
p1_id、p2_id容易和实际Popen对象脱节,导致调用终止时用了旧ID - 原
terminate_process()方法没有检查进程状态,重复操作或无效操作多
修复方案
1. 先终止旧进程,再删除实例
更新摄像头时,先调用旧实例的terminate_process(),再删除引用,不要直接删实例。
2. 直接用Popen对象操作,别存冗余ID
完全不需要单独存储进程ID,直接通过self.p1、self.p2这两个Popen对象来终止进程,避免ID和对象不一致的问题。
3. 优化终止方法,增加状态检查和兜底
给terminate_process()加进程存活判断、超时处理,避免僵尸进程,同时给类加析构方法兜底。
修改后的代码
import subprocess from subprocess import Popen import os, sys import threading import time from datetime import datetime import pytz import signal """This class is used to create camera objects to display live stream and recordings, it is also used to manage recordings files by deleting them at the given time""" class CameraStream: def __init__(self, device_name, username, password, ip_address, storage_days): self.cam_name = device_name self.username = username self.password = password self.cam_ip = ip_address self.storage_days = storage_days self.p1 = None # 直播流进程对象 self.p2 = None # 录像进程对象 self.recordings_list = [] # 简化目录创建逻辑 folder_name = f"videos/Recordings/{self.cam_name}" folder_name2 = f"videos/LiveStreams/{self.cam_name}" os.makedirs(folder_name, exist_ok=True) os.makedirs(folder_name2, exist_ok=True) self.directory = folder_name # 设置守护线程,主线程退出时自动结束 t = threading.Thread(target=self.maintain_recordings, args=(), daemon=True) t.start() # 构建ffmpeg命令 self.live_stream = ( f"ffmpeg -fflags nobuffer -rtsp_transport tcp -i rtsp://{self.username}:{self.password}@{self.cam_ip}:554/stream1 " f"-copyts -vcodec copy -acodec copy -hls_flags delete_segments+append_list -f hls -hls_time 6 " f"-hls_list_size 5 -hls_segment_type mpegts -hls_segment_filename videos/LiveStreams/{self.cam_name}/%d.ts " f"videos/LiveStreams/{self.cam_name}/index.m3u8" ).split(" ") self.recording = ( f"ffmpeg -use_wallclock_as_timestamps 1 -rtsp_transport tcp -i rtsp://{self.username}:{self.password}@{self.cam_ip}:554/stream1 " f"-vcodec copy -acodec copy -f segment -reset_timestamps 1 -segment_time 1800 -segment_format mp4 " f"-segment_atclocktime 1 -strftime 1 videos/Recordings/{self.cam_name}/%Y%m%dT%H%M%S.mp4" ).split(" ") self.start() def start(self): # 启动新进程前,先终止已有进程 if self.p1 is not None or self.p2 is not None: self.terminate_process() self.p1 = Popen(self.live_stream) self.p2 = Popen(self.recording) # 直接通过对象获取PID,不需要sleep print("In start func: ", self.p1.pid, self.p2.pid) def terminate_process(self): print("开始终止摄像头进程...") # 封装通用的进程终止逻辑 def kill_proc(proc, proc_label): if proc is None: return # 检查进程是否还在运行 if proc.poll() is None: try: proc.terminate() # 等待进程退出,避免僵尸进程 proc.wait(timeout=5) print(f"{proc_label} 已正常终止") except subprocess.TimeoutExpired: proc.kill() proc.wait() print(f"{proc_label} 超时,已强制终止") except Exception as e: print(f"终止{proc_label}失败: {str(e)}") else: print(f"{proc_label} 已停止") kill_proc(self.p1, "直播流进程") kill_proc(self.p2, "录像进程") # 重置进程对象 self.p1 = None self.p2 = None def __del__(self): # 兜底析构方法,确保对象被回收时终止进程 self.terminate_process() # 保留原录像维护逻辑 def maintain_recordings(self): # 原方法代码... pass
关键修改点
- 移除冗余的
p1_id、p2_id,直接用Popen对象操作 start()方法增加前置进程检查,避免重复启动terminate_process()增加进程状态判断、超时处理,防止僵尸进程- 线程设为守护线程,避免主线程退出后残留后台线程
- 用
os.makedirs(..., exist_ok=True)简化目录创建 - 增加
__del__兜底方法,确保对象销毁时终止进程
内容的提问来源于stack exchange,提问作者Bond_007
相关产品推荐
相关产品推荐

