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

MacOS Sonoma下Python+PyQt6录屏时长不匹配问题求助

屏幕录制时长与实际视频时长不同步的问题分析与解决

问题描述

在MacOS Sonoma操作系统中,使用Python、PyQt6和QtWebEngineWidgets实现屏幕录制功能时出现异常:设置20秒录制时长,生成的视频却不足1秒。测试显示20秒仅录制了32帧,尝试强制设置每秒20或24帧也无法解决问题。

相关代码

父脚本启停录制代码

def start_recording(self):
    applicazione_log(f"BROWSER: Start recording") 
    self.record_button.setText("STOP RECORDING AND SAVE ACQUISITION")
    self.recording = True
    self.recording_task = VideoRecorder(self.unique_code)
    self.recording_thread = RecordingThread(self.recording_task)
    self.recording_thread.start()

def stop_recording(self):
    applicazione_log(f"BROWSER: Stop recording") 
    self.recording = False
    self.record_button.setText("START RECORDING")
    if self.recording_task:
        self.recording_task.stop()
        self.recording_thread.quit()
        self.recording_thread.wait()

录屏脚本代码

import os
import ffmpeg
import numpy as np
import cv2
from mss import mss
import pyautogui
from PyQt6.QtCore import *
from PyQt6.QtWidgets import *
from PyQt6.QtGui import *
import time

from .monitoraggio_applicazione import applicazione_log

class VideoRecorder(QObject):
    frameAcquired = pyqtSignal(int)

    def __init__(self, unique_code):
        super().__init__()
        self.running = True
        self.last_frame_time = 0
        self.frame_count = 0
        self.width, self.height = pyautogui.size()

        applicazione_log(f"pyautogui: width = {str(self.width)} width = {str(self.height)}")

        self.script_directory = os.path.dirname(os.path.abspath(__file__))
        self.parent_directory = os.path.dirname(self.script_directory)
        self.folder_documents = 'documents'
        self.folder_documents_path = os.path.join(self.parent_directory, self.folder_documents, unique_code)

        self.filename = os.path.join(self.folder_documents_path, "recorded_video.mp4")

        # 帧缓冲
        self.buffer_size = 30
        self.buffer = []
        
        self.frame_rate = 24
        self.frame_duration = 1 / self.frame_rate

        try:
            self.video_stream = (
                ffmpeg.input('pipe:', format='rawvideo', pix_fmt='bgr24', s=f'{self.width}x{self.height}', r=self.frame_rate)
                .output(self.filename, pix_fmt='yuv420p', vcodec='libx264', crf=20)
                .run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True)
            )

        except Exception as e:
            applicazione_log(f"Errore durante la configurazione di ffmpeg: {str(e)}")

    def stop(self):
        try:
            self.running = False
            # 写入缓冲中剩余的所有帧
            while self.buffer:
                frame = self.buffer.pop(0)
                self.video_stream.stdin.write(frame.tobytes('C'))
            self.video_stream.stdin.close()
            self.video_stream.wait()  
        except Exception as e:
            if "write to closed file" in str(e):
                applicazione_log(f"Errore di scrittura su file chiuso: {str(e)}")
            else:
                raise e

    @pyqtSlot()
    def run(self):
        with mss() as sct:
            # 获取主显示器区域
            monitor = sct.monitors[1]
            self.last_frame_time = time.perf_counter()
            while self.running:
                try:
                    # 内存直接获取截图,跳过磁盘IO
                    screenshot = sct.grab(monitor)
                    # 转换为cv2兼容的BGR格式
                    frame = np.array(screenshot)
                    frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)

                    # 严格控制帧率间隔
                    current_time = time.perf_counter()
                    elapsed_time = current_time - self.last_frame_time
                    if elapsed_time < self.frame_duration:
                        time.sleep(self.frame_duration - elapsed_time)
                    self.last_frame_time = time.perf_counter()

                    # 帧缓冲处理
                    self.buffer.append(frame)
                    if len(self.buffer) > self.buffer_size:
                        self.buffer.pop(0)

                    # 写入单帧到ffmpeg
                    if self.buffer:
                        frame_to_write = self.buffer.pop(0)
                        self.video_stream.stdin.write(frame_to_write.tobytes('C'))
                        self.frame_count += 1

                except Exception as e:
                    applicazione_log(f"Errore durante l'acquisizione e l'elaborazione dei frame: {str(e)}")

        # 收尾时清空缓冲
        while self.buffer:
            frame_to_write = self.buffer.pop(0)
            self.video_stream.stdin.write(frame_to_write.tobytes('C'))
        self.video_stream.stdin.close()
        self.video_stream.wait()
        applicazione_log(f"录制完成,总帧数: {self.frame_count}")

class RecordingThread(QThread):
    def __init__(self, video_recorder):
        super().__init__()
        self.video_recorder = video_recorder

    def run(self):
        self.video_recorder.run()

问题根源

  1. 磁盘IO拖慢采集速度:原代码用sct.shot()将截图保存为PNG文件,再通过cv2.imread()读取,磁盘读写的高耗时导致每秒仅能采集1-2帧,20秒仅生成32帧左右。而ffmpeg按24fps封装视频,32帧对应时长仅约1.3秒,因此出现时长不符。
  2. 无帧率控制逻辑:原循环未控制帧采集间隔,完全依赖硬件和IO速度,实际帧率远低于设定值。
  3. 缓冲逻辑无效:原代码每次添加帧到缓冲后立刻弹出写入,缓冲未起到平滑帧率的作用,反而增加冗余操作。

解决措施

  1. 内存直连采集:改用sct.grab()直接获取内存中的截图,再转换为cv2格式,彻底消除磁盘IO开销,大幅提升采集速度。
  2. 强制帧率控制:通过time.perf_counter()计算帧间隔,用time.sleep()确保每帧的采集/写入间隔严格匹配设定帧率(24fps),保证实际帧率与预期一致。
  3. 优化缓冲与收尾:停止录制时先写入缓冲剩余帧,避免数据丢失;调整缓冲逻辑实现帧率平滑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 20:05:59