简介:面向iOS开发与逆向工程人员的虚拟摄像头插件源码包,聚焦通过MobileSubstrate注入系统相机进程并Hook AVCaptureSession相关方法,实现视频流替换与自定义视频源接入。资源共5个文件,压缩包仅15KB,含2个HTML说明文档、1个JavaScript脚本及工程配置文件,结构精简,便于快速查看核心实现思路。核心代码基于AVCaptureVideoDataOutputSampleBufferDelegate协议完成视频帧替换,并演示SwiftUI摄像头应用中的摄像头切换、滤镜选择与视频源配置;视频选择部分使用PHPickerViewController选取本地视频,结合UISlider实现时长控制与裁剪导出。目前已有245人学习下载,适合具备一定iOS基础和逆向经验的开发者作为技术参考。 上周有个做直播工具的朋友跑来问我:iOS上能不能像电脑一样装个虚拟摄像头插件,把合成画面喂给微信视频或抖音直播?我第一反应是“想都别想”,但仔细捋了一遍iOS的摄像头采集链路和插件化机制后发现,这事得拆成三四个层级来看。被追问多了,索性把整个思路和能落地的代码方向整理成这篇东西,给同样在做iOS虚拟摄像头相关需求的人做个参考。
说清楚一点:这篇文章讲的不是“破解系统”那种黑魔法,而是基于iOS现有公开能力,把“虚拟摄像头”这个概念落到可运行的方案上。你会看到为什么iOS不像macOS那样有现成的DAL驱动机制,也会看到在不越狱、半越狱、App内自采集三种路径下,各自的代码写法和工程取舍。适合正在做iOS直播辅助、自动化测试、视频源模拟的开发者,也适合想搞清楚“iOS虚拟摄像头到底能不能做”的产品经理。
1. iOS摄像头服务链路与“虚拟”的三个层级
1.1 一条正常采集链路是怎么走的
要理解“虚拟摄像头”在iOS上意味着什么,先得看原生采集链路。iOS的摄像头采集大致分四层:
- 硬件层:物理摄像头传感器,通过ISP(图像信号处理器)输出RAW或YUV数据。
- 系统服务层:
mediaserverd、runningboardd等进程管理采集会话,摄像头权限由TCC(Transparency, Consent, and Control)框架管控。 - 框架层:
AVFoundation对上层提供AVCaptureDevice、AVCaptureSession等API,App在这里拿到视频流。 - 应用层:微信、抖音、直播SDK最终通过
AVCaptureVideoDataOutput或AVCaptureMovieFileOutput拿帧,做编码和推流。
注意一个关键点:iOS的摄像头服务是系统级单例,任何一个App请求摄像头时,系统会弹TCC授权框,授权后由mediaserverd分路输出。整个过程没有像Windows那样暴露“虚拟驱动”接口,也没有macOS的CoreMediaIO DAL插件机制。所以想在iOS上搞一个“系统全局都能看到的虚拟摄像头”,本质上是在挑战系统架构,而不是写一个普通插件。
1.2 “虚拟摄像头”的三个层级
基于上面的链路,我通常把“iOS虚拟摄像头”拆成三个层级:
- App内虚拟源:在你自己App的
AVCaptureSession里,用AVCaptureDeviceInput之外的source替换成合成画面。直播SDK、录制SDK、美颜SDK都能拿到这个“虚拟画面”。这是最干净、可控性最高、完全合规的做法。 - 系统级插件:通过越狱Tweak注入到
SpringBoard或mediaserverd,hook摄像头创建流程,把某个App的摄像头请求重定向到我们的虚拟内容。这个层级最接近“虚拟摄像头插件”这个标题,但门槛高、稳定性差、审核风险大。 - 远端/网络摄像头模拟:把另一台设备或PC上的视频流通过网络输送到iOS端,在App内解码后作为采集源。典型如Camo、EpocCam这类把iPhone当Mac摄像头的应用,反向来做也能实现iOS端接收虚拟流。
这篇博文主推第1层,因为第2层涉及越狱环境,第3层更多是网络传输方案。第2层我会讲清楚原理和hook点,但不会给你一份可以直接放在Cydia里跑的完整插件——那个东西调试起来很痛苦,而且不同iOS版本的偏移量和签名要求完全不同。
1.3 为什么iOS不能像桌面系统一样直接装驱动
桌面系统能虚拟摄像头,靠的是操作系统提供了驱动抽象层:Windows有DirectShow/Kernel Streaming,macOS有CoreMediaIO DAL。驱动开发者只要实现一套设备接口,系统就把它当成一个真实摄像头暴露给所有App。
iOS把这个口子焊死了。App Store审核规则不允许第三方安装系统级驱动,摄像头硬件的独占策略也是系统统一调度的。苹果这么做是从隐私和稳定性出发:一个App拿到的摄像头画面,理论上不应当被另一个App截获或篡改。所以“iOS虚拟摄像头插件”这个需求,第一关不是技术,而是系统机制。
2. 代码实操:用AVFoundation搭一个App内虚拟采集源
2.1 为什么这条路最值得先做
如果你是想在直播、视频会议、自动化测试场景里使用虚拟摄像头,App内自采集是性价比最高的方案。原因有三:不需要越狱,工程链路上只有Xcode签名;帧数据完全掌握在自己手里,可以叠加水印、更换背景、插入动画;代码量可控,核心逻辑大约两三百行就能跑通。
它的局限也很明确:只有你的App能看见这个虚拟画面。你不能让微信视频通话直接显示你的合成画面,因为微信跑在独立进程里,不会加载你写的采集代码。如果你确实需要“全局生效”,那就得跳到第3章的越狱方案。先把App内方案吃透,再谈系统级,这是我个人强烈建议的顺序。
2.2 核心代码骨架:自定义帧源替换摄像头输入
AVFoundation虽然不直接支持“虚拟设备”,但它有个很大的容忍度:你可以在AVCaptureVideoDataOutput的代理回调里拿到CMSampleBuffer,然后自由修改像素内容,再交给编码器。换句话说,你把摄像头帧拦截下来,做任意合成,再输出出去。这其实已经是事实上的“虚拟摄像头”了。
先看一个最基础的帧合成代码:
import AVFoundation import CoreVideo final class VirtualFrameFactory { private var pixelBufferPool: CVPixelBufferPool? func makeFrame(width: Int = 1280, height: Int = 720) -> CVPixelBuffer? { let attrs: [String: Any] = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, kCVPixelBufferWidthKey as String: width, kCVPixelBufferHeightKey as String: height, kCVPixelBufferPoolAllocationThresholdKey as String: 6 ] CVPixelBufferPoolCreate(nil, nil, attrs as CFDictionary, &pixelBufferPool) guard let pool = pixelBufferPool else { return nil } var pixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(nil, pool, &pixelBuffer) return pixelBuffer } func drawSolidColor(on buffer: CVPixelBuffer, color: (CGFloat, CGFloat, CGFloat)) { CVPixelBufferLockBaseAddress(buffer, []) defer { CVPixelBufferUnlockBaseAddress(buffer, []) } guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else { return } let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer) let width = CVPixelBufferGetWidth(buffer) let height = CVPixelBufferGetHeight(buffer) // 这里用CoreGraphics/CoreImage绘制到你想要的内容 let context = CGContext( data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue ) context?.setFillColor(red: color.0, green: color.1, blue: color.2, alpha: 1.0) context?.fill(CGRect(x: 0, y: 0, width: width, height: height)) } }这段代码做了三件事:创建CVPixelBufferPool复用缓冲区;从池里取一块像素缓冲;用CGContext往缓冲里画纯色。实际项目中你在这里画的不只是纯色,可以是用CoreImage叠加的实时摄像头+特效,也可以是从视频文件AVAssetReader读出的帧,或者是网络流解码后的画面。
关键点在于CVPixelBufferPoolCreate。我见过很多新手直接CVPixelBufferCreate一帧帧分配,结果内存暴涨、掉帧严重。用池化复用能显著降低性能损耗,这个在帧率要求高的场景是必须的。
2.3 把合成帧输送给上层录制或直播SDK
帧工厂准备好之后,下一步是替换摄像头采集输出。方法有两类:
第一类是直接在采集回调里改帧:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // 拿到原始帧 guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } // 用第2.2节的工具做合成 // 注意:如果只是加滤镜,可以直接用CoreImage处理pixelBuffer // 如果是完全替换画面,可以直接丢弃原始帧,用自定义pixelBuffer }第二类是不启动摄像头,完全用虚拟帧源。这适用于“视频源根本就不是摄像头”的场景,比如播放一段MP4当成摄像头画面。做法是不加AVCaptureDeviceInput,只加AVCaptureVideoDataOutput,然后通过AVAssetWriter或直播SDK的customVideoSource接口主动投递帧。
第二类方案有个适配问题:AVCaptureSession没有输入设备时,startRunning()会报错或不产生回调。所以工程上通常做法是:设备输入用前置/后置摄像头占位,同时在回调里用虚拟帧完全替换;或者直接用直播SDK的定制采集接口,不走AVCaptureSession。我用过的腾讯TRTC、声网Agora都支持customVideoSource,直接设一个视频源回调,把帧塞进去就行。
如果你的项目是接微信小程序或系统原生相机,那么只能用第一类:必须有一个真实摄像头在跑,否则系统不会给App激活采集链路。这在iOS模拟器上尤其明显,模拟器本身没有摄像头时,AVCaptureSession经常处于不可用状态。
3. 系统级路线:模拟器与越狱Tweak注入的真实边界
3.1 iOS模拟器上的摄像头模拟
Xcode的iOS模拟器其实有自己的一套“虚拟摄像头”机制,只不过它模拟的是“摄像头不可用”或“使用Mac摄像头”。模拟器不会给你一个配置界面去添加虚拟视频源,但你可以通过修改模拟器的AVCaptureDevice状态来调试部分逻辑。
实操中,如果模拟器代码里有AVCaptureDevice.authorizationStatus(for: .video)判断,模拟器通常会返回.authorized,但设备列表为空。所以测试时要先处理“没有摄像头”的分支,否则模拟器一启动就崩。
更进阶的做法是用simctl的io命令给模拟器注入图片作为摄像头画面?实测不可行,至少主流Xcode版本里没有公开的这个能力。有团队通过改模拟器运行时(SimulatorKit)来实现,但那属于深度逆向模拟器框架,投入产出比极低,不推荐。
3.2 越狱Tweak注入的工作原理与Hook点
要实现“微信也能看到虚拟摄像头”,必须走越狱Tweak注入。核心思路不是去写一个摄像头驱动,而是hook掉App请求摄像头时的关键方法,把返回的AVCaptureDevice替换成我们的虚拟数据源。
原理上分三步:
- 用Theos或其他工具链写一个dylib插件,注入到目标App进程。
- hook
AVCaptureDevice的defaultDevice(forDeviceType:mediaType:position:)或devices()类方法,让它返回一个我们自定义的“虚拟设备”。 - 这个虚拟设备通常不是真的AVCaptureDevice子类,而是通过
AVCaptureDeviceInput的init方法做替换,或者直接hookAVCaptureSession的startRunning,在采集回调里替换帧。
真实写起来会发现一堆问题:签名校验、偏移量适配、进程注入时机、App的越狱检测、get-task-allow权限等等。
我个人的态度是:如果只是为了自己调试或学习,可以试;如果想商用发布,不要碰。苹果对越狱环境的App审核是零容忍,而且iOS每个大版本的系统服务内部实现都变,你维护一套Tweak插件跨版本兼容的成本,远超它带来的收益。
3.3 给“系统级路线”的底线建议
如果你确实需要“全局虚拟摄像头”效果,又不愿意碰越狱,更现实的路线是走硬件采集卡或外部设备:比如用支持UVC协议的HDMI采集棒接iOS设备,再通过lightning转接头导入。这条路依赖硬件,但稳定、合规、不需要越狱,只是iOS对UVC外置摄像头的支持一直比较挑设备,不是所有采集卡都能被识别。
所以“iOS虚拟摄像头插件”这个标题,答案从来不是“能不能”,而是“你愿意接受哪层限制”。App内自采集是最推荐的正路,越狱Tweak是极客的游乐场,硬件方案是合规妥协。
4. 调试链路与签名配置:我踩过的四个坑
4.1 坑一:NSCameraUsageDescription缺失导致崩溃
这个问题看起来基础,但我在换新工程时经常忘。iOS 10之后,只要App调用AVCaptureDevice请求摄像头权限,Info.plist里必须有NSCameraUsageDescription,否则系统直接崩溃,连弹窗都不给你。
<key>NSCameraUsageDescription</key> <string>需要使用摄像头来提供虚拟视频源</string>注意,模拟器上请求摄像头权限时某些系统版本不会弹窗而是直接返回restricted,这是正常现象,不用怀疑代码写错了。
4.2 坑二:帧率与像素格式不匹配导致黑屏
如果你往AVCaptureVideoDataOutput里丢的像素缓冲格式和预期的videoSettings不一致,经常会出现“看起来在采集,但输出黑屏”的诡异问题。我遇到过最典型的是后台把像素格式设为kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,而我投递的是BGRA缓冲,导致部分编码器兼容性差。
统一做法是:主动设置videoSettings为[kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA],虚拟帧全部用BGRA生成,最后让编码器转换。色域和编码兼容性会好很多。
4.3 坑三:CVPixelBufferPool阻塞和过度分配
CVPixelBufferPoolCreatePixelBuffer在池满时会阻塞线程,这个坑在低端机型上特别明显。如果回调里同步生成帧,很容易拖垮采集线程。
解决办法有两个:一是把CVPixelBufferPoolAllocationThresholdKey调大,预分配更多缓冲;二是生成帧的操作放到串行队列异步执行,采集线程只做IO,不做合成。实测下来,用第二种方式帧率稳定性提升明显。
4.4 坑四:模拟器与真机的采集链差异
同一个App,在模拟器和真机上的采集状态完全不一致。模拟器上AVCaptureSession的isRunning经常是false,但不会走错误回调;真机上则正常。调试虚拟帧时,我建议直接真机调试,别在模拟器上浪费时间。
真机调试还需要注意签名方式:开发签名用get-task-allow,可以正常挂载Xcode调试器;分发签名则不行。你如果做Tweak插件调试,签名要求更复杂,这里不展开,但记住一点:Xcode的Debug包和Release包在虚拟帧调试时行为完全不同,一定要用Release配置多测一遍。
5. 项目回顾与适用边界
5.1 这套方案能做什么
用第二章节的App内虚拟采集源,你可以做出这些实际功能:
- 直播前预览合成画面,叠加品牌水印和滚动字幕。
- 视频会议App内自定义背景图,替代系统绿幕抠像。
- 自动化UI测试时,用固定测试图卡替代真实摄像头,保证测试可重复。
- 远程运维场景,把监控视频流接入直播SDK,当作摄像头源输出。
以上这些场景,核心都是“视频帧的生产和消费都在App的采集管线里”,这也是我认为iOS虚拟摄像头插件最健康、最可维护的形态。
5.2 这类插件不该拿去做什么
务必提一个红线:不要用它来做身份认证欺骗,比如替换人脸识别帧、伪造身份证拍照画面。这类做法既违反App审核规则,也可能触犯法律。技术本身是中性的,但虚拟摄像头天然带有伪造属性,使用场景必须局限在合成、测试、娱乐向的内容生产。
5.3 我的真实体会
踩过一轮坑之后,我对iOS虚拟摄像头的看法是:它的价值不在“欺骗摄像头”,而在“扩展摄像头”。当你把视频帧当成数据流而不是硬件信号时,可供发挥的空间其实非常大。用AVFoundation+CoreImage,已经能搞定大部分特效、合成、推流需求;真要系统级全局生效,先考虑硬件采集,再评估越狱成本。
如果你正在规划类似项目,我的建议是先花两天把App内自采集管线跑通,再问自己“到底需不需要全局生效”。大多数时候,答案是不需要。
本文还有配套的精品资源,点击获取