用Python实现屏幕录制(含音频)程序源代码

程序概述

本程序是一个功能强大的屏幕录制工具,支持全屏或自定义区域录制,可同时录制系统音频,并提供实时预览、性能监控等功能。为解决长时间录制对内存的占用问题,程序采用流式直接写入文件,不保存到内存,避免内存积累。程序基于Python开发,具有友好的图形用户界面。在Windows下测试成功。源代码1100多行,GUI和内部功能均认真书写和测试,可以直接实用。

程序初始界面:基础界面由AI生成,但作者进行了大量修正、测试和完善。

用Python实现屏幕录制(含音频)程序源代码

录制前可以测试音频设备,只有确定正确的音频设备,才能录制到声音。

用Python实现屏幕录制(含音频)程序源代码

选择屏幕录制区域:用鼠标在屏幕上拉出录屏区域

用Python实现屏幕录制(含音频)程序源代码

正在录制中:实时显示录制情况

用Python实现屏幕录制(含音频)程序源代码

录制完成时:提示相关文件信息,包括视频文件、声音文件和合成后的文件,均放在同一目录中,可以查看或播放。我的电脑性能不太好,fps跟不上,有丢帧现象,但经过智能算法,在合成时会实现声图同步。
用Python实现屏幕录制(含音频)程序源代码

程序功能

主要功能

屏幕录制

支持全屏录制和自定义区域录制

可调节帧率(1-60fps)

多种视频编码器选择(mp4v、XVID、MJPG、DIVX)

实时预览录制画面

音频录制

系统音频录制功能

支持多种音频设备选择

音频设备测试功能

自动音视频同步

录制控制

开始/暂停/停止录制

录制状态实时显示

性能监控(实际FPS、帧数、错误计数)

文件管理

自定义保存路径

自动文件命名

磁盘空间检查

一键打开输出文件夹

高级特性

长时间录制优化(自动分段)

错误恢复机制

健康状态监控

音视频自动同步

程序安装调试注意事项

系统要求

操作系统: Windows 7/8/10/11(推荐Windows 10及以上)

Python版本: Python 3.7及以上

内存: 至少4GB RAM

磁盘空间: 录制期间至少需要2GB可用空间,如果空间不足,会提示是否继续录屏

安装步骤

    需要安装的Python依赖库



pip install opencv-python
pip install pillow
pip install numpy
pip install sounddevice
pip install soundfile

    安装FFmpeg(必需)
    程序依赖FFmpeg进行音视频合并,请参考下文FFmpeg安装说明。

常见问题解决

问题1: 音频录制不可用

解决方案:安装sounddevice库 
pip install sounddevice

检查音频设备是否支持立体声混音

问题2: 录制文件无法播放

解决方案:确保安装了合适的视频解码器

尝试使用不同的编码器(推荐mp4v)

问题3: 录制卡顿

降低帧率设置(建议10-30fps)

关闭其他占用系统资源的程序

选择较小的录制区域

问题4: FFmpeg未找到错误

确保FFmpeg已正确安装并添加到系统PATH

重启程序或命令行窗口

相关库说明及安装方法

核心依赖库

tkinter

说明:Python标准GUI库,用于创建图形界面

安装:Python自带,无需额外安装

opencv-python (cv2)

说明:计算机视觉库,用于视频编码和图像处理

安装:
pip install opencv-python

Pillow (PIL)

说明:图像处理库,用于屏幕截图和图像转换

安装:
pip install pillow

numpy

说明:数值计算库,用于音频数据处理

安装:
pip install numpy

sounddevice

说明:音频录制库,用于捕获系统音频

安装:
pip install sounddevice

soundfile

说明:音频文件处理库

安装:
pip install soundfile

可选库

pycaw: 用于高级音频控制(当前版本未使用)

pyaudio: 替代的音频录制方案

FFmpeg安装方法及程序依赖性

FFmpeg的重要性

本程序依赖FFmpeg进行音视频文件的合并和同步处理,是程序完整功能的关键组件。

安装方法

Windows系统安装

下载FFmpeg

访问 https://ffmpeg.org/download.html

选择Windows版本下载

安装步骤

解压下载的ZIP文件到指定目录(如 
C:ffmpeg

将FFmpeg的bin目录添加到系统PATH环境变量

右键”此电脑” → “属性” → “高级系统设置”

点击”环境变量”

在”系统变量”中找到Path,点击”编辑”

点击”新建”,添加FFmpeg的bin目录路径(如 
C:ffmpegin

验证安装

打开命令提示符,输入:
ffmpeg -version

如果显示版本信息,说明安装成功

程序对FFmpeg的依赖

音视频合并: 将单独录制的视频和音频文件合并为MP4格式

时长同步: 自动调整视频速度以匹配音频时长

格式转换: 确保输出文件格式兼容性

程序亮点

1. 智能音视频同步



def merge_audio_video(self):
    # 自动检测音视频时长差异
    video_duration = self.get_video_duration(video_path)
    audio_duration = self.get_audio_duration(audio_path)
    
    # 动态调整视频速度以匹配音频
    if duration_diff > 0.5 and video_duration > 0:
        speed_factor = audio_duration / video_duration
        cmd = [
            'ffmpeg', '-i', video_path, '-i', audio_path,
            '-filter:v', f'setpts={speed_factor:.6f}*PTS',
            '-c:v', 'libx264', '-c:a', 'aac', output_path
        ]

    程序同时录制视频和音频,存入不同的文件中,经过测试,两个文件时长并不完全一致,会产生少许差异,当合成时会出现声图不同步现象。为解决这个问题,在将其合并时,根据各自时长的不同,设置一个比率,从而让合成后的文件做到声图同步。这均是程序自动判别的。

2. 健壮的错误处理机制



def record_screen(self):
    """录制屏幕的主循环 - 增强版,包含错误恢复"""
    while self.recording and self.error_count < self.max_errors:
        try:
            # 定期检查资源状态
            if self.frame_count % 100 == 0:
                if not self.video_writer or not self.video_writer.isOpened():
                    self.recover_video_writer()  # 自动恢复视频写入器
        except Exception as e:
            self.error_count += 1
            # 错误计数机制,避免无限循环

3. 内存优化的音频录制



def record_audio(self):
    """使用 sounddevice 录制系统音频 - 流式写入避免内存泄漏"""
    # 直接写入文件,不保存到内存
    wf = wave.open(self.temp_audio_path, 'wb')
    while self.audio_recording and self.recording:
        data, overflowed = stream.read(self.chunk)
        wf.writeframes(data.tobytes())  # 实时写入,避免内存积累

4. 实时性能监控



def update_performance(self):
    """更新性能监控显示"""
    if self.recording:
        # 计算实际FPS
        time_diff = current_time - self.last_frame_time
        self.actual_fps = min(int(self.frame_count / time_diff), int(self.fps_var.get()))
        
        # 显示详细性能信息
        self.perf_var.set(f"状态: 实际FPS: {self.actual_fps}/{self.fps_var.get()}, 帧数: {self.frame_count}, 错误: {self.error_count}")

5. 智能磁盘空间管理



def check_disk_space(self, required_gb=5):
    """检查磁盘剩余空间"""
    drive = os.path.splitdrive(save_path)[0] + "\"
    total, used, free = shutil.disk_usage(drive)
    free_gb = free / (1024**3)
    
    if free_gb < required_gb:
        return False, f"磁盘空间不足: 剩余{free_gb:.1f}GB,建议至少{required_gb}GB"

特殊功能代码

1. 自定义区域选择实现



def select_area(self):
    """打开区域选择窗口"""
    self.area_selection_window = tk.Toplevel(self.root)
    self.area_selection_window.attributes('-fullscreen', True)
    self.area_selection_window.attributes('-alpha', 0.3)  # 半透明覆盖层
    
    # 使用Canvas绘制选择区域
    self.area_canvas = tk.Canvas(self.area_selection_window, cursor="crosshair")
    self.area_rect = self.area_canvas.create_rectangle(...)  # 动态绘制选择框

2. 音频设备自动检测



def setup_audio_devices(self):
    """获取所有音频设备信息"""
    self.audio_devices = sd.query_devices()
    
    # 智能选择默认音频设备
    for i, device in enumerate(self.audio_devices):
        device_name = device['name'].lower()
        # 优先选择虚拟音频设备
        if any(keyword in device_name for keyword in ['cable', 'virtual', 'stereo mix']):
            self.audio_device_index = i
            break

3. 帧率精确控制



def record_screen(self):
    """精确控制录制帧率"""
    target_fps = int(self.fps_var.get())
    frame_interval = 1.0 / target_fps
    
    while self.recording:
        loop_start = time.time()
        
        # 执行录制操作
        frame = self.capture_screen()
        self.video_writer.write(frame)
        
        # 精确睡眠控制帧率
        processing_time = time.time() - loop_start
        sleep_time = frame_interval - processing_time
        if sleep_time > 0:
            time.sleep(sleep_time)

4. 状态信息管理:实时显示录制情况



def status_var_set(self, str):
    """管理状态栏三行信息显示"""
    # 实现信息滚动更新
    self.status_var_init[0] = self.status_var_init[1]
    self.status_var_init[1] = self.status_var_init[2]
    self.status_var_init[2] = str
    self.status_var.set(f'{self.status_var_init[0]}
{self.status_var_init[1]}
{self.status_var_init[2]}')

使用建议

初次使用

先测试音频设备功能

选择较低的帧率(10-15fps)进行测试

录制短片段验证功能正常

高质量录制

使用MP4格式和mp4v编码器

帧率设置在10-30fps之间

确保有足够的磁盘空间

长时间录制

定期检查磁盘空间

监控程序性能状态

避免同时运行其他资源密集型程序

技术支持

如遇到问题,请检查:

所有依赖库是否已正确安装

FFmpeg是否已安装并配置到PATH

音频设备是否支持系统音频录制

是否有足够的系统权限

本程序持续优化中,欢迎反馈使用体验和改进建议!

完整的源代码



# 录屏.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import time
import cv2
import numpy as np
from PIL import Image, ImageTk, ImageGrab
import os
import wave
from webbrowser import open as webopen
from datetime import datetime
import subprocess
import shutil
 
# 尝试导入 sounddevice,如果不可用则禁用音频功能
try:
    import sounddevice as sd
    SOUNDDEVICE_AVAILABLE = True
except ImportError:
    SOUNDDEVICE_AVAILABLE = False
    print("未安装 sounddevice 库,音频录制将被禁用")
 
class ScreenRecorder:
    def __init__(self, root):
        self.root = root
        self.root.title("屏幕录制软件 - 含音频 (优化版)")
        self.root.geometry("570x675")
        self.root.resizable(False, False)
 
        # 设置字体
        self.font_style = "simhei"
 
        # 录制状态
        self.recording = False
        self.paused = False
        self.video_writer = None
        self.start_time = None
        self.audio_recording = False
        self.audio_frames = []
        self.audio_thread = None
        self.recording_area = None  # 存储录制区域 (x1, y1, x2, y2)
        
        # 性能监控
        self.frame_count = 0
        self.last_frame_time = None
        self.actual_fps = 0
        self.status_var_init = ['','','']
        self.error_count = 0
        self.max_errors = 10
        
        # 长时间录制优化
        self.temp_audio_path = None
        self.last_health_check = None
        self.last_frame_count = 0
        self.recording_segment = 1
        self.segment_start_time = None
        self.max_segment_duration = 30 * 60  # 30分钟一个分段
 
        # 音频设置
        self.audio_enabled = SOUNDDEVICE_AVAILABLE
        self.chunk = 1024
        self.format = None  # sounddevice 使用 numpy 数组,不需要指定格式
        self.channels = 2
        self.rate = 44100
 
        # 音频设备设置
        self.audio_devices = []
        self.audio_device_index = None
        if self.audio_enabled:
            self.setup_audio_devices()
 
        # 文件信息
        self.video_file_path = None
        self.audio_file_path = None
 
        # 创建界面
        self.create_widgets()
 
    def setup_audio_devices(self):
        """获取所有音频设备信息"""
        try:
            # 获取所有音频设备
            self.audio_devices = sd.query_devices()
            print("找到以下音频设备:")
            for i, device in enumerate(self.audio_devices):
                print(f"{i}: {device['name']} (输入通道: {device['max_input_channels']}, 输出通道: {device['max_output_channels']})")
 
            # 查找虚拟音频设备或立体声混音设备作为默认选择
            for i, device in enumerate(self.audio_devices):
                device_name = device['name'].lower()
                # 查找可能的虚拟音频设备名称
                if any(keyword in device_name for keyword in ['cable', 'virtual', 'stereo mix', 'loopback']):
                    print(f"选择默认音频设备: {device['name']} (索引: {i})")
                    self.audio_device_index = 0
                    break
 
            # 如果没有找到虚拟设备,尝试使用默认输入设备
            if self.audio_device_index is None:
                try:
                    default_input = sd.default.device[0]  # 默认输入设备索引
                    if default_input < len(self.audio_devices):
                        self.audio_device_index = default_input
                        print(f"使用默认输入设备: {self.audio_devices[default_input]['name']}")
                except:
                    print("未找到可用的音频输入设备,音频录制将被禁用")
                    self.audio_enabled = False
 
        except Exception as e:
            print(f"音频设备设置失败: {e}")
            self.audio_enabled = False
 
    def create_widgets(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
 
        # 录制区域选择
        area_frame = ttk.LabelFrame(main_frame, text="录制区域", padding="5")
        area_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5))
 
        self.area_var = tk.StringVar(value="全屏")
        fullscreen_radio = ttk.Radiobutton(area_frame, text="全屏录制", variable=self.area_var, value="全屏")
        fullscreen_radio.grid(row=0, column=0, sticky=tk.W)
 
        custom_radio = ttk.Radiobutton(area_frame, text="自定义区域", variable=self.area_var, value="自定义")
        custom_radio.grid(row=0, column=1, sticky=tk.W, padx=(20, 0))
 
        self.select_area_button = ttk.Button(area_frame, text="选择区域", command=self.select_area)
        self.select_area_button.grid(row=0, column=2, padx=(10, 0))
 
        self.area_label = ttk.Label(area_frame, text="未选择区域", font=(self.font_style, 9))
        self.area_label.grid(row=0, column=3, padx=(10, 0))
 
        # 预览区域
        preview_frame = ttk.LabelFrame(main_frame,width=540,height=280, text="预览", padding="5")
        preview_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5))
        preview_frame.grid_propagate(False)  # 关键:禁止Frame根据内容调整大小
 
        # 使用普通 Label 而不是 ttk.Label,并设置固定大小
        self.preview_label = tk.Label(
            preview_frame,
            text="预览区域

点击开始录制后显示预览",
            background="white",
            width=88,
            height=20,
            justify=tk.CENTER,
            font=(self.font_style, 9)
        )
        self.preview_label.grid(row=0, column=0,padx=0,pady=0)
 
        # 控制按钮区域
        controls_frame = ttk.Frame(main_frame)
        controls_frame.grid(row=3, column=0, columnspan=5, pady=5)
 
        self.start_button = ttk.Button(controls_frame, text="开始录制", command=self.start_recording,width=8)
        self.start_button.grid(row=0, column=0, padx=5)
 
        self.pause_button = ttk.Button(controls_frame, text="暂停", command=self.pause_recording, state=tk.DISABLED,width=8)
        self.pause_button.grid(row=0, column=1, padx=5)
 
        self.stop_button = ttk.Button(controls_frame, text="停止录制", command=self.stop_recording, state=tk.DISABLED,width=8)
        self.stop_button.grid(row=0, column=2, padx=5)
 
        # 磁盘空间检查按钮
        self.disk_space_button = ttk.Button(controls_frame, text="检查磁盘空间", command=self.check_disk_space)
        self.disk_space_button.grid(row=0, column=3, padx=25)
 
        self.web_button = ttk.Button(controls_frame, text="交流", command=self.web,width=8)
        self.web_button.grid(row=0, column=4, padx=5)
 
        self.exit_button = ttk.Button(controls_frame, text="退出", command=self.exit,width=8)
        self.exit_button.grid(row=0, column=5, padx=5)
 
        # 设置区域
        settings_frame = ttk.LabelFrame(main_frame, text="设置", padding="5")
        settings_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5))
 
        # 帧率设置
        ttk.Label(settings_frame, text="帧率(fps):", font=(self.font_style, 9)).grid(row=0, column=0, sticky=tk.W)
        self.fps_var = tk.StringVar(value="10")  # 降低默认帧率以提高稳定性
        fps_spinbox = ttk.Spinbox(settings_frame, from_=1, to=60, textvariable=self.fps_var, width=10)
        fps_spinbox.grid(row=0, column=1, sticky=tk.W, padx=(5, 0))
 
        # 编码器设置
        ttk.Label(settings_frame, text="编码器:", font=(self.font_style, 9)).grid(row=0, column=2, sticky=tk.W, padx=(20, 0))
        self.codec_var = tk.StringVar(value="mp4v")
        codec_combo = ttk.Combobox(settings_frame, textvariable=self.codec_var, width=10, state="readonly")
        codec_combo['values'] = ("mp4v", "XVID", "MJPG", "DIVX")
        codec_combo.grid(row=0, column=3, sticky=tk.W, padx=(5, 0))
 
        # 音频设置
        self.audio_var = tk.BooleanVar(value=self.audio_enabled)
        audio_check = ttk.Checkbutton(settings_frame, text="录制系统音频", variable=self.audio_var, command=self.toggle_audio_settings)
        audio_check.grid(row=0, column=4, sticky=tk.W, pady=(5, 0))
 
        # 音频设备选择
        ttk.Label(settings_frame, text="音频设备:", font=(self.font_style, 9)).grid(row=2, column=0, sticky=tk.W, pady=(5, 0))
 
        # 创建音频设备下拉列表
        device_names = []
        if self.audio_enabled:
            for i, device in enumerate(self.audio_devices):
                # 只显示有输入通道的设备
                if device['max_input_channels'] > 0:
                    device_info = f"{i}: {device['name']} (输入:{device['max_input_channels']}, 输出:{device['max_output_channels']})"
                    device_names.append(device_info)
 
        self.audio_device_var = tk.StringVar()
        self.audio_device_combo = ttk.Combobox(
            settings_frame,
            textvariable=self.audio_device_var,
            values=device_names,
            width=50,
            state="readonly"
        )
        self.audio_device_combo.grid(row=2, column=1, columnspan=3, sticky=(tk.W, tk.E), padx=(5, 0), pady=(5, 0))
 
        # 设置默认选择的音频设备
        if self.audio_enabled and self.audio_device_index is not None and device_names:
            for i, device_info in enumerate(device_names):
                if device_info.startswith(f"{self.audio_device_index}:"):
                    self.audio_device_combo.set(device_info)
                    break
 
        # 测试音频设备按钮
        self.test_audio_button = ttk.Button(settings_frame, text="测试音频设备", command=self.test_audio_device)
        self.test_audio_button.grid(row=2, column=4, pady=(5, 0))
 
        # 如果音频不可用,禁用音频选项并显示提示
        if not self.audio_enabled:
            audio_check.config(state=tk.DISABLED)
            self.audio_device_combo.config(state=tk.DISABLED)
            self.test_audio_button.config(state=tk.DISABLED)
            ttk.Label(settings_frame, text="(音频设备不可用)", foreground="red", font=(self.font_style, 9)).grid(row=1, column=1, sticky=tk.W, pady=(5, 0))
            # 添加音频设置说明
            audio_help = ttk.Label(
                settings_frame,
                text="要录制系统音频,请安装 sounddevice 库: pip install sounddevice",
                foreground="blue",
                font=(self.font_style, 8)
            )
            audio_help.grid(row=3, column=0, columnspan=5, sticky=tk.W, pady=(5, 0))
 
        # 保存路径设置
        ttk.Label(settings_frame, text="保存路径:", font=(self.font_style, 9)).grid(row=4, column=0, sticky=tk.W, pady=(5, 0))
 
        # 修复默认路径问题 - 使用正斜杠
        desktop = os.path.join(os.path.expanduser("~"), "Desktop")
        default_filename = f"recording_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"  # 默认使用MP4
        default_path = os.path.join(desktop, default_filename).replace("\", "/")
 
        self.path_var = tk.StringVar(value=default_path)
        path_entry = ttk.Entry(settings_frame, textvariable=self.path_var, width=50)
        path_entry.grid(row=4, column=1, columnspan=3, sticky=(tk.W, tk.E), padx=(5, 0), pady=(5, 0))
 
        browse_button = ttk.Button(settings_frame, text="浏览", command=self.browse_path)
        browse_button.grid(row=4, column=4, pady=(5, 0))
 
        # 性能监控显示
        perf_frame = ttk.Frame(settings_frame)
        perf_frame.grid(row=6, column=0, columnspan=5, sticky=(tk.W, tk.E), pady=(5, 0))
 
        self.perf_var = tk.StringVar(value="状态: 等待开始...")
        perf_label = ttk.Label(perf_frame, textvariable=self.perf_var, font=(self.font_style, 8),width=88)
        perf_label.grid(row=0, column=0, sticky=tk.W)
 
        # 文件信息区域
        info_frame = ttk.LabelFrame(main_frame, text="文件信息", padding="5")
        info_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 0))
        
 
        # 视频文件信息
        self.video_info_var = tk.StringVar(value="视频文件: 未录制")
        video_info_label = ttk.Label(info_frame, textvariable=self.video_info_var, font=(self.font_style, 9),width=70)
        video_info_label.grid(row=0, column=0, columnspan=2, sticky=tk.W)
 
        # 音频文件信息
        self.audio_info_var = tk.StringVar(value="音频文件: 未录制")
        audio_info_label = ttk.Label(info_frame, textvariable=self.audio_info_var, font=(self.font_style, 9),width=70)
        audio_info_label.grid(row=1, column=0, columnspan=2, sticky=tk.W)
 
        # 合成的文件信息
        self.end_info_var = tk.StringVar(value="合成文件: 未合成")
        end_info_label = ttk.Label(info_frame, textvariable=self.end_info_var, font=(self.font_style, 9),width=70)
        end_info_label.grid(row=2, column=0, columnspan=2, sticky=tk.W)
 
        # 打开文件夹按钮
        self.open_folder_button = ttk.Button(info_frame, text="打开文件夹", command=self.open_output_folder, state=tk.DISABLED)
        self.open_folder_button.grid(row=0, column=2, rowspan=3, padx=(20, 0))
        # info_frame.grid_propagate(False)  # 关键:禁止Frame根据内容调整大小
        
        # 状态栏
        self.status_var = tk.StringVar(value="")
        self.status_var_set("准备就绪")
        self.status_var_set("现在可以点击“开始录制”进行全屏录屏 或 进行相关设定后再“开始录制”")
        self.status_var_set("fps的设置是每秒多少帧,受机器性能影响,设置过高可能会录屏失败!")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, font=(self.font_style, 9))
        status_bar.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))        
 
        # 配置网格权重
        main_frame.columnconfigure(0, weight=1)
        settings_frame.columnconfigure(1, weight=1)
        info_frame.columnconfigure(0, weight=1)
 
        # 初始更新音频设备状态
        self.toggle_audio_settings()
        
        # 初始检查磁盘空间
        self.root.after(1000, self.check_disk_space)
 
    def web(self):
        webopen("https://blog.csdn.net/weixin_69832035?spm=1010.2135.3001.5343", new=2,
                        autoraise=True)  # 在新窗口或标签页中打开网页
 
    def check_disk_space(self, required_gb=5):
        """检查磁盘剩余空间"""
        save_path = self.path_var.get()
        resule1, resule2 = True, ""
        if not save_path:
            resule1, resule2 = True, "未设置保存路径,请先设置保存路径,然后再检查磁盘空间"
        else:
            drive = os.path.splitdrive(save_path)[0] + "\"
            try:
                total, used, free = shutil.disk_usage(drive)
                free_gb = free / (1024**3)  # 转换为GB
                
                if free_gb < required_gb:
                    resule1, resule2 = False, f"磁盘空间不足: 剩余{free_gb:.1f}GB,建议至少{required_gb}GB"
                else:
                    resule1, resule2 = True, f"磁盘空间充足: 剩余{free_gb:.1f}GB"
            except Exception as e:
                resule1, resule2 = True, f"无法检测磁盘空间: {str(e)}"
        if self.status_var_init[0] == '准备就绪':
            resule2 = "准备就绪..." + resule2
            self.status_var_set(resule2)
        elif not resule2 == "" and not resule2 == self.status_var_init[2]:
            self.status_var_set(resule2)
        return resule1, resule2
 
    def toggle_audio_settings(self):
        """切换音频设置的状态"""
        if self.audio_var.get() and self.audio_enabled:
            self.audio_device_combo.config(state="readonly")
            self.test_audio_button.config(state="normal")
        else:
            self.audio_device_combo.config(state="disabled")
            self.test_audio_button.config(state="disabled")
 
    def test_audio_device(self):
        """测试选定的音频设备"""
        selected_device = self.audio_device_combo.get()
        if not selected_device:
            messagebox.showwarning("警告", "请先选择一个音频设备")
            return
 
        # 从选定的设备信息中提取设备索引
        try:
            device_index = int(selected_device.split(":")[0])
        except:
            messagebox.showerror("错误", "无法解析设备索引")
            return
 
        # 在后台线程中测试音频设备
        test_thread = threading.Thread(target=self._test_audio_device, args=(device_index,))
        test_thread.daemon = True
        test_thread.start()
 
    def _test_audio_device(self, device_index):
        """在后台测试音频设备"""
        try:
            self.status_var_set("正在测试音频设备...")
 
            # 创建音频流
            stream = sd.InputStream(
                device=device_index,
                channels=1,  # 单声道测试
                samplerate=44100,
                blocksize=1024,
                dtype='int16'
            )
 
            # 开始流
            stream.start()
 
            # 录制一小段音频
            test_frames = []
            start_time = time.time()
            while time.time() - start_time < 3:  # 录制3秒
                data, overflowed = stream.read(1024)
                test_frames.append(data.tobytes())
                if overflowed:
                    print("音频缓冲区溢出")
 
            # 停止流
            stream.stop()
            stream.close()
 
            # 分析录制的音频
            if test_frames:
                audio_data = b''.join(test_frames)
                audio_array = np.frombuffer(audio_data, dtype=np.int16)
 
                # 计算音频级别
                rms = np.sqrt(np.mean(audio_array**2))
                max_level = np.max(np.abs(audio_array))
 
                # 显示测试结果
                if rms > 100 or max_level > 1000:  # 阈值可以根据需要调整
                    result = f"音频设备测试成功 (电平: {rms:.1f}, 峰值: {max_level})"
                    self.status_var_set(result)
                    messagebox.showinfo("测试结果", f"音频设备测试成功!
检测到音频信号。
电平: {rms:.1f}
峰值: {max_level}")
                else:
                    result = f"音频设备测试完成但信号较弱 (电平: {rms:.1f}, 峰值: {max_level})"
                    self.status_var_set(result)
                    messagebox.showwarning("测试结果", f"音频设备测试完成但信号较弱。
可能是设备没有输入信号或音量太低。
电平: {rms:.1f}
峰值: {max_level}")
            else:
                self.status_var_set("音频设备测试失败 - 无数据")
                messagebox.showerror("测试结果", "音频设备测试失败 - 未录制到任何数据")
 
        except Exception as e:
            error_msg = f"音频设备测试失败: {str(e)}"
            self.status_var_set(error_msg[:81])
            messagebox.showerror("测试结果", error_msg)
 
    def get_video_duration(self, video_path):
        """获取视频文件时长(秒)"""
        try:
            cap = cv2.VideoCapture(video_path)
            if not cap.isOpened():
                return 0
            
            # 获取帧率
            fps = cap.get(cv2.CAP_PROP_FPS)
            # 获取总帧数
            frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
            
            # 计算时长
            duration = frame_count / fps if fps > 0 else 0
            
            cap.release()
            return duration
        except Exception as e:
            print(f"获取视频时长失败: {e}")
            return 0
    
    def get_audio_duration(self, audio_path):
        """获取音频文件时长(秒)"""
        try:
            with wave.open(audio_path, 'rb') as wf:
                frames = wf.getnframes()
                rate = wf.getframerate()
                duration = frames / float(rate)
                return duration
        except Exception as e:
            print(f"获取音频时长失败: {e}")
            return 0
    
    def format_duration(self, seconds):
        """将秒数格式化为 MM:SS 或 HH:MM:SS 格式"""
        if seconds < 60:
            return f"{seconds:.1f}秒"
        elif seconds < 3600:
            minutes = int(seconds // 60)
            seconds_remaining = seconds % 60
            return f"{minutes}分{seconds_remaining:.1f}秒"
        else:
            hours = int(seconds // 3600)
            minutes = int((seconds % 3600) // 60)
            seconds_remaining = seconds % 60
            return f"{hours}时{minutes}分{seconds_remaining:.1f}秒"
 
    def select_area(self):
        """打开区域选择窗口"""
        # 自动切换到自定义区域选项
        self.area_var.set("自定义")
 
        self.area_selection_window = tk.Toplevel(self.root)
        self.area_selection_window.title("选择录制区域")
        self.area_selection_window.attributes('-fullscreen', True)
        self.area_selection_window.attributes('-alpha', 0.3)
        self.area_selection_window.configure(background='black')
 
        # 创建画布用于绘制选择区域
        self.area_canvas = tk.Canvas(
            self.area_selection_window,
            highlightthickness=0,
            cursor="crosshair"
        )
        self.area_canvas.pack(fill=tk.BOTH, expand=True)
 
        # 绑定鼠标事件
        self.area_canvas.bind("<ButtonPress-1>", self.on_area_press)
        self.area_canvas.bind("<B1-Motion>", self.on_area_drag)
        self.area_canvas.bind("<ButtonRelease-1>", self.on_area_release)
 
        # 添加说明文本
        self.area_canvas.create_text(
            self.area_selection_window.winfo_screenwidth() // 2,
            self.area_selection_window.winfo_screenheight() // 2,
            text="拖动鼠标选择录制区域,按ESC取消",
            fill="red",
            font=(self.font_style, 24)
        )
 
        # 绑定ESC键取消选择
        self.area_selection_window.bind("<Escape>", lambda e: self.area_selection_window.destroy())
 
        self.area_start_x = None
        self.area_start_y = None
        self.area_rect = None
 
    def on_area_press(self, event):
        """开始区域选择"""
        self.area_start_x = event.x
        self.area_start_y = event.y
 
        # 创建选择矩形
        if self.area_rect:
            self.area_canvas.delete(self.area_rect)
 
        self.area_rect = self.area_canvas.create_rectangle(
            self.area_start_x, self.area_start_y,
            self.area_start_x, self.area_start_y,
            outline="red", width=2
        )
 
    def on_area_drag(self, event):
        """拖动鼠标调整选择区域"""
        if self.area_rect:
            self.area_canvas.coords(
                self.area_rect,
                self.area_start_x, self.area_start_y,
                event.x, event.y
            )
 
    def on_area_release(self, event):
        """完成区域选择"""
        # 确保坐标正确(左上角到右下角)
        x1 = min(self.area_start_x, event.x)
        y1 = min(self.area_start_y, event.y)
        x2 = max(self.area_start_x, event.x)
        y2 = max(self.area_start_y, event.y)
 
        # 设置录制区域
        self.recording_area = (x1, y1, x2, y2)
        self.area_label.config(text=f"区域: {x1},{y1} - {x2},{y2} ({x2-x1}x{y2-y1})")
 
        # 关闭选择窗口
        self.area_selection_window.destroy()
 
    def browse_path(self):
        filename = filedialog.asksaveasfilename(
            defaultextension=".mp4",  # 默认保存为MP4格式以支持音视频合并
            filetypes=[("MP4 files", "*.mp4"), ("AVI files", "*.avi"), ("所有文件", "*.*")]
        )
        if filename:
            # 确保使用正斜杠
            self.path_var.set(filename.replace("\", "/"))
 
            # 根据文件扩展名自动选择编码器
            if filename.lower().endswith('.mp4'):
                self.codec_var.set("mp4v")  # MP4 格式推荐使用 mp4v 编码
            else:
                self.codec_var.set("XVID")  # AVI 格式使用 XVID 编码
 
    def start_recording(self):
        # 检查磁盘空间
        disk_ok, disk_msg = self.check_disk_space(required_gb=2)  # 至少2GB空间
        if not disk_ok:
            if not messagebox.askyesno("磁盘空间警告", f"{disk_msg}

是否继续录制?"):
                return
 
        try:
            self.fps = int(self.fps_var.get())
            if self.fps <= 0 :
                messagebox.showerror("错误", "帧率必须大于0")
                return
            if self.fps > 60:
                messagebox.showerror("错误", "帧率过大可能会丢帧严重,建议控制在60帧以内")
                return
        except ValueError:
            messagebox.showerror("错误", "请输入有效的帧率")
            return
 
        save_path = self.path_var.get()
        if os.path.exists(save_path):
            messagebox.showerror("提示",f"文件 {save_path} 已经存在!
请更改文件名 或 删除文件 或 更换路径后再试。")
            return
      
        if not save_path:
            messagebox.showerror("错误", "请选择保存路径")
            return
 
        # 强制使用MP4格式避免文件大小限制
        if not save_path.lower().endswith('.mp4'):
            save_path = os.path.splitext(save_path)[0] + '.mp4'
            self.path_var.set(save_path)
 
        # 获取录制区域
        if self.area_var.get() == "自定义" and self.recording_area:
            screen_size = (self.recording_area[2] - self.recording_area[0],
                          self.recording_area[3] - self.recording_area[1])
        else:
            # 全屏录制
            try:
                screen_size = ImageGrab.grab().size
                self.recording_area = None  # 全屏录制不需要区域
            except:
                screen_size = (1920, 1080)  # 默认尺寸
                self.recording_area = None
 
        # 根据选择的编码器设置 fourcc
        codec = self.codec_var.get()
        if codec == "XVID":
            fourcc = cv2.VideoWriter_fourcc(*'XVID')
            extension = ".avi"
        elif codec == "MJPG":
            fourcc = cv2.VideoWriter_fourcc(*'MJPG')
            extension = ".avi"  # MJPG 通常用于 AVI
        elif codec == "DIVX":
            fourcc = cv2.VideoWriter_fourcc(*'DIVX')
            extension = ".avi"
        elif codec == "mp4v":
            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
            extension = ".mp4"
        else:
            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
            extension = ".mp4"
 
        # 确保文件扩展名与编码器匹配
        if not save_path.lower().endswith(extension):
            save_path = os.path.splitext(save_path)[0] + extension
            self.path_var.set(save_path)
 
        try:
            # 初始化视频编写器
            self.video_writer = cv2.VideoWriter(save_path, fourcc, self.fps, screen_size)
            if not self.video_writer.isOpened():
                messagebox.showerror("错误", "无法创建视频文件,请检查编码器和分辨率设置")
                return            
        except Exception as e:
            messagebox.showerror("错误", f"无法创建视频文件: {str(e)}")
            return
 
        # 初始化音频录制
        self.audio_frames = []
        self.audio_enabled = self.audio_var.get() and self.audio_enabled
 
        # 获取选定的音频设备索引
        if self.audio_enabled:
            selected_device = self.audio_device_combo.get()
            if selected_device:
                try:
                    self.audio_device_index = int(selected_device.split(":")[0])
                    print(f"使用音频设备: {self.audio_devices[self.audio_device_index]['name']}")
                except:
                    print("无法解析音频设备索引,使用默认设备")
            else:
                self.audio_enabled = False
                print("未选择音频设备,禁用音频录制")
 
        # 重置录制状态
        self.frame_count = 0
        self.last_frame_time = time.time()
        self.actual_fps = 0
        self.error_count = 0
        self.recording_segment = 1
        self.segment_start_time = time.time()
        self.last_health_check = time.time()
        self.last_frame_count = 0
 
        # 更新状态和按钮
        self.recording = True
        self.paused = False
        self.start_time = time.time()
        self.start_button.config(state=tk.DISABLED)
        self.pause_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.NORMAL)
 
        # 开始录制线程
        self.recording_thread = threading.Thread(target=self.record_screen)
        self.recording_thread.daemon = True
        self.recording_thread.start()
 
        # 开始音频录制线程
        if self.audio_enabled:
            self.audio_thread = threading.Thread(target=self.record_audio)
            self.audio_thread.daemon = True
            self.audio_thread.start()
            self.status_var_set("录制中... (含音频)")
        else:
            self.status_var_set("录制中... (无音频)")
 
        # 开始预览更新
        self.update_preview()
        # 开始性能监控
        self.update_performance()
        # 开始健康监控
        self.start_health_monitor()
        
    def update_performance(self):
        """更新性能监控显示"""
        if self.recording:
            try:
                current_time = time.time()
                if self.last_frame_time and self.frame_count > 0:
                    time_diff = current_time - self.last_frame_time
                    if time_diff > 0:
                        self.actual_fps = min(int(self.frame_count / time_diff), int(self.fps_var.get()))
 
                # 计算录制时长
                recording_time = current_time - self.start_time if self.start_time else 0
                time_str = self.format_duration(recording_time)
                
                self.perf_var.set(f"录制中: {time_str} | 状态: 实际FPS: {self.actual_fps}/{self.fps_var.get()}, 帧数: {self.frame_count}, 错误: {self.error_count}")
 
                # 每2秒更新一次性能显示
                self.root.after(2000, self.update_performance)
            except Exception as e:
                print(f"性能监控更新失败: {e}")
 
    def record_audio(self):
        """使用 sounddevice 录制系统音频 - 流式写入避免内存泄漏"""
        try:
            print(f"开始录制系统音频,使用设备: {self.audio_devices[self.audio_device_index]['name']}")
 
            # 创建临时音频文件并立即开始写入
            video_path = self.path_var.get()
            self.temp_audio_path = os.path.splitext(video_path)[0] + ".wav"
            
            wf = wave.open(self.temp_audio_path, 'wb')
            wf.setnchannels(self.channels)
            wf.setsampwidth(2)  # 16位音频,样本宽度为2字节
            wf.setframerate(self.rate)
 
            # 创建音频流
            stream = sd.InputStream(
                device=self.audio_device_index,
                channels=self.channels,
                samplerate=self.rate,
                blocksize=self.chunk,
                dtype='int16'
            )
 
            self.audio_recording = True
            stream.start()
 
            while self.audio_recording and self.recording:
                if not self.paused:
                    try:
                        # 读取音频数据并直接写入文件
                        data, overflowed = stream.read(self.chunk)
                        if overflowed:
                            print("音频缓冲区溢出")
 
                        # 直接写入文件,不保存到内存
                        wf.writeframes(data.tobytes())
                    except Exception as e:
                        print(f"音频读取错误: {e}")
                        self.error_count += 1
                        break
                else:
                    time.sleep(0.1)  # 暂停时减少CPU使用
 
            # 停止录制
            stream.stop()
            stream.close()
            wf.close()
            print("音频录制结束")
 
        except Exception as e:
            print(f"音频录制失败: {e}")
            self.audio_enabled = False
            self.error_count += 1
            # 在状态栏显示错误信息
            error_msg = str(e)
            self.root.after(0, lambda msg=error_msg: self.status_var_set(f"音频录制失败: {msg[:73]}"))
 
    def status_var_set(self,str):
        if '准备就绪' in self.status_var_init[0]:
            self.status_var_init[0] = str
        else:
            self.status_var_init[0] = self.status_var_init[1]
            self.status_var_init[1] = self.status_var_init[2]
            self.status_var_init[2] = str
        self.status_var.set(f'{self.status_var_init[0]}
{self.status_var_init[1]}
{self.status_var_init[2]}')
 
    def pause_recording(self):
        if self.paused:
            self.paused = False
            self.pause_button.config(text="暂停")
            if self.audio_enabled:
                self.status_var_set("录制中... (含音频)")
            else:
                self.status_var_set("录制中... (无音频)")
        else:
            self.paused = True
            self.pause_button.config(text="继续")
            self.status_var_set("已暂停")
 
    def exit(self):
        if self.recording:
            self.stop_recording()
        self.root.destroy()  #关闭窗口
        self.root.quit()
 
    def stop_recording(self):
        self.recording = False
        self.audio_recording = False
        self.start_button.config(state=tk.NORMAL)
        self.pause_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.DISABLED)
 
        # 等待录制线程结束
        if hasattr(self, 'recording_thread') and self.recording_thread.is_alive():
            self.recording_thread.join(timeout=2.0)
 
        # 等待音频线程结束
        if self.audio_enabled and hasattr(self, 'audio_thread') and self.audio_thread.is_alive():
            self.audio_thread.join(timeout=2.0)
 
        # 保存视频
        video_path = self.path_var.get()
        if self.video_writer:
            self.video_writer.release()
            self.video_writer = None
 
        # 获取录制时长信息
        recording_time = time.time() - self.start_time if self.start_time else 0
        video_duration = self.get_video_duration(video_path) if os.path.exists(video_path) else 0
 
        # 格式化时长信息
        recording_time_str = self.format_duration(recording_time)
        video_duration_str = self.format_duration(video_duration)
 
        # 构建状态信息
        status_parts = [f"录制完成 - 实际时长: {recording_time_str}"]
 
        if video_duration > 0:
            status_parts.append(f"视频: {video_duration_str}")
 
        if self.audio_enabled and hasattr(self, 'temp_audio_path') and self.temp_audio_path:
            audio_duration = self.get_audio_duration(self.temp_audio_path)
            audio_duration_str = self.format_duration(audio_duration)
            status_parts.append(f"音频: {audio_duration_str}")
        elif self.audio_var.get() and not self.audio_enabled:
            status_parts.append("(无音频)")
 
        status_text = ", ".join(status_parts)
        self.status_var.set(status_text)
 
        # 更新文件信息
        if os.path.exists(video_path):
            file_size = os.path.getsize(video_path) / (1024 * 1024)  # MB
            self.video_info_var.set(f"视频文件:{os.path.basename(video_path)} ({file_size:.1f}MB, {video_duration_str})")
        if os.path.exists(self.temp_audio_path):
            file_size = os.path.getsize(self.temp_audio_path) / (1024 * 1024)  # MB
            self.audio_info_var.set(f"音频文件:{os.path.basename(self.temp_audio_path)} ({file_size:.1f}MB, {audio_duration_str})")
 
        # 启用打开文件夹按钮
        self.open_folder_button.config(state=tk.NORMAL)
 
        # 在控制台输出详细信息
        msg = f"录制结束:录制时间:{recording_time_str} 帧数:{self.frame_count} FPS:{self.frame_count/recording_time if recording_time > 0 else 0:.1f} 错误:{self.error_count}"
        print(msg)
        # 重置性能显示
        self.perf_var.set(msg)        
 
        # 合并音视频
        if self.audio_enabled and hasattr(self, 'temp_audio_path') and os.path.exists(self.temp_audio_path):
            self.merge_audio_video()
        else:
            # 如果没有音频,直接使用视频文件
            if self.start_time:
                recording_time = time.time() - self.start_time
                status_text = f"录制完成 - 时长: {recording_time:.2f} 秒"
                if not self.audio_enabled and self.audio_var.get():
                    status_text += " (无音频)"
                self.status_var_set(status_text)
 
        # 重置预览
        self.preview_label.config(image='')
        self.preview_label.config(
            text="预览区域

点击开始录制后显示预览",
            background="white",
            width=88,
            height=20,
            justify=tk.CENTER,
            font=(self.font_style, 9)
        )
        self.preview_label.grid(row=0, column=0,padx=0,pady=0)
                
    def merge_audio_video(self):
        """合并视频和音频文件,并同步时长"""
        self.status_var_set("正在合并音视频...")
        
        # 获取视频文件路径
        video_path = self.path_var.get()
        
        # 使用已经保存的音频文件路径
        audio_path = self.temp_audio_path
 
        try:
            # 获取视频和音频的实际时长
            video_duration = self.get_video_duration(video_path)
            audio_duration = self.get_audio_duration(audio_path)
            
            print(f"视频时长: {video_duration:.2f}秒, 音频时长: {audio_duration:.2f}秒")
            
            # 创建输出文件路径(MP4格式)
            output_path = os.path.splitext(video_path)[0] + "_with_audio.mp4"
 
            # 如果视频和音频时长差异较大(超过0.5秒),则调整视频速度以匹配音频
            duration_diff = abs(video_duration - audio_duration)
            
            if duration_diff > 0.5 and video_duration > 0:
                # 计算速度调整因子
                speed_factor = audio_duration / video_duration
                print(f"检测到时长差异: {duration_diff:.2f}秒,将视频速度调整为: {speed_factor:.3f}")
                
                # 使用FFmpeg调整视频速度并合并音频
                cmd = [
                    'ffmpeg',
                    '-i', video_path,      # 输入视频文件
                    '-i', audio_path,      # 输入音频文件
                    '-filter:v', f'setpts={speed_factor:.6f}*PTS',  # 调整视频播放速度
                    '-c:v', 'libx264',     # 视频编码器
                    '-c:a', 'aac',         # 音频编码器
                    '-strict', 'experimental',
                    '-y',                  # 覆盖输出文件
                    output_path
                ]
                
                self.status_var_set(f"正在同步音视频 (速度调整: {speed_factor:.4f}x)...请耐心等候...")
 
            else:
                # 如果时长差异不大,直接合并
                print(f"时长差异较小 ({duration_diff:.2f}秒),直接合并")
                cmd = [
                    'ffmpeg',
                    '-i', video_path,      # 输入视频文件
                    '-i', audio_path,      # 输入音频文件
                    '-c:v', 'libx264',     # 视频编码器
                    '-c:a', 'aac',         # 音频编码器
                    '-strict', 'experimental',
                    '-y',                  # 覆盖输出文件
                    output_path
                ]
                self.status_var_set("正在合并音视频...请耐心等候...")
 
            print(cmd)
            if audio_duration >=60 and not messagebox.askyesno("即将进行频文件和音频文件的合并", "文件较大,可以会耗费较多时间,需要您耐心等待!

是否继续合并?"):
                self.status_var_set("合并音视频被中止(因为在问您是否继续时您选择了“否”)...")
                return            
            # 执行FFmpeg命令
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
 
            if result.returncode == 0:
                # 合并成功,删除临时文件
                # try:
                #     if os.path.exists(audio_path):
                #         os.remove(audio_path)
                #     # 可以选择删除原始无音频视频文件
                #     # if os.path.exists(video_path):
                #     #     os.remove(video_path)
                # except Exception as e:
                #     print(f"删除临时文件失败: {e}")
 
                # 更新保存路径为合并后的文件
                self.path_var.set(output_path)
                
                # 验证合并后的文件时长
                final_duration = self.get_video_duration(output_path)
                print(f"合并后文件时长: {final_duration:.2f}秒")
                
                if self.start_time:
                    sync_info = f" (已同步)" if duration_diff > 0.5 else ""
                    self.status_var_set(f"视频文件和音频文件合成完成 - 时长: {final_duration:.2f} 秒 (含音频{sync_info})")
                    file_size = os.path.getsize(self.temp_audio_path) / (1024 * 1024)  # MB
                    self.end_info_var.set(f"合成文件:{os.path.basename(output_path)} ({file_size:.1f}MB, {self.format_duration(final_duration)})")  
            else:
                # 合并失败,保留原始文件
                error_msg = f"音视频合并失败: {result.stderr}"
                print(error_msg)
                self.status_var_set("合并失败,已保存独立文件")
 
        except subprocess.TimeoutExpired:
            self.status_var_set("音视频合并超时")
        except FileNotFoundError:
            self.status_var_set("未找到FFmpeg,请安装FFmpeg以合并音视频")
            messagebox.showwarning("警告", "未找到FFmpeg,视频和音频已分别保存为独立文件。
请安装FFmpeg以自动合并音视频。")
        except Exception as e:
            error_msg = f"合并过程中出错: {str(e)}"
            print(error_msg)
            self.status_var_set("合并失败,已保存独立文件")
 
    def capture_screen(self):
        """使用 PIL 的 ImageGrab 捕获屏幕"""
        try:
            if self.recording_area:
                # 捕获指定区域
                screenshot = ImageGrab.grab(bbox=self.recording_area)
            else:
                # 捕获全屏
                screenshot = ImageGrab.grab()
 
            frame = np.array(screenshot)
            frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
            return frame
        except Exception as e:
            print(f"屏幕捕获失败: {e}")
            # 创建一个黑色帧作为备选
            return np.zeros((1080, 1920, 3), dtype=np.uint8)
 
    def recover_video_writer(self):
        """恢复视频写入器"""
        try:
            if self.video_writer:
                self.video_writer.release()
                
            save_path = self.path_var.get()
            
            # 获取录制区域
            if self.area_var.get() == "自定义" and self.recording_area:
                screen_size = (self.recording_area[2] - self.recording_area[0],
                              self.recording_area[3] - self.recording_area[1])
            else:
                screen_size = (1920, 1080)
                
            # 根据选择的编码器设置 fourcc
            codec = self.codec_var.get()
            if codec == "XVID":
                fourcc = cv2.VideoWriter_fourcc(*'XVID')
            elif codec == "MJPG":
                fourcc = cv2.VideoWriter_fourcc(*'MJPG')
            elif codec == "DIVX":
                fourcc = cv2.VideoWriter_fourcc(*'DIVX')
            else:
                fourcc = cv2.VideoWriter_fourcc(*'mp4v')
                
            self.video_writer = cv2.VideoWriter(save_path, fourcc, int(self.fps_var.get()), screen_size)
            print("视频写入器已恢复")
            
        except Exception as e:
            print(f"恢复视频写入器失败: {e}")
 
    def record_screen(self):
        """录制屏幕的主循环 - 增强版,包含错误恢复"""
        target_fps = int(self.fps_var.get())
        frame_interval = 1.0 / target_fps
 
        while self.recording and self.error_count < self.max_errors:
            if not self.paused:
                try:
                    loop_start = time.time()
 
                    # 定期检查资源状态
                    if self.frame_count % 100 == 0:  # 每100帧检查一次
                        if not self.video_writer or not self.video_writer.isOpened():
                            print("视频写入器异常,尝试恢复...")
                            self.recover_video_writer()
 
                    # 截取屏幕
                    frame = self.capture_screen()
 
                    # 写入视频
                    if self.video_writer and self.video_writer.isOpened():
                        self.video_writer.write(frame)
                        self.frame_count += 1
                    else:
                        print("视频写入器不可用")
                        self.error_count += 1
 
                    # 精确控制帧率
                    processing_time = time.time() - loop_start
                    sleep_time = frame_interval - processing_time
 
                    if sleep_time > 0:
                        time.sleep(sleep_time)
                    else:
                        # 如果处理时间超过帧间隔,记录警告
                        if processing_time > frame_interval * 2:  # 超过100%
                            print(f"性能警告: 帧处理时间({processing_time:.3f}s)严重超时")
                            self.error_count += 1
 
                    # 定期重置错误计数
                    if self.frame_count % 1000 == 0:
                        self.error_count = max(0, self.error_count - 5)
                        
                except Exception as e:
                    self.error_count += 1
                    print(f"录制错误 #{self.error_count}: {e}")
                    # time.sleep(1)  # 错误后暂停1秒
                    
                if self.error_count >= self.max_errors:
                    self.root.after(0, lambda: self.stop_recording())
                    # self.stop_recording()
                    break
            else:
                # 暂停时适当睡眠
                time.sleep(0.1)
 
    def start_health_monitor(self):
        """监控录制健康状态"""
        if self.recording:
            try:
                current_time = time.time()
                
                # 检查帧率健康度
                if self.last_health_check:
                    time_elapsed = current_time - self.last_health_check
                    expected_frames = time_elapsed * int(self.fps_var.get())
                    actual_frames = self.frame_count - self.last_frame_count
                    
                    if expected_frames > 10 and actual_frames < expected_frames * 0.5:  # 丢帧超过50%
                        print(f"健康警告: 严重丢帧,预期{expected_frames:.0f}帧,实际{actual_frames}帧")
                        self.error_count += 1
                
                self.last_health_check = current_time
                self.last_frame_count = self.frame_count
                
                # 每30秒检查一次
                self.root.after(30000, self.start_health_monitor)
            except Exception as e:
                print(f"健康监控错误: {e}")
                # 继续监控
                self.root.after(30000, self.start_health_monitor)
 
    def update_preview(self):
        """更新预览图像"""
        if self.recording and not self.paused:
            try:
                # 截取屏幕用于预览
                frame = self.capture_screen()
 
                # 调整预览图像大小
                preview_size = (534, 246)
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                pil_image = Image.fromarray(frame_rgb)
                pil_image.thumbnail(preview_size) #, Image.Resampling.LANCZOS)
 
                # 转换为Tkinter图像
                preview_image = ImageTk.PhotoImage(pil_image)
                w = preview_image.width()
                h = preview_image.height()
 
                # 更新预览标签
                self.preview_label.config(image=preview_image, text="", width=w, height=h, anchor="center", justify=tk.CENTER)
                self.preview_label.image = preview_image  # 保持引用
                self.preview_label.grid(row=0, column=0, padx=(preview_size[0]-w)//2, pady=(preview_size[1]-h)//2)
 
            except Exception as e:
                print(f"预览更新失败: {e}")
 
            # 计算下一帧预览
            self.root.after(100, self.update_preview)
        elif self.recording and self.paused:
            # 如果暂停,继续检查状态
            self.root.after(100, self.update_preview)
            
    def open_output_folder(self):
        """打开输出文件所在的文件夹"""
        video_path = self.path_var.get()
        if video_path and os.path.exists(video_path):
            folder_path = os.path.dirname(video_path)
            os.startfile(folder_path)  # Windows
            # 对于Mac: os.system(f'open "{folder_path}"')
            # 对于Linux: os.system(f'xdg-open "{folder_path}"')
 
def main():
    root = tk.Tk()
    app = ScreenRecorder(root)
    root.mainloop()
 
if __name__ == "__main__":
    main()

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...