项目备份

This commit is contained in:
林觅
2026-04-09 10:37:51 +08:00
parent 7ed8d2dcb4
commit 4eab443148
32 changed files with 9468 additions and 0 deletions

View File

@@ -0,0 +1,385 @@
"""功能
python 获取window11电脑的屏幕的视频/音频流并保存为mp4格式文件
"""
"""安装核心依赖
& .venv/Scripts/python.exe -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade aiortc pywin32 numpy opencv-python sounddevice pycaw comtypes
"""
import os
import asyncio
import ctypes
import numpy as np
import win32gui
import win32ui
import win32con
import win32api
import sounddevice as sd
from aiortc import MediaStreamTrack, RTCPeerConnection
from aiortc.contrib.media import MediaRecorder
# -------------------------- 1. Windows 11高DPI适配 --------------------------
ctypes.windll.shcore.SetProcessDpiAwareness(2) # 解决高分屏采集偏移问题
# -------------------------- 2. 兼容VideoFrame导入 --------------------------
try:from aiortc.contrib.media import VideoFrame,AudioFrame
except ImportError:from aiortc import VideoFrame,AudioFrame
# -------------------------- 2. 屏幕视频采集轨道 --------------------------
class ScreenVideoTrack(MediaStreamTrack):
kind = "video"
def __init__(self, fps=30, scale_factor=1.0):
super().__init__()
self.fps = fps
self.scale_factor = scale_factor
self.stop_flag = False
self.pts = 0
self.frame_interval = 1.0 / fps
# 获取真实屏幕分辨率适配高DPI
self.user32 = ctypes.windll.user32
self.screen_width = self.user32.GetSystemMetrics(0)
self.screen_height = self.user32.GetSystemMetrics(1)
def capture_screen(self):
"""Windows 11原生API采集屏幕低延迟"""
left, top, width, height = 0, 0, self.screen_width, self.screen_height
# 1. 创建设备上下文DC
hdesktop = win32gui.GetDesktopWindow()
hwnd_dc = win32gui.GetWindowDC(hdesktop)
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
save_dc = mfc_dc.CreateCompatibleDC()
# 2. 复制屏幕内容到位图
save_bitmap = win32ui.CreateBitmap()
save_bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
save_dc.SelectObject(save_bitmap)
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (left, top), win32con.SRCCOPY)
# 3. 转换为numpy数组BGR格式
bmp_data = save_bitmap.GetBitmapBits(True)
frame = np.frombuffer(bmp_data, dtype=np.uint8).reshape((height, width, 4))
frame = frame[:, :, :3] # 去掉Alpha通道
frame = frame[:, :, ::-1] # BGRA → BGR适配aiortc
# 4. 缩放(可选,降低文件大小)
if self.scale_factor != 1.0:
new_width = int(width * self.scale_factor)
new_height = int(height * self.scale_factor)
frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
# 5. 释放资源(避免内存泄漏)
win32gui.DeleteObject(save_bitmap.GetHandle())
save_dc.DeleteDC()
mfc_dc.DeleteDC()
win32gui.ReleaseDC(hdesktop, hwnd_dc)
return frame
async def recv(self):
"""aiortc核心方法持续返回视频帧"""
if self.stop_flag:
raise StopAsyncIteration
# 控制帧率
await asyncio.sleep(self.frame_interval)
# 获取windows11 原始的视频帧
frame_data = self.capture_screen()
# 转换为aiortc VideoFrame
video_frame = self._convert_to_video_frame(frame_data)
video_frame.pts = int(self.pts)
video_frame.time_base = np.array([1, self.fps])
self.pts += 1
return video_frame
def _convert_to_video_frame(self, img):
"""将numpy数组转换为aiortc的VideoFrame"""
return VideoFrame.from_ndarray(img, format="bgr24")
def stop(self):
"""停止屏幕采集"""
self.stop_flag = True
# -------------------------- 3. 系统音频采集轨道 --------------------------
class SystemAudioTrack(MediaStreamTrack):
kind = "audio"
def __init__(self, sample_rate=48000, channels=2):
super().__init__()
self.sample_rate = sample_rate
self.channels = channels
self.stop_flag = False
self.pts = 0
self.audio_queue = asyncio.Queue(maxsize=10) # 音频数据队列
# 启动麦克风/系统音频采集(这里默认采集麦克风,系统音频见备注)
self._start_audio_capture()
def _start_audio_capture(self):
"""启动音频采集(麦克风)"""
def audio_callback(indata, frames, time, status):
if status or self.stop_flag:
return
# 将音频数据转为float32格式aiortc要求
audio_data = indata.astype(np.float32)
self.audio_queue.put_nowait(audio_data)
# 打开音频输入流
self.audio_stream = sd.InputStream(
samplerate=self.sample_rate,
channels=self.channels,
callback=audio_callback,
blocksize=1024
)
self.audio_stream.start()
async def recv(self):
"""aiortc核心方法持续返回音频帧"""
if self.stop_flag and self.audio_queue.empty():
raise StopAsyncIteration
# 从队列获取音频数据
audio_data = await self.audio_queue.get()
sample_count = len(audio_data)
# 转换为aiortc AudioFrame
audio_frame = AudioFrame(
samples=audio_data.T, # 转置为 (channels, samples)
sample_rate=self.sample_rate,
channels=self.channels
)
audio_frame.pts = int(self.pts)
audio_frame.time_base = np.array([1, self.sample_rate])
self.pts += sample_count
return audio_frame
def stop(self):
"""停止音频采集"""
self.stop_flag = True
self.audio_stream.stop()
self.audio_stream.close()
# -------------------------- 2. 工具函数枚举音频设备解决Error querying device -1错误 --------------------------
def list_audio_devices():
"""枚举所有音频设备输出ID和名称方便手动选择"""
print("📢 可用音频设备列表:")
devices = sd.query_devices()
for idx, dev in enumerate(devices):
print(f"ID {idx}: {dev['name']} | 输入通道:{dev['max_input_channels']} | 输出通道:{dev['max_output_channels']}")
return devices
def get_vb_cable_device_id():
"""查找VB-Cable虚拟音频设备ID系统声音采集必需"""
devices = sd.query_devices()
for idx, dev in enumerate(devices):
if "Cable" in dev['name'] and dev['max_input_channels'] > 0:
return idx
raise Exception("❌ 未找到VB-Cable虚拟音频设备\n 请先安装https://vb-audio.com/Cable/")
# -------------------------- 3. 系统声音录制为MP3 --------------------------
def record_system_audio_to_mp3(output_mp3="system_audio.mp3", duration=5, sample_rate=48000):
"""
录制系统声音为MP3需先安装VB-Cable并设置系统音频输出到VB-Cable
:param output_mp3: 输出MP3文件名
:param duration: 录制时长(秒)
:param sample_rate: 采样率
"""
# 修复device -1错误手动指定VB-Cable设备ID
vb_cable_id = get_vb_cable_device_id()
print(f"✅ 已找到VB-Cable设备ID{vb_cable_id}")
# 1. 录制音频为WAV先录WAV再转MP3避免编码问题
wav_file = "temp_audio.wav"
print(f"🎙️ 开始录制系统声音({duration}秒)...")
audio_data = sd.rec(
int(duration * sample_rate),
samplerate=sample_rate,
channels=2,
dtype='float32',
device=vb_cable_id # 关键指定有效设备ID解决-1错误
)
sd.wait() # 等待录制完成
# 2. 保存为WAV临时文件
with wave.open(wav_file, 'wb') as wf:
wf.setnchannels(2)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes((audio_data * 32767).astype(np.int16).tobytes())
# 3. WAV转MP3用OpenCV/ffmpeg无需额外依赖
print("🔄 转换WAV到MP3...")
os.system(f"ffmpeg -y -i {wav_file} -codec:a libmp3lame -b:a 192k {output_mp3}")
# 4. 删除临时WAV文件
if os.path.exists(wav_file):
os.remove(wav_file)
print(f"✅ 系统声音已保存:{output_mp3}")
# -------------------------- 4. 屏幕60帧/秒截图5秒并合成MP4 --------------------------
def capture_screen_60fps(output_dir="screen_shots", duration=5, fps=60):
"""
5秒内每秒60张屏幕截图共300张并合成高帧率MP4
:param output_dir: 截图保存目录
:param duration: 录制时长(秒)
:param fps: 帧率60帧/秒)
"""
# 1. 创建截图目录
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"🖥️ 开始屏幕截图({duration}秒,{fps}帧/秒)...")
# 2. 获取屏幕真实分辨率适配高DPI
user32 = ctypes.windll.user32
screen_width = user32.GetSystemMetrics(0)
screen_height = user32.GetSystemMetrics(1)
total_frames = duration * fps # 总帧数5*60=300帧
frame_interval = 1.0 / fps # 每帧间隔约16.67ms
# 3. 循环截图60帧/秒)
for frame_idx in range(total_frames):
# Windows原生API采集屏幕低延迟适配60帧
hdesktop = win32gui.GetDesktopWindow()
hwnd_dc = win32gui.GetWindowDC(hdesktop)
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
save_dc = mfc_dc.CreateCompatibleDC()
save_bitmap = win32ui.CreateBitmap()
save_bitmap.CreateCompatibleBitmap(mfc_dc, screen_width, screen_height)
save_dc.SelectObject(save_bitmap)
save_dc.BitBlt((0, 0), (screen_width, screen_height), mfc_dc, (0, 0), win32con.SRCCOPY)
# 转换为numpy数组并保存为PNG无损
bmp_data = save_bitmap.GetBitmapBits(True)
frame = np.frombuffer(bmp_data, dtype=np.uint8).reshape((screen_height, screen_width, 4))
frame = frame[:, :, :3][:, :, ::-1] # BGRA→BGR
# 保存截图命名frame_0001.png ~ frame_0300.png
frame_filename = os.path.join(output_dir, f"frame_{frame_idx+1:04d}.png")
cv2.imwrite(frame_filename, frame, [cv2.IMWRITE_PNG_COMPRESSION, 0])
# 释放资源
win32gui.DeleteObject(save_bitmap.GetHandle())
save_dc.DeleteDC()
mfc_dc.DeleteDC()
win32gui.ReleaseDC(hdesktop, hwnd_dc)
# 控制帧率确保每秒60帧
asyncio.run(
asyncio.sleep(frame_interval)
)
# 进度提示
if (frame_idx + 1) % 60 == 0:
print(f" 已截图:{frame_idx+1}/{total_frames}帧({int((frame_idx+1)/total_frames*100)}%")
# 4. 将截图合成60帧/秒的MP4
print("🔄 将截图合成60帧/秒MP4...")
output_mp4 = "screen_60fps.mp4"
# FFmpeg合成命令H.264编码60帧/秒)
ffmpeg_cmd = (
f'ffmpeg -y -framerate {fps} -i {output_dir}/frame_%04d.png '
f'-c:v libx264 -r {fps} -pix_fmt yuv420p {output_mp4}'
)
os.system(ffmpeg_cmd)
print(f"✅ 60帧屏幕视频已保存{output_mp4}")
print(f"📁 原始截图保存在:{output_dir}")
# -------------------------- 4. 主逻辑采集并保存为MP4 --------------------------
async def record_screen_to_mp4(output_file="screen_record.mp4", duration=10):
"""
录制屏幕+音频到MP4
:param output_file: 输出MP4文件名
:param duration: 录制时长(秒)
"""
# 1. 创建音视频轨道
video_track = ScreenVideoTrack(fps=30, scale_factor=0.8) # 0.8倍缩放,减小文件
audio_track = SystemAudioTrack(sample_rate=48000)
# 2. 创建MediaRecorder封装为MP4
recorder = MediaRecorder(
output_file,
format="mp4", # 指定输出格式
options={
"video_codec": "h264", # H.264编码,兼容性最好
"audio_codec": "aac", # AAC音频编码
"video_bitrate": "2000k" # 视频码率(可调整)
}
)
# 3. 添加音视频轨道到录制器
recorder.addTrack(video_track)
recorder.addTrack(audio_track)
# 4. 开始录制
print(f"✅ 开始录制屏幕,时长{duration}秒,输出文件:{output_file}")
await recorder.start()
# 5. 录制指定时长
await asyncio.sleep(duration)
# 6. 停止录制并清理资源
print("🔚 录制结束,正在保存文件...")
video_track.stop()
audio_track.stop()
await recorder.stop()
print(f"✅ 文件已保存:{output_file}")
# -------------------------- 5. 运行入口 --------------------------
if __name__ == "__main__":
import cv2 # 延迟导入,避免启动报错
# 列出所有 音频设备
all_audio_devices = list_audio_devices()
# 运行录制录制10秒输出screen_record.mp4
try:
# asyncio.run(record_screen_to_mp4(output_file="screen_record.mp4", duration=10))
# 第二步录制系统声音为MP35秒
if all_audio_devices:
record_system_audio_to_mp3(output_mp3="system_audio.mp3", duration=5)
# 第三步屏幕60帧/秒截图5秒并合成MP4
capture_screen_60fps(output_dir="screen_shots", duration=5, fps=60)
pass
except KeyboardInterrupt:
print("\n🛑 录制被手动终止")
except Exception as e:
print(f"❌ 录制出错:{str(e)}")
print("💡 排查步骤:")
print(" 1. 确认FFmpeg已添加到系统PATH")
print(" 2. 确认依赖包已安装pip install -r requirements.txt")
print(" 3. 以管理员身份运行脚本")
# -------------------------- 备注:系统音频采集(进阶) --------------------------
# 如需采集系统播放的音频而非麦克风需替换SystemAudioTrack为以下逻辑
# 1. 安装虚拟音频线VB-Cablehttps://vb-audio.com/Cable/
# 2. 将系统音频输出设置为VB-Cable
# 3. 将SystemAudioTrack的设备ID指定为VB-Cable的输入设备ID
# 示例:
# def _start_audio_capture(self):
# # 列出所有音频设备找到VB-Cable的ID
# devices = sd.query_devices()
# cable_id = None
# for idx, dev in enumerate(devices):
# if "Cable" in dev['name'] and dev['max_input_channels'] > 0:
# cable_id = idx
# break
# if cable_id is None:
# raise Exception("未找到VB-Cable虚拟音频设备")
# # 用VB-Cable ID启动采集
# self.audio_stream = sd.InputStream(
# device=cable_id,
# samplerate=self.sample_rate,
# channels=self.channels,
# callback=self.audio_callback,
# blocksize=1024
# )
# self.audio_stream.start()