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----------------------')
关键修改说明
重构VideoRecorder类:
- 新增队列
frame_queue和锁lock,子线程只负责读取视频帧并写入文件,同时把帧放入队列。 - 把
cv2.imshow()和cv2.waitKey()移到主线程的循环中执行,符合OpenCV的GUI规范。
- 新增队列
优化按键监听方式:
- 用
keyboard.on_press()注册回调函数,替代轮询式的while True循环,不会阻塞主线程。 - 新增
stop_flag变量,用于控制主线程的视频显示循环和整个录制流程。
- 用
线程停止逻辑优化:
- 在
stop_AVrecording()中增加等待超时,避免因为子线程异常导致无限等待。 - 把
cv2.destroyAllWindows()移到主线程执行,确保GUI资源正确释放。
- 在
音频录制健壮性提升:
- 在
AudioRecorder.record()中增加异常捕获,避免音频流溢出导致崩溃。
- 在
这样修改后,无论是用按键停止还是自动停止,都能确保录制线程正常终止,后续的合并和文件清理操作也能正确执行了。
内容的提问来源于stack exchange,提问作者scacchi
相关产品推荐
相关产品推荐

