You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python OpenCV FFmpeg多线程音视频录制:按键停止异常排查

问题分析与解决方案

看起来你遇到的问题根源有两个核心点:

1. OpenCV GUI操作放在子线程中违反规范

OpenCV的cv2.imshow()和cv2.waitKey()必须在主线程中运行,你现在把这些操作放在了VideoRecorder.record()的子线程里,这会导致GUI事件处理异常,尤其是当主线程被其他循环阻塞时,视频录制线程会卡住,无法响应停止指令。

2. 按键监听的循环阻塞了主线程

你的keyboard_pressed()函数用while True循环不断检查按键,这种轮询方式会完全占用主线程的CPU资源,导致OpenCV的GUI事件无法被处理,进而让视频录制线程的循环无法正常退出——即使你调用了stop()设置open=False,子线程也因为cv2.waitKey(1)卡住而无法终止,最终卡在while threading.active_count() > 1的等待循环里,后续的stop_AVrecording()和file_manager()自然无法执行。


修正后的代码方案

下面是调整后的完整代码,我会标注关键修改点:

from __future__ import print_function, division
import numpy as np
import cv2
import pyaudio
import wave
import threading
import time
import subprocess
import os
import keyboard

class VideoRecorder:
    """Video class based on openCV"""
    def __init__(self, name="temp_video.avi", fourcc="MJPG", sizex=640, sizey=480, fps=30):
        self.open = True
        self.fps = fps
        self.fourcc = fourcc
        self.frameSize = (sizex, sizey)
        self.video_filename = name
        self.video_cap = cv2.VideoCapture(1, cv2.CAP_DSHOW)
        self.video_writer = cv2.VideoWriter_fourcc(*self.fourcc)
        self.video_out = cv2.VideoWriter(self.video_filename, self.video_writer, self.fps, self.frameSize)
        self.frame_counts = 1
        self.start_time = time.time()
        # 新增:用于传递帧的队列,子线程读帧,主线程显示
        self.frame_queue = []
        self.lock = threading.Lock()

    def record(self):
        """Video starts being recorded (仅负责读帧和写文件,不处理GUI)"""
        counter = 1
        timer_start = time.time()
        timer_current = 0
        while self.open:
            ret, video_frame = self.video_cap.read()
            if ret:
                self.video_out.write(video_frame)
                # 用锁保护队列,避免线程冲突
                with self.lock:
                    self.frame_queue.append(video_frame)
                self.frame_counts += 1
                counter += 1
                timer_current = time.time() - timer_start
                # 控制帧率,避免占用过多资源
                time.sleep(1/self.fps)
            else:
                break

    def stop(self):
        """Finishes the video recording therefore the thread too"""
        if self.open:
            self.open=False
            self.video_out.release()
            self.video_cap.release()

    def start(self):
        """Launches the video recording function using a thread"""
        video_thread = threading.Thread(target=self.record)
        video_thread.start()

class AudioRecorder():
    """Audio class based on pyAudio and Wave"""
    def __init__(self, filename="temp_audio.wav", rate=44100, fpb=1024, channels=2):
        self.open = True
        self.rate = rate
        self.frames_per_buffer = fpb
        self.channels = channels
        self.format = pyaudio.paInt16
        self.audio_filename = filename
        self.audio = pyaudio.PyAudio()
        self.stream = self.audio.open(format=self.format, channels=self.channels, rate=self.rate, input=True, frames_per_buffer = self.frames_per_buffer)
        self.audio_frames = []

    def record(self):
        """Audio starts being recorded"""
        self.stream.start_stream()
        while self.open:
            try:
                data = self.stream.read(self.frames_per_buffer, exception_on_overflow=False)
                self.audio_frames.append(data)
            except:
                break
            if not self.open:
                break

    def stop(self):
        """Finishes the audio recording therefore the thread too"""
        if self.open:
            self.open = False
            self.stream.stop_stream()
            self.stream.close()
            self.audio.terminate()
            waveFile = wave.open(self.audio_filename, 'wb')
            waveFile.setnchannels(self.channels)
            waveFile.setsampwidth(self.audio.get_sample_size(self.format))
            waveFile.setframerate(self.rate)
            waveFile.writeframes(b''.join(self.audio_frames))
            waveFile.close()

    def start(self):
        """Launches the audio recording function using a thread"""
        audio_thread = threading.Thread(target=self.record)
        audio_thread.start()

def start_AVrecording(filename="test"):
    global video_thread
    global audio_thread
    video_thread = VideoRecorder()
    audio_thread = AudioRecorder()
    audio_thread.start()
    video_thread.start()
    return filename

def stop_AVrecording(filename="test"):
    audio_thread.stop()
    frame_counts = video_thread.frame_counts
    elapsed_time = time.time() - video_thread.start_time
    recorded_fps = frame_counts / elapsed_time
    print("total frames " + str(frame_counts))
    print("elapsed time " + str(elapsed_time))
    print("recorded fps " + str(recorded_fps))
    video_thread.stop()
    # 等待线程结束,增加超时避免无限等待
    wait_time = 0
    while threading.active_count() > 1 and wait_time < 5:
        time.sleep(0.5)
        wait_time += 0.5
    # 关闭所有OpenCV窗口(移到主线程执行)
    cv2.destroyAllWindows()
    # Merging audio and video signal
    if abs(recorded_fps - 6) >= 0.01:
        print("Re-encoding")
        cmd = "ffmpeg -r " + str(recorded_fps) + " -i temp_video.avi -pix_fmt yuv420p -r 6 temp_video2.avi"
        subprocess.call(cmd, shell=True)
        print("Muxing")
        cmd = "ffmpeg -y -ac 2 -channel_layout stereo -i temp_audio.wav -i temp_video2.avi -pix_fmt yuv420p " + filename + ".avi"
        subprocess.call(cmd, shell=True)
    else:
        print("Normal recording\nMuxing")
        cmd = "ffmpeg -y -ac 2 -channel_layout stereo -i temp_audio.wav -i temp_video.avi -pix_fmt yuv420p " + filename + ".avi"
        subprocess.call(cmd, shell=True)
    print("..")

def file_manager(filename="test"):
    """Required and wanted processing of final files"""
    local_path = os.getcwd()
    temp_files = ["temp_audio.wav", "temp_video.avi", "temp_video2.avi"]
    for file in temp_files:
        file_path = os.path.join(local_path, file)
        if os.path.exists(file_path):
            os.remove(file_path)

# 新增:用回调函数处理按键,避免阻塞主线程
stop_flag = False
def on_key_press(event):
    global stop_flag
    if event.name == 'q':
        print('------------You Pressed Q Key!--------------')
        stop_flag = True

if __name__ == '__main__':
    # 注册按键回调
    keyboard.on_press(on_key_press)
    start_AVrecording()
    
    # 主线程负责显示视频帧和处理GUI事件
    while not stop_flag:
        # 从队列取帧显示
        with video_thread.lock:
            if video_thread.frame_queue:
                frame = video_thread.frame_queue.pop(0)
                cv2.imshow('video_frame', frame)
        # 必须在主线程调用waitKey,处理GUI事件
        if cv2.waitKey(1) & 0xFF == ord('q'):
            stop_flag = True
        # 控制循环频率
        time.sleep(1/30)
    
    print('-------------Stop AVrecording-------------')
    stop_AVrecording()
    print('-------------File Manager-----------------')
    file_manager()
    print('-----------------End----------------------')

关键修改说明

  1. 重构VideoRecorder类:

    • 新增队列frame_queue和锁lock,子线程只负责读取视频帧并写入文件,同时把帧放入队列。
    • 把cv2.imshow()和cv2.waitKey()移到主线程的循环中执行,符合OpenCV的GUI规范。
  2. 优化按键监听方式:

    • 用keyboard.on_press()注册回调函数,替代轮询式的while True循环,不会阻塞主线程。
    • 新增stop_flag变量,用于控制主线程的视频显示循环和整个录制流程。
  3. 线程停止逻辑优化:

    • 在stop_AVrecording()中增加等待超时,避免因为子线程异常导致无限等待。
    • 把cv2.destroyAllWindows()移到主线程执行,确保GUI资源正确释放。
  4. 音频录制健壮性提升:

    • 在AudioRecorder.record()中增加异常捕获,避免音频流溢出导致崩溃。

这样修改后,无论是用按键停止还是自动停止,都能确保录制线程正常终止,后续的合并和文件清理操作也能正确执行了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 07:38:20