You need to enable JavaScript to run this app. PyQt5 按钮触发while循环运行虚拟相机时GUI崩溃问题求助
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5 按钮触发while循环运行虚拟相机时GUI崩溃问题求助

崩溃根因

PyQt5的界面渲染、鼠标交互、系统事件处理完全依赖主线程的事件循环机制,直接在按钮绑定的槽函数中运行无限循环,会完全占住主线程的计算资源,导致系统无法接收界面重绘、交互点击的系统消息,最终表现为GUI无响应、系统判定程序崩溃。
原代码同时存在多处逻辑缺陷,会进一步加剧异常:

  • 推流循环内每读一帧就重复调用front_page()、重设布局、重复调用super().__init__(),会导致Qt布局树被重复构建,触发内存泄漏甚至直接崩溃
  • 本地视频读取对象loop_cap没有提前初始化,首次进入推流循环时会因变量未定义抛出异常
  • 没有推流的关闭逻辑,启动后无法正常停止、释放摄像头资源
  • 每帧轮询热键的检测方式效率极低,容易出现热键触发延迟
  • 视频读取没有做空返回判断,读不到帧的时候会直接触发cv2的运行时错误
修正实现思路

推流逻辑完全从主线程剥离

所有持续读帧、推流的循环逻辑放到独立的QThread子线程中运行,主线程只负责处理UI交互、界面绘制,从根本上解决循环阻塞主线程的问题。

清理冗余的UI操作

UI控件的初始化、布局绑定只需要在窗口构造阶段执行一次,推流循环内绝对不能调用会触发界面重绘、布局重构的方法。

补全资源管理与异常逻辑

  • 提前初始化所有摄像头、视频文件的读取对象,避免变量未定义的异常
  • 本地视频读取到文件末尾时,自动回退到视频开头循环播放,避免读空帧报错
  • 改用keyboard.add_hotkey绑定热键,替代每帧轮询的低效率检测方式
  • 增加推流状态标记,支持再次点击On/Off按钮关闭推流、释放所有摄像头资源

线程安全控制

子线程的运行状态通过标记位控制,退出时等待子线程完全释放资源后再重置状态,避免跨线程操作的崩溃。

修正后可运行的参考代码
import PyQt5.QtWidgets as qtw
from PyQt5.QtCore import QThread
import pyvirtualcam
import cv2
import keyboard


class VirtualCamStreamThread(QThread):
    # 独立推流子线程,所有阻塞逻辑都在此运行
    def __init__(self, main_window):
        super().__init__()
        self.main_window = main_window
        self.is_running = True
        self.local_video_cap = None
        self.webcam_cap = main_window.cap
        self.current_frame_source = 0  # 0=本地视频,1=物理摄像头
        # 绑定热键实现视频源切换
        keyboard.add_hotkey(self.main_window.HOTKEY, self.switch_source)

    def switch_source(self):
        # 热键触发的视频源切换逻辑
        self.current_frame_source = 1 - self.current_frame_source
        # 切回本地视频时自动加载已选择的本地文件
        if self.current_frame_source == 0 and self.main_window.path[0]:
            self.local_video_cap = cv2.VideoCapture(self.main_window.path[0])

    def run(self):
        while self.is_running:
            with pyvirtualcam.Camera(
                width=self.main_window.WIDTH,
                height=self.main_window.HEIGHT,
                fps=self.main_window.FPS,
                fmt=self.main_window.FMT
            ) as cam:
                print(f'Using virtual camera: {cam.device}')
                while self.is_running:
                    # 根据当前源读取视频帧
                    if self.current_frame_source == 0:
                        if not self.local_video_cap:
                            cam.sleep_until_next_frame()
                            continue
                        ret, frame = self.local_video_cap.read()
                        # 本地视频播完自动循环
                        if not ret:
                            self.local_video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
                            continue
                    else:
                        ret, frame = self.webcam_cap.read()
                        if not ret:
                            continue
                    # 统一缩放到虚拟相机要求的分辨率
                    frame = cv2.resize(
                        frame,
                        (self.main_window.WIDTH, self.main_window.HEIGHT),
                        interpolation=cv2.INTER_AREA
                    )
                    cam.send(frame)
                    cam.sleep_until_next_frame()
            # 退出循环后释放本地视频资源
            if self.local_video_cap:
                self.local_video_cap.release()


class MainWindow(qtw.QWidget):
    def __init__(self):
        super().__init__()
        # 初始化全局参数
        self.path = ""
        self.WIDTH = 1920
        self.HEIGHT = 1080
        self.FPS = 30
        self.HOTKEY = "alt+shift+;"
        self.cap = cv2.VideoCapture(0)
        self.FMT = pyvirtualcam.PixelFormat.BGR
        self.stream_thread = None

        # 初始化UI控件
        self.btn_off_on = qtw.QPushButton("On/Off")
        self.btn_path = qtw.QPushButton("Get Video")

        # 窗口基础配置
        self.setWindowTitle("Matthew's Cute")
        self.setLayout(qtw.QVBoxLayout())
        self.front_page()
        self.show()

    def off_on(self):
        # 按钮点击仅控制推流线程启停,不运行阻塞循环
        if not self.stream_thread:
            self.stream_thread = VirtualCamStreamThread(self)
            self.stream_thread.finished.connect(self.on_stream_stop)
            self.stream_thread.start()
        else:
            # 停止推流并等待资源释放
            self.stream_thread.is_running = False
            self.stream_thread.wait()
            self.stream_thread = None

    def on_stream_stop(self):
        # 推流停止后的资源清理
        self.stream_thread = None
        keyboard.remove_hotkey(self.HOTKEY)

    def get_path(self):
        self.path = qtw.QFileDialog.getOpenFileName()
        print(self.path[0])

    def front_page(self):
        # UI初始化仅执行一次,不在推流循环中重复调用
        container = qtw.QWidget()
        container.setLayout(qtw.QGridLayout())
        container.layout().addWidget(self.btn_off_on, 0, 0, 1, 2)
        self.btn_off_on.clicked.connect(self.off_on)
        container.layout().addWidget(self.btn_path, 2, 0, 1, 2)
        self.btn_path.clicked.connect(self.get_path)
        self.layout().addWidget(container)


if __name__ == '__main__':
    app = qtw.QApplication([])
    mw = MainWindow()
    app.setStyle(qtw.QStyleFactory.create('Fusion'))
    app.exec_()

内容的提问来源于stack exchange,提问作者asd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 18:45:04