Highlight
AVFoundation 在 WWDC22 引入了一系列 async/await API,让视频缩略图生成、音视频合成、资源属性读取等操作不再阻塞主线程,同时新增
entireLengthAvailableOnDemand标志优化本地自定义数据源的播放性能。
核心内容
缩略图生成:从阻塞到异步
你的 App 里有一个视频播放器,用户拖动进度条时,你想在进度条上方显示对应时间点的预览图。这个需求很常见,但实现方式直接影响用户体验。
以前用 AVAssetImageGenerator 的 copyCGImage(at:actualTime:) 方法生成缩略图,这个方法在主线程同步加载视频帧数据。视频文件大、存储在远程服务器时,主线程会被卡住,UI 冻结,用户看到的只有转圈。
WWDC22 给 AVAssetImageGenerator 加了 image(at:) 异步方法。调用时线程立即释放,等数据加载完成后再通过 await 返回结果。用户界面保持响应,缩略图生成在后台进行。
批量缩略图:Async Sequence 让代码更自然
需要一次性生成多个时间点的缩略图,比如为整个时间轴做预览。以前用 generateCGImagesAsynchronously(forTimes:completionHandler:),需要把 [CMTime] 映射成 [NSValue],在回调里处理结果,代码冗长。
新 API images(for:) 直接接受 [CMTime],返回一个 Async Sequence。用 for await 循环逐个获取结果,成功失败都在循环体里处理,代码结构和同步遍历数组一样直观。
音视频合成:隐藏的同步陷阱
AVMutableComposition 的 insertTimeRange(_:of:at:) 看起来只是往合成轨道里插入一段引用,但内部会同步读取源资源的轨道信息。如果轨道数据还没加载,主线程会在这里阻塞。
WWDC22 推出了 insertTimeRange 的异步版本,自动在后台加载所需属性。AVVideoComposition 的构造方法和验证方法也有了异步版本,不需要手动预加载资源属性。
资源属性读取:类型安全替代字符串键
去年引入的 load(_:) 方法用类型安全的属性标识符替代了传统的字符串键值加载。字符串键拼写错误不会报错,但会导致属性同步加载、阻塞线程。load(_:) 在编译期就能发现错误,直接返回指定类型的属性值。
今年 Apple 正式废弃了基于字符串的异步键值加载,推荐所有新代码使用 load(_:)。
自定义数据源:本地媒体跳过缓存
用 AVAssetResourceLoader 加载自定义数据源时,AVAsset 默认按网络通信假设来管理缓存。即使数据就在本地,也会等缓存填满才开始播放。
新属性 entireLengthAvailableOnDemand 告诉 AVAsset 数据可以按需即时获取,跳过缓存机制。这减少了内存占用,也缩短了播放启动时间。但只适用于纯本地数据源,涉及网络操作会导致播放不可靠。
详细内容
异步生成单个缩略图
(01:41)
func thumbnail() async throws -> UIImage {
let generator = AVAssetImageGenerator(asset: asset)
generator.requestedTimeToleranceBefore = .zero
generator.requestedTimeToleranceAfter = CMTime(seconds: 3, preferredTimescale: 600)
let thumbnail = try await generator.image(at: time).image
return UIImage(cgImage: thumbnail)
}
关键点:
AVAssetImageGenerator(asset:)创建生成器,绑定到目标视频资源requestedTimeToleranceBefore和requestedTimeToleranceAfter设置时间容差,给生成器更多帧选择空间generator.image(at: time)是新的异步方法,返回(image: CGImage, actualTime: CMTime)元组.image直接取 CGImage,省去解包元组的步骤CMTime(seconds: 3, preferredTimescale: 600)表示容差为 3 秒,timescale 600 是视频编辑常用精度
关于容差:视频压缩帧分 iFrame(关键帧,可独立解码)和依赖帧(需要相邻帧才能解码)。把时间容差设为零会强制生成器找精确时间点,那个帧大概率是依赖帧,生成器还得加载周围多个帧。放宽容差让生成器选最近的关键帧,加载数据更少,返回更快。
批量生成缩略图
(02:56)
func timelineThumbnails(for times: [CMTime]) async {
for await result in generator.images(for: times) {
switch result {
case .success(requestedTime: let requestedTime, image: let image, actualTime: _):
updateThumbnail(for: requestedTime, with: image)
case .failed(requestedTime: let requestedTime, error: _):
updateThumbnail(for: requestedTime, with: placeholder)
}
}
}
关键点:
images(for:)接受[CMTime],不需要再转成[NSValue]- 返回 Async Sequence,用
for await逐个消费结果 - 每个结果包含
requestedTime(请求的时间点)、image(生成的图像)、actualTime(实际使用的帧时间) - 失败时走
.failed分支,可以回退到占位图
更简洁的写法:
(03:49)
func timelineThumbnails(for times: [CMTime]) async {
for await result in generator.images(for: times) {
updateThumbnail(for: result.requestedTime, with: (try? result.image) ?? placeholder)
}
}
这里 result.image 可能抛出错误,用 try? 转成可选值,失败时用 ?? placeholder 兜底。
异步音视频合成
(04:40)
let composition = AVMutableComposition()
try await composition.insertTimeRange(timeRange, of: asset, at: startTime)
关键点:
insertTimeRange现在有await版本- 内部自动异步加载
asset的轨道信息 - 不需要先手动
try await asset.load(.tracks)
视频合成验证也支持异步:
(04:57)
let videoComposition = try await AVVideoComposition.videoComposition(withPropertiesOf: asset)
try await videoComposition.isValid(for: asset, timeRange: range, validationDelegate: delegate)
类型安全的资源属性加载
(05:33)
旧写法(已废弃):
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
guard asset.statusOfValue(forKey: "duration", error: &error) == .loaded else { ... }
guard asset.statusOfValue(forKey: "tracks", error: &error) == .loaded else { ... }
myFunction(thatUses: asset.duration, and: asset.tracks)
}
新写法:
let (duration, tracks) = try await asset.load(.duration, .tracks)
myFunction(thatUses: duration, and: tracks)
关键点:
- 旧写法用字符串
"duration"、"tracks"做键,拼写错误编译器不报错,运行时同步加载导致卡顿 - 新写法的
.duration、.tracks是类型安全的AVAsyncProperty,拼写错误编译期就能发现 - 一次
load调用可以加载多个属性,用元组接收结果 - 适用于
AVAsset、AVAssetTrack、AVMetadataItem及其子类
同步插入合成轨道(特殊情况)
(07:06)
let composition = AVMutableComposition()
guard let compositionTrack = composition
.addMutableTrack(withMediaType: .video, preferredTrackID: 1) else { return }
try compositionTrack.insertTimeRange(firstFiveSeconds,
of: videoTrack1, at: .zero)
try compositionTrack.insertTimeRange(firstFiveSeconds,
of: videoTrack2, at: fiveSeconds)
关键点:
- 合成轨道插入可以同步执行,因为
AVMutableComposition是内存中的数据结构 - 不涉及文件 I/O,不会阻塞主线程
- 这是少数保留同步属性访问的类之一
优化本地自定义数据源
(09:09)
let resourceLoader = asset.resourceLoader
resourceLoader.setDelegate(myDelegate, queue: myQueue)
// 在 delegate 中设置
// resourceLoader.entireLengthAvailableOnDemand = true
关键点:
AVAssetResourceLoader让 App 自定义数据加载逻辑entireLengthAvailableOnDemand告诉系统数据可以即时获取- 跳过缓存机制,减少内存占用,加快播放启动
- 仅适用于纯本地数据,网络数据源开启后播放会不稳定
核心启发
1. 为视频编辑器做实时时间轴预览
- 做什么:在视频剪辑 App 的时间轴上,鼠标悬停时显示对应时间点的缩略图预览
- 为什么值得做:
images(for:)的 Async Sequence 天然适合流式生成时间轴缩略图,配合requestedTimeToleranceAfter设置合理容差,可以在用户拖动时快速响应 - 怎么开始:用
AVAssetImageGenerator的images(for:)方法,按固定间隔(如每 1 秒)生成缩略图,存到NSCache里复用
2. 本地加密视频播放器
- 做什么:播放存储在自定义容器里的加密视频,比如教育 App 的离线课程包
- 为什么值得做:
AVAssetResourceLoader+entireLengthAvailableOnDemand让解密和播放无缝衔接,本地数据跳过缓存后启动更快 - 怎么开始:实现
AVAssetResourceLoaderDelegate,在resourceLoader(_:shouldWaitForLoadingOfRequestedResource:)里解密数据并返回,设置entireLengthAvailableOnDemand = true
3. 视频拼接/混剪工具
- 做什么:让用户选择多个视频片段,按顺序拼接成一个视频,支持添加转场效果
- 为什么值得做:异步
insertTimeRange和AVVideoComposition的异步构造方法让多段视频合成不再卡 UI,可以实时预览合成结果 - 怎么开始:创建
AVMutableComposition,用异步insertTimeRange插入各片段,用AVVideoComposition.videoComposition(withPropertiesOf:)生成转场,用AVPlayerItem预览
4. 视频信息快速扫描工具
- 做什么:批量扫描相册或文件夹里的视频,提取时长、分辨率、码率等元数据
- 为什么值得做:
asset.load(.duration, .tracks)的类型安全 API 让批量处理代码更可靠,async/await 让并发控制更简单 - 怎么开始:用
TaskGroup并发加载多个视频的属性,用load(.formatDescriptions, .naturalSize)获取技术参数
关联 Session
- Meet async sequence — 深入了解 Swift Async Sequence 的工作原理,配合
images(for:)使用 - Discover concurrency in SwiftUI — 在 SwiftUI 中结合 AVFoundation 的异步 API 构建响应式界面
- Add Live Text interaction to your app — 在视频帧上叠加文本识别能力,和缩略图生成结合使用
- What’s new in AVQT — AVFoundation 视频质量工具的新特性,与媒体处理互补
评论
GitHub Issues · utterances