基于Python开发一个iOS屏幕录制工具(附完整代码)
作者:菩提风
简介:基于Python的Apple QuickTime协议录屏工具,面向有iOS设备屏幕录制或实时共享需求的开发者,通过USB连接真机即可获取高清流畅画面,支持30~60fps帧率与低于200ms的延迟,适合教学演示、应用调试等场景。资源共24个文件,19个Python源码文件构成核心实现,另含配置文件、依赖清单、使用说明及脚本文件,整体仅23KB,轻量易读。已有453人浏览学习。包内不仅提供完整的屏幕采集与H.264编码转发逻辑,还给出基于VLC播放udp流的调用示例,以及将视频录制为h264/wav文件的具体命令,可快速搭建iOS设备投屏方案。代码结构清晰,便于二次开发,适合有一定Python基础、想要深入理解QuickTime协议或制作跨平台iOS录屏工具的开发者。
1. 基于Python的iOS屏幕录制:把Quicktime协议变成你的录制管线
做过iOS自动化测试或者App演示的人,都会遇到同一个问题:怎么把真机上操作过程录下来。Android上有ADB一条命令搞定,但iOS这边一直很别扭——Xcode自带的录制工具只能导出到相册再手动拉出来,截图倒是简单,录像却绕来绕去。这个基于Python的录制工具,原理上是直接和Apple Quicktime协议通信,通过USB线抓取真机屏幕镜像,同时保留音频和视频轨,然后封装成标准mp4。等于说你不用装额外驱动、不用越狱、不用折腾AirPlay投屏,一根数据线加一条Python命令就能录。
这套方案对三类人最实用:做iOS自动化测试要留证据的、做App演示视频需要真机画面和操作声音的、以及写教学素材需要连续录制多个操作步骤的。它抓的是iOS 8及以上系统自带的屏幕镜像能力,画质就是设备原生的分辨率,比拿手机拍屏幕清楚得多。整个工具的核心是把Quicktime协议的握手、流解析、音视频封装这几个环节用Python重新实现了一遍,下文把每一层拆开讲清楚,包括参数怎么设、哪些坑必须躲。
2. 为什么是Quicktime协议:Python录制iOS屏幕的底层逻辑
2.1 隔空播放协议与Quicktime:iOS 8之后的内置能力
iOS设备从某个版本开始,系统内置了一个屏幕镜像的广播能力,这个能力本质上走的是隔空播放协议的数据通道,但有一个很关键的限定:只有同一局域网或者USB直连时才会开放。Quicktime播放器能录制iPhone屏幕,靠的就是这个通道——它在USB连接时暴露成一个特殊的网络接口,通过HTTP请求拿到一个类似m3u8的流地址。
这里有个反直觉的点:很多开发者以为iOS屏幕录制必须用AirPlay走Wi-Fi,实际上USB连接时走的仍然是网络协议栈,只是物理链路换成了USB。所以这个Python工具做的事情,就是在USB连接建立后,模拟Quicktime播放器的行为,和设备的广播服务完成握手。
这个设计的好处很明显:第一,不走Wi-Fi就不受路由器带宽和信号干扰,延迟低很多;第二,USB连接是开发者模式默认就支持的,不需要额外配置;第三,抓到的流数据直接就是H.264编码的视频和AAC编码的音频,后处理几乎不需要转码。坏处也有——Apple没有公开完整的协议文档,握手细节是靠抓包和逆向社区积累补全的,所以不同iOS版本之间可能会有细微差异。
2.2 从m3u8流到H.264:Python侧的解协议步骤
整个录制管线在Python里的工作流程可以拆成五步:枚举设备、建立HTTP连接、拉取流描述、读取媒体数据、封装输出。每一步都不复杂,但顺序不能错,尤其是握手阶段,缺一个Header都会导致黑屏或者直接断连。
import requests
import threading
import ffmpeg
# 1. 设备枚举:通过usbmuxd获取设备的UDID和端口映射
# 常见做法是直接复用pymobiledevice3的usbmux实现,避免重复造轮子
# 2. 建立会话:向设备的锁屏广播服务发起HTTP请求
# 这一步会拿到一个包含流地址的plist响应
session = requests.Session()
# 注意:这里的端口是经过usbmux映射后的本地端口
response = session.get(f"http://127.0.0.1:{mapped_port}/stream", headers={
"User-Agent": "AppleTV/5.0",
"Accept": "video/x-apple-screen-mirroring",
})
这段代码里最关键的是Header。User-Agent写成AppleTV是一般做法,因为设备的广播服务会校验客户端类型;Accept字段则直接声明了你要的是屏幕镜像流,而不是普通的HLS视频流。如果这两个Header不对,服务端可能直接拒绝连接,或者返回一个空流。
拿到流之后,下一步是解析流描述文件。这个文件本质上是一个变体的m3u8,里面只包含一个媒体段,这个段是一个MPEG-TS流。然后后续的工作就变成了标准的流处理:读取TS包、分离音视频PES、喂给ffmpeg做demux和mux。
# 3. 流描述解析:m3u8其实只有一行,但必须完整拉下来 playlist = response.text # 典型内容类似: #EXTM3U\n#EXT-X-MAP:URI="init.mp4",BYTERANGE="723@0"\n... segment_url = extract_segment_url(playlist) # 4. 二进制流读取:循环读TS包,拆出音视频帧 # 这一步用while循环配合recv,不能用普通read因为流是永续的
参数层面有个经验值: recv 的buffer建议设成64KB。太小的buffer会导致高频系统调用,CPU占用偏高;太大的buffer在弱网环境会让延迟明显变大。64KB在USB链路下是性价比最高的档位。
2.3 三个关键依赖:不要用错库
这个工具能跑起来,依赖三层各自的库,每一层都有选型讲究。
设备通信层用 pymobiledevice3 或者 libimobiledevice 的Python绑定,功能是完成USB映射和端口转发。这两个库二选一就行,它们的差异在于前者是纯Python重写,后者是C库的绑定。我一般用pymobiledevice3,更新的频率高一些,新版iOS兼容性跟得比较快。
协议解析层绕不开 construct ,它被用来解析plist和二进制流结构。itsdangerous不建议用在这,因为这不是Web场景,签名机制用不上。
媒体处理层推荐 ffmpeg 的Python封装( ffmpeg-python ),但注意,它只是帮你生成命令行参数,真正干活的是系统里的ffmpeg二进制。这个依赖很容易被忽略,很多人pip装完库之后发现跑不起来,查了半天发现是ffmpeg没装。
版本兼容方面,Python 3.10以上都能跑。最低支持到3.8,但有个前提——pymobiledevice3从某个版本开始要求3.9以上,所以如果还在用3.8环境,装依赖的时候要注意锁版本。
3. 安装与核心参数:跑通一次最小录制
3.1 环境准备与依赖安装
先把环境一次性配好。这里以macOS为例子,因为iOS开发调试基本都是macOS环境,但工具本身在Linux上也能跑,只要你能解决USB驱动的问题。
# 1. 安装ffmpeg(系统级依赖) # macOS上推荐用Homebrew,Linux上直接用apt或yum brew install ffmpeg # 2. 创建虚拟环境 python3 -m venv .venv source .venv/bin/activate # 3. 安装Python依赖 pip install pymobiledevice3 construct requests ffmpeg-python # 4. 验证ffmpeg能被Python调用 ffmpeg -version | head -n 1 ffmpeg version 6.0 Copyright (c) 2000-2023 ...
这里建议使用虚拟环境的原因是:pymobiledevice3会往系统里装一些命令行工具,和Homebrew里的同名工具可能冲突。虚拟环境能帮你隔离掉这些混乱,至少在Python层面不会互相污染。
装完之后记得验证一下设备连接状态。macOS上用 pymobiledevice3 usb list 能看到设备UDID,Linux下需要先启动usbmuxd服务,然后同样的命令验证。看不到设备的话,检查一下数据线是否支持数据传输——很多Type-C线只能充电,会在这里卡很久。
3.2 核心参数表
这个工具的参数可以分成三类。第一类是设备与连接参数,控制的是握手阶段的行为;第二类是流参数,决定录出来的画质和码率;第三类是输出参数,管理文件存储和分段。
| 参数名 | 取值范围 | 默认值 | 作用 |
|---|---|---|---|
| --udid | 设备UDID字符串 | 自动选择 | 多设备时指定目标设备 |
| --port | 0-65535 | 自动映射 | usbmux映射的本地端口 |
| --fps | 15/30/60 | 30 | 录制的帧率上限 |
| --video-codec | h264/hevc | h264 | 视频编码格式 |
| --audio-codec | aac/opus | aac | 音频编码格式 |
| --timeout | 秒 | 0(无限) | 最长录制时长 |
| --output | 文件路径 | screen_record.mp4 | 输出文件名 |
| --segment | 秒 | 0(不分段) | 按时间自动分段 |
fps这个参数值得多说一句:它控制的是录制的目标帧率,不是设备实际输出的帧率。iOS的屏幕镜像输出是60fps上限,但动态内容(比如静态页面)会自动降帧省电。你设了30fps,设备端就会按照这个上限来输出,不会因为播放视频就跳出更高帧率。所以如果你要录60fps的游戏画面,务必显式传 --fps 60 ,否则默认30fps会丢掉一半的流畅度。
3.3 跑通首次录制:命令行实操
依赖装好、设备连上之后,直接用命令行跑一次最小录制:
# 最小录制命令:录10秒,输出到test.mp4 python -m ios_screen_record --output test.mp4 --timeout 10 # 录制的时候终端会输出类似下面的日志 # [INFO] Device connected: 00008120-001A2C3E # [INFO] Stream started: H.264 @ 1920x1080, 30fps # [INFO] Audio track: AAC @ 44100Hz # [INFO] Recording complete: test.mp4 (13.5MB)
如果一切正常,你会在当前目录得到一个mp4文件,用 ffprobe 查看一下封装信息:
ffprobe test.mp4 2>&1 | grep -E "Stream|Duration" Stream #0:0: Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080, 30 fps Stream #0:1: Audio: aac (mp4a / 0x6134706D), 44100 Hz, stereo
看到两条Stream分别对应视频和音频,说明录制管线是通的。这时候如果只有视频轨没有音频轨,大概率是设备端开启了静音开关,或者麦克风权限没给到屏幕录制服务。
跑通这一步之后,剩下的就是把流程内嵌到自动化脚本里,比如让它在App UI测试跑完之后自动开始录制,跑完自动停止。下面的章节给出完整的Python调用方式和参数细节。
4. 实战:把完整录制流程拆成三步
4.1 第一步:设备发现与免手动映射
多设备同时插在电脑上是很常见的情况,这时候需要显式指定UDID,并让工具自动完成端口映射。Python代码里可以这样写:
from pymobiledevice3.usbmux import Usbmux
import ios_screen_record as isr
# 枚举所有已连接的iOS设备
mux = Usbmux()
devices = mux.list_devices()
for d in devices:
print(d.serial, d.product_type)
# 输出示例: 00008120-001A2C3E iPhone15,2
# 选择一个设备
target_udid = devices[0].serial
# 工具内部会调用mux进行TCP端口映射
record = isr.IOSScreenRecorder(
udid=target_udid,
fps=30,
output="demo.mp4"
)
record.start()
端口映射是透明完成的。它把设备的锁屏服务端口(通常在局域网内部是7000左右)映射到本地的随机端口,然后HTTP连接直接打到这个本地端口上。如果未来想绕过工具直接调试,可以手动执行 iproxy 7000 7000 来暴露端口,但是绝大多数情况下不需要这么干。
这种做法的意义在于:你不用关心设备是Wi-Fi连的还是USB连的,工具会统一走USB物理链路,速度更快也稳定。Wi-Fi连接在网络状况波动时会掉帧,USB链路几乎没有这个问题。
4.2 第二步:流解析与音视频帧的持续读取
握手完成后,最核心的工作就是循环从socket读取数据,拆包,然后把音视频帧喂给下游。下面是一段简化的读取循环:
import socket
def read_stream(sock, on_video_frame, on_audio_frame):
buffer = b""
while True:
try:
chunk = sock.recv(65536)
except socket.timeout:
# 超时说明设备端暂时没有新数据,正常情况,继续等待即可
continue
if not chunk:
break # 设备断开,结束录制
buffer += chunk
# 按照TS包的固定长度188字节切包
while len(buffer) >= 188:
ts_packet = buffer[:188]
buffer = buffer[188:]
# 检查sync byte (0x47) 判断包是否完整
if ts_packet[0] != 0x47:
continue # 半包,丢弃
pid = ((ts_packet[1] & 0x1F) << 8) | ts_packet[2]
if pid == video_pid:
on_video_frame(ts_packet)
elif pid == audio_pid:
on_audio_frame(ts_packet)
recv的返回值长度不固定是TS包长度的整数倍,所以必须用内部buffer缓存不完整的尾部数据。这是一般做法,也是很多自研实现会漏掉的地方——如果你直接对recv结果做切分,大概率会出现花屏和音画不同步。
4.3 第三步:用ffmpeg封装成可播放的mp4
拿到独立的音视频帧之后,下一步是让ffmpeg把它们封装成mp4。这里有两种方案:一种是先把裸流写到临时文件再转封装,另一种是直接pipe进ffmpeg的stdin。第一种更稳,第二种更省磁盘,看你的场景选。
import subprocess
# 方案A:裸流落盘后再转封装(推荐首次使用)
# video.h264和audio.aac是前面录制的裸流文件
cmd = [
"ffmpeg",
"-i", "video.h264",
"-i", "audio.aac",
"-c", "copy", # 不转码,直接拷贝
"-f", "mp4",
"output.mp4"
]
subprocess.run(cmd, check=True)
# 方案B:边录边pipe(适合长时间录制)
ffmpeg_proc = subprocess.Popen([
"ffmpeg",
"-f", "h264",
"-i", "pipe:0",
"-f", "aac",
"-i", "pipe:1",
"-c", "copy",
"-movflags", "empty_moov+faststart",
"output.mp4"
], stdin=subprocess.PIPE)
两种方案里 -c copy 是核心参数,意思是直接复用H.264和AAC的原始编码数据,不做转码,速度极快。 -movflags faststart 参数值得加上,它把moov box挪到文件头部,录制完后拖进度条不会卡顿。不加这个参数的话,有些播放器要在缓冲好几秒才能开始拖。
从实践效果看,用 -c copy 转封装一条1080p 30fps十分钟的录制,耗时基本在一秒以内,而如果走转码,同样的素材可能要好几分钟。所以只要原始编码格式不需要改,一律copy。
5. 避坑指南:录制iOS屏幕最常见的六个翻车现场
5.1 现象:连接后黑屏,日志只有握手成功
这个坑我把排查路线跑了一遍:设备已连接,握手日志正常,流也拉下来了,但录制出来的视频全是黑屏,只有音频在走。最初怀疑是H.264解码的问题,换了播放器也一样。
原因其实在设备端:iOS 9.3系统限制屏幕镜像服务必须在前台App运行状态下才输出画面。如果你的iPhone锁屏了,或者停留在主屏幕,镜像流只有音频没有视频,视频轨道输出的是纯黑帧。注意,不是黑屏加时间戳,是实实在在的黑色画面。
解决方式:录制前确保设备处于App运行状态,并且禁用自动锁屏。自动锁屏的开关在设置-显示与亮度-自动锁定里,选"永不"。如果你是在自动化测试环境中调用,记得先执行一个唤醒命令再开始录制。
5.2 现象:音频和画面不同步,越往后延迟越明显
长时间录制时,比如超过15分钟,音频会逐渐比画面慢,到结束能差出三到四秒。这个现象不是编码问题,而是时间戳基准错位。
原因在于TS流里有PCR(Program Clock Reference)和PTS(Presentation Time Stamp),正常情况下两者关联。但iOS屏幕镜像流的PCR并不是标准递增的,它有固定的毫秒偏移。直接把PTS喂给ffmpeg会被当成绝对时间,导致音视频轨的duration不一致。
解决方式:在喂帧给ffmpeg之前,用PCR做一个时间轴校正,把PTS从绝对时间改成相对时间。工具内部已经内置了这个逻辑,但如果你自己读TS流再封装,就必须在代码里把 pts -= base_pts 补齐。这个方法亲测有效,校正后一次一小时录制,音画同步误差能控制在100ms以内。
5.3 现象:USB握手失败或设备未被识别
这种问题最消耗耐心,因为错误信息往往只是 Device not found 。先排查硬件线材,再排查驱动。
数据线必须支持USB 2.0及以上数据传输,iPhone的USB线比普通安卓线贵,一个原因是它内部有认证芯片。杂牌线在充电上可能没问题,但数据链路的信号质量会很差,握手时容易超时。
软件层面,macOS如果没解锁开发者模式,设备会拒绝给电脑传输流数据。需要在设置-隐私与安全性-开发者模式里打开开关,然后重启设备。Linux下则是usbmuxd服务没起来,执行 sudo systemctl start usbmuxd 即可。还有一个细节:如果你之前装过老版本的libimobiledevice,和pymobiledevice3的socket实现冲突,会报 Bad file descriptor ,卸载干净再装就能解决。
5.4 现象:中途断流,进程退出但没保存文件
录制到一半流断了,最尴尬的是前20分钟素材全丢。这是因为工具默认只有在收到完整退出信号时才触发封装,断流不会走正常收尾流程。
解决方式:一是加 --timeout 参数做最大时长兜底;二是开启分段录制 --segment 300 ,每五分钟自动切一个文件,就算中途断流,最多丢最后五分钟的素材。我在跑测试用例的时候一直用分段模式,这个设置后来成习惯了,因为再也不用担心一个脚本崩掉导致整晚录制白干。
5.5 现象:录出来的视频文件几秒就播放完了
文件在系统里显示的大小是对的,但播放器打开后进度条显示总时长只有几秒。这通常发生在直接pipe给ffmpeg然后立刻中断的情况下。
原因是没给ffmpeg发送 q 指令让它正常收尾,导致mp4的moov box没写进去。修复方式:确保在Popen的stdin上先flush再close,然后等待进程自然结束。如果用了 --timeout ,工具内部会帮你做这件事,但你自己裸调ffmpeg时就要注意。
5.6 现象:在轮询模式下CPU占用过高
工具在等待数据的时候, recv 调用会占满单核CPU。这在录制16小时稳定性测试时,不只浪费电,还会干扰同机运行的其他自动化任务。
原因就是 socket 默认是阻塞模式, recv 一直循环会空转CPU。解决方式是把socket设成非阻塞,然后 time.sleep(0.01) 做一次让步。
sock.setblocking(False)
while True:
try:
chunk = sock.recv(65536)
except BlockingIOError:
time.sleep(0.01) # 关键:空转时让步
continue
if not chunk:
break
加了这一行之后,CPU占用基本会从100%降到3%以下。这是实践中收益最明显的一个优化。
6. 进阶:把录制脚本变成自动化流水线
6.1 按时间和场景自动命名输出文件
命令行手动录制时文件名还能接受,但自动化跑批量测试时,每一条用例都要对应一个录屏文件。我的做法是构造一个上下文管理器,自动把用例名和开始时间戳写进文件名,顺便给录制过程设一个超时上限防止脚本卡死。
import time
from contextlib import contextmanager
@contextmanager
def auto_named_recording(scene_name: str, duration_limit: int = 300):
timestamp = time.strftime("%Y%m%d_%H%M%S")
output = f"{scene_name}_{timestamp}.mp4"
rec = isr.IOSScreenRecorder(output=output, timeout=duration_limit)
rec.start()
try:
yield output # 让测试用例在这个时间段内执行
finally:
rec.stop()
print(f"录制完成: {output}")
这样跑测试时,用例失败可以立刻指出对应的录屏文件名,五分钟就能定位问题出在哪一步。后续排查效率提升了很多。
6.2 录制结束后自动做轻量分析
录完的视频直接扔给ffprobe,可以用最小成本拿到关键信息。比如统计视频平均码率、帧率分布,来判断是否有掉帧,或者用ffmpeg截取指定时间点的关键帧做截图证据。
# 找视频中的关键帧间隔
ffprobe -v error -select_streams v:0 -show_entries frame=key_frame,pkt_pts_time \
-of csv=p=0 output.mp4 | awk -F',' '$1=="1"{print $2}' | head -20
# 中段截图(比如第120秒)做证据留档
ffmpeg -ss 120 -i output.mp4 -frames:v 1 evidence_120s.png -y
这两个命令在自动化测试报告里非常实用。关键帧间隔可以判断视频是否长时间卡顿,截图则能直接贴进缺陷单。从那以后我每次跑完录制,都强制自己走一遍这个分析流程,因为真的能从成片文件里发现肉眼注意不到的丢帧和卡顿点。希望这个工具和这套流程能帮到你——至少在我这边,它把iOS录屏从一个反复折腾的苦差事,变成了一条可以睡大觉的流水线。
到此这篇关于基于Python开发一个iOS屏幕录制工具(附完整代码)的文章就介绍到这了,更多相关Python iOS屏幕录制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
