复制代码
import os
import sys
import time
import ctypes # 系统交互
import threading #进程
import subprocess #调用
from functools import wraps #装饰
import signal # 用于处理信号
# 基础参数
FFMPEG_BIN_DIR = r"../bin" # FFmpeg所在目录
save_path = "video" # 保存文件地址
# ************************** 1. 强制以管理员身份运行(Windows)**************************
def is_admin():
"""判断当前进程是否有管理员权限"""
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
def run_as_admin():
"""重新以管理员身份启动当前脚本"""
if not is_admin():
# 重新启动脚本并请求管理员权限
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1
)
sys.exit(0)
# 强制管理员运行(移到最前面,确保后续操作有权限)
run_as_admin()
# ************************** 2. 手动指定FFmpeg的路径 **************************
ffmpeg_exe = os.path.join(FFMPEG_BIN_DIR, "ffmpeg.exe") # FFmpeg可执行文件路径
# 将FFmpeg路径加入环境变量(可选,确保能直接调用)
os.environ["PATH"] = FFMPEG_BIN_DIR + os.pathsep + os.environ["PATH"]
# 验证FFmpeg路径
if not os.path.exists(ffmpeg_exe):
print(f"错误:未找到ffmpeg.exe,请检查路径:{ffmpeg_exe}")
sys.exit(1)
# 全局变量
recording_process = None # 全局存储FFmpeg进程
recording_thread = None # 录制线程
stop_recording = False # 停止录制标志
business_thread = None # 主线程业务逻辑线程
def fix_mp4_duration(input_file, output_file):
"""修复MP4文件的时长信息:通过FFmpeg命令行重新封装为标准MP4"""
try:
print(f"\n正在修复视频时长信息:{input_file} -> {output_file}")
# 构造FFmpeg修复命令行参数
ffmpeg_cmd = [
ffmpeg_exe,
"-i", input_file, # 输入文件
"-c", "copy", # 直接复制流,不重新编码(保留VBR的比特率特征)
"-movflags", "faststart", # 将moov原子移到文件开头
"-y", # 覆盖输出文件
output_file # 输出文件
]
# 执行修复命令,静默运行
subprocess.run(
ffmpeg_cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
)
# 删除原碎片化文件,保留修复后的文件
os.remove(input_file)
print(f"时长修复完成,最终文件:{output_file}")
except subprocess.CalledProcessError as e:
print(f"\n修复时长失败:FFmpeg执行返回错误码 {e.returncode}")
except Exception as e:
print(f"\n修复时长时发生错误:{str(e)}")
def record_screen():
"""录制屏幕的主函数(保留VBR+解决时长偏差)"""
global recording_process, stop_recording,save_path
# 统一路径处理
temp_file = save_path + "_temp.mp4"
output_file = save_path + ".mp4"
framerate = 15
# ---------------------- VBR核心参数(完全保留,可根据需求调整)----------------------
video_bitrate = "1000k" # VBR目标比特率
video_maxrate = "1500k" # VBR最大比特率(上限)
video_bufsize = "5000k" # VBR缓冲区大小(决定比特率波动范围)
# ---------------------------------------------------------------------------------
try:
# 构造FFmpeg录屏命令行参数(核心:保留VBR + 强制恒定帧率 + 移除空音频源)
ffmpeg_cmd = [
ffmpeg_exe,
# 视频源参数(gdigrab抓取桌面)
"-f", "gdigrab", # 格式:Windows桌面抓取
"-framerate", str(framerate), # 输入帧率(抓取帧率)
"-thread_queue_size", "1024", # 队列大小
"-probesize", "10M", # 探测缓冲区
"-draw_mouse", "1", # 显示鼠标
"-i", "desktop", # 输入:整个桌面
# ---------------------- VBR相关编码参数(核心保留)----------------------
"-c:v", "libx264", # x264编码器完美支持VBR
"-preset", "fast", # 编码速度/质量预设(不影响VBR)
"-b:v", video_bitrate, # VBR目标比特率
"-maxrate", video_maxrate, # VBR最大比特率上限
"-bufsize", video_bufsize, # VBR缓冲区(关键)
# ----------------------------------------------------------------------
# 关键:强制恒定帧率+同步策略(解决时长偏差的核心,与VBR兼容)
"-r", str(framerate), # 输出帧率强制等于输入帧率
"-vsync", "cfr", # 恒定帧率模式:保证时间轴与实际时长一致
# 输出格式参数(碎片化,确保中断后可播放)
"-movflags", "+frag_keyframe+empty_moov", # 碎片化参数
"-max_muxing_queue_size", "1024", # 混流队列大小
"-pix_fmt", "yuv420p", # 颜色空间(兼容x264)
"-y", # 覆盖已存在的文件
temp_file # 临时输出文件
]
# 打印FFmpeg命令行(调试用,可查看VBR参数是否生效)
print(f"\nFFmpeg执行命令:{' '.join(ffmpeg_cmd)}")
# 启动FFmpeg录屏进程
recording_process = subprocess.Popen(
ffmpeg_cmd,
stdout=subprocess.DEVNULL, # 丢弃标准输出
stderr=subprocess.DEVNULL, # 丢弃标准错误
stdin=subprocess.PIPE,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP # Windows下创建新进程组
)
print("开始录制屏幕...")
# 等待录制进程结束或收到停止信号(优化:减少sleep时间,提高响应速度)
while recording_process.poll() is None and not stop_recording:
time.sleep(0.2) # 从0.5秒改为0.2秒,减少检查延迟
# 优雅停止FFmpeg进程(优化:先尝试发送q,再用信号,减少超时)
if stop_recording and recording_process.poll() is None:
print("\n正在停止录制并保存视频...")
try:
# 第一步:尝试发送q信号(FFmpeg的标准停止指令),非阻塞
recording_process.stdin.write(b'q\n')
recording_process.stdin.flush()
time.sleep(0.5) # 给FFmpeg一点时间响应
# 第二步:等待进程退出,缩短超时时间到5秒
recording_process.wait(timeout=5)
except (subprocess.TimeoutExpired, BrokenPipeError):
# 超时或管道错误,再用信号强制停止
try:
if os.name == 'nt':
ctypes.windll.kernel32.GenerateConsoleCtrlEvent(0, recording_process.pid)
else:
recording_process.send_signal(signal.SIGINT)
recording_process.wait(timeout=2)
except:
print("FFmpeg未及时响应,强制终止...")
recording_process.terminate()
recording_process.wait()
except Exception as e:
print(f"停止FFmpeg时出错:{e}")
recording_process.terminate()
recording_process.wait()
except Exception as e:
print(f"\n录制过程中发生错误:{str(e)}")
finally:
# 等待一小段时间确保FFmpeg完全退出(缩短sleep时间)
time.sleep(0.5)
# 修复时长(如果临时文件存在,修复时保留VBR特征)
if os.path.exists(temp_file):
fix_mp4_duration(temp_file, output_file)
else:
print("未找到临时文件,录制可能未开始或已删除")
def stop_recording_thread():
"""停止录制线程"""
global stop_recording, recording_thread
stop_recording = True
if recording_thread and recording_thread.is_alive():
print("等待录制线程结束...")
recording_thread.join(timeout=10) # 从15秒改回10秒(优化后无需更长时间)
if recording_thread.is_alive():
print("警告:录制线程未在10秒内结束,强制结束")
else:
print("录制已完全停止")
recording_thread = None
def video_run():
"""屏幕录制装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
global recording_thread, business_thread
global stop_recording
stop_recording = False
# 启动录制线程
recording_thread = threading.Thread(target=record_screen,daemon=False)
recording_thread.start()
time.sleep(0.5) # 从1秒改为0.5秒,减少初始化延迟
# 定义业务逻辑执行函数
def run_business():
try:
func(*args, **kwargs)
except Exception as e:
print(f"\n主线程业务逻辑发生异常:{str(e)}")
# 启动业务逻辑线程
business_thread = threading.Thread(target=run_business, daemon=False)
business_thread.start()
# 等待业务逻辑结束
business_thread.join()
print("\n主线程业务逻辑执行结束,准备停止录制")
stop_recording_thread()
return wrapper
return decorator
# ************************** 主线程业务逻辑 **************************
@video_run() # 路径使用原始字符串
def main_business():
"""示例业务逻辑"""
print("主线程业务逻辑开始执行...")
time.sleep(10) # 模拟业务执行10秒
raise RuntimeError("业务逻辑出错了!") # 测试异常情况
print("主线程业务逻辑执行完成!")
# ************************** 程序入口 **************************
if __name__ == "__main__":
main_business()
print("主线程运行完毕,程序即将退出")
python
复制代码
import os
import sys
import time
import ctypes # 系统交互
import threading #进程
import subprocess #调用
from functools import wraps #装饰
import signal # 用于处理信号
# 基础参数
FFMPEG_BIN_DIR = r"../bin" # FFmpeg所在目录
save_path = "video" # 保存文件地址
# ************************** 1. 强制以管理员身份运行(Windows)**************************
def is_admin():
"""判断当前进程是否有管理员权限"""
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
def run_as_admin():
"""重新以管理员身份启动当前脚本"""
if not is_admin():
# 重新启动脚本并请求管理员权限
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1
)
sys.exit(0)
# 强制管理员运行(移到最前面,确保后续操作有权限)
run_as_admin()
# ************************** 2. 手动指定FFmpeg的路径 **************************
ffmpeg_exe = os.path.join(FFMPEG_BIN_DIR, "ffmpeg.exe") # FFmpeg可执行文件路径
# 将FFmpeg路径加入环境变量(可选,确保能直接调用)
os.environ["PATH"] = FFMPEG_BIN_DIR + os.pathsep + os.environ["PATH"]
# 验证FFmpeg路径
if not os.path.exists(ffmpeg_exe):
print(f"错误:未找到ffmpeg.exe,请检查路径:{ffmpeg_exe}")
sys.exit(1)
# 全局变量
recording_process = None # 全局存储FFmpeg进程
recording_thread = None # 录制线程
stop_recording = False # 停止录制标志
business_thread = None # 主线程业务逻辑线程
def fix_mp4_duration(input_file, output_file):
"""修复MP4文件的时长信息:通过FFmpeg命令行重新封装为标准MP4"""
try:
print(f"\n正在修复视频时长信息:{input_file} -> {output_file}")
# 构造FFmpeg修复命令行参数
ffmpeg_cmd = [
ffmpeg_exe,
"-i", input_file, # 输入文件
"-c", "copy", # 直接复制流,不重新编码(保留VBR的比特率特征)
"-movflags", "faststart", # 将moov原子移到文件开头
"-y", # 覆盖输出文件
output_file # 输出文件
]
# 执行修复命令,静默运行
subprocess.run(
ffmpeg_cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
)
# 删除原碎片化文件,保留修复后的文件
os.remove(input_file)
print(f"时长修复完成,最终文件:{output_file}")
except subprocess.CalledProcessError as e:
print(f"\n修复时长失败:FFmpeg执行返回错误码 {e.returncode}")
except Exception as e:
print(f"\n修复时长时发生错误:{str(e)}")
def record_screen():
"""录制屏幕的主函数(保留VBR+解决时长偏差)"""
global recording_process, stop_recording,save_path
# 统一路径处理
temp_file = save_path + "_temp.mp4"
output_file = save_path + ".mp4"
framerate = 15
# ---------------------- VBR核心参数(完全保留,可根据需求调整)----------------------
video_bitrate = "1000k" # VBR目标比特率
video_maxrate = "1500k" # VBR最大比特率(上限)
video_bufsize = "5000k" # VBR缓冲区大小(决定比特率波动范围)
# ---------------------------------------------------------------------------------
try:
# 构造FFmpeg录屏命令行参数(核心:保留VBR + 强制恒定帧率 + 移除空音频源)
ffmpeg_cmd = [
ffmpeg_exe,
# 视频源参数(gdigrab抓取桌面)
"-f", "gdigrab", # 格式:Windows桌面抓取
"-framerate", str(framerate), # 输入帧率(抓取帧率)
"-thread_queue_size", "1024", # 队列大小
"-probesize", "10M", # 探测缓冲区
"-draw_mouse", "1", # 显示鼠标
"-i", "desktop", # 输入:整个桌面
# ---------------------- VBR相关编码参数(核心保留)----------------------
"-c:v", "libx264", # x264编码器完美支持VBR
"-preset", "fast", # 编码速度/质量预设(不影响VBR)
"-b:v", video_bitrate, # VBR目标比特率
"-maxrate", video_maxrate, # VBR最大比特率上限
"-bufsize", video_bufsize, # VBR缓冲区(关键)
# ----------------------------------------------------------------------
# 关键:强制恒定帧率+同步策略(解决时长偏差的核心,与VBR兼容)
"-r", str(framerate), # 输出帧率强制等于输入帧率
"-vsync", "cfr", # 恒定帧率模式:保证时间轴与实际时长一致
# 输出格式参数(碎片化,确保中断后可播放)
"-movflags", "+frag_keyframe+empty_moov", # 碎片化参数
"-max_muxing_queue_size", "1024", # 混流队列大小
"-pix_fmt", "yuv420p", # 颜色空间(兼容x264)
"-y", # 覆盖已存在的文件
temp_file # 临时输出文件
]
# 打印FFmpeg命令行(调试用,可查看VBR参数是否生效)
print(f"\nFFmpeg执行命令:{' '.join(ffmpeg_cmd)}")
# 启动FFmpeg录屏进程
recording_process = subprocess.Popen(
ffmpeg_cmd,
stdout=subprocess.DEVNULL, # 丢弃标准输出
stderr=subprocess.DEVNULL, # 丢弃标准错误
stdin=subprocess.PIPE,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP # Windows下创建新进程组
)
print("开始录制屏幕...")
# 等待录制进程结束或收到停止信号(优化:减少sleep时间,提高响应速度)
while recording_process.poll() is None and not stop_recording:
time.sleep(0.2) # 从0.5秒改为0.2秒,减少检查延迟
# 优雅停止FFmpeg进程(优化:先尝试发送q,再用信号,减少超时)
if stop_recording and recording_process.poll() is None:
print("\n正在停止录制并保存视频...")
try:
# 第一步:尝试发送q信号(FFmpeg的标准停止指令),非阻塞
recording_process.stdin.write(b'q\n')
recording_process.stdin.flush()
time.sleep(0.5) # 给FFmpeg一点时间响应
# 第二步:等待进程退出,缩短超时时间到5秒
recording_process.wait(timeout=5)
except (subprocess.TimeoutExpired, BrokenPipeError):
# 超时或管道错误,再用信号强制停止
try:
if os.name == 'nt':
ctypes.windll.kernel32.GenerateConsoleCtrlEvent(0, recording_process.pid)
else:
recording_process.send_signal(signal.SIGINT)
recording_process.wait(timeout=2)
except:
print("FFmpeg未及时响应,强制终止...")
recording_process.terminate()
recording_process.wait()
except Exception as e:
print(f"停止FFmpeg时出错:{e}")
recording_process.terminate()
recording_process.wait()
except Exception as e:
print(f"\n录制过程中发生错误:{str(e)}")
finally:
# 等待一小段时间确保FFmpeg完全退出(缩短sleep时间)
time.sleep(0.5)
# 修复时长(如果临时文件存在,修复时保留VBR特征)
if os.path.exists(temp_file):
fix_mp4_duration(temp_file, output_file)
else:
print("未找到临时文件,录制可能未开始或已删除")
def stop_recording_thread():
"""停止录制线程"""
global stop_recording, recording_thread
stop_recording = True
if recording_thread and recording_thread.is_alive():
print("等待录制线程结束...")
recording_thread.join(timeout=10) # 从15秒改回10秒(优化后无需更长时间)
if recording_thread.is_alive():
print("警告:录制线程未在10秒内结束,强制结束")
else:
print("录制已完全停止")
recording_thread = None
def video_run():
"""屏幕录制装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
global recording_thread, business_thread
global stop_recording
stop_recording = False
# 启动录制线程
recording_thread = threading.Thread(target=record_screen,daemon=False)
recording_thread.start()
time.sleep(0.5) # 从1秒改为0.5秒,减少初始化延迟
# 定义业务逻辑执行函数
def run_business():
try:
func(*args, **kwargs)
except Exception as e:
print(f"\n主线程业务逻辑发生异常:{str(e)}")
# 启动业务逻辑线程
business_thread = threading.Thread(target=run_business, daemon=False)
business_thread.start()
# 等待业务逻辑结束
business_thread.join()
print("\n主线程业务逻辑执行结束,准备停止录制")
stop_recording_thread()
return wrapper
return decorator
# ************************** 主线程业务逻辑 **************************
@video_run() # 路径使用原始字符串
def main_business():
"""示例业务逻辑"""
print("主线程业务逻辑开始执行...")
time.sleep(10) # 模拟业务执行10秒
raise RuntimeError("业务逻辑出错了!") # 测试异常情况
print("主线程业务逻辑执行完成!")
# ************************** 程序入口 **************************
if __name__ == "__main__":
main_business()
print("主线程运行完毕,程序即将退出")