WWDC Quick Look 💓 By SwiftGGTeam
Create a more responsive media app

Create a more responsive media app

观看原视频

Highlight

AVFoundation 在 WWDC22 引入了一系列 async/await API,让视频缩略图生成、音视频合成、资源属性读取等操作不再阻塞主线程,同时新增 entireLengthAvailableOnDemand 标志优化本地自定义数据源的播放性能。

核心内容

缩略图生成:从阻塞到异步

你的 App 里有一个视频播放器,用户拖动进度条时,你想在进度条上方显示对应时间点的预览图。这个需求很常见,但实现方式直接影响用户体验。

以前用 AVAssetImageGeneratorcopyCGImage(at:actualTime:) 方法生成缩略图,这个方法在主线程同步加载视频帧数据。视频文件大、存储在远程服务器时,主线程会被卡住,UI 冻结,用户看到的只有转圈。

WWDC22 给 AVAssetImageGenerator 加了 image(at:) 异步方法。调用时线程立即释放,等数据加载完成后再通过 await 返回结果。用户界面保持响应,缩略图生成在后台进行。

批量缩略图:Async Sequence 让代码更自然

需要一次性生成多个时间点的缩略图,比如为整个时间轴做预览。以前用 generateCGImagesAsynchronously(forTimes:completionHandler:),需要把 [CMTime] 映射成 [NSValue],在回调里处理结果,代码冗长。

新 API images(for:) 直接接受 [CMTime],返回一个 Async Sequence。用 for await 循环逐个获取结果,成功失败都在循环体里处理,代码结构和同步遍历数组一样直观。

音视频合成:隐藏的同步陷阱

AVMutableCompositioninsertTimeRange(_: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:) 创建生成器,绑定到目标视频资源
  • requestedTimeToleranceBeforerequestedTimeToleranceAfter 设置时间容差,给生成器更多帧选择空间
  • 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 调用可以加载多个属性,用元组接收结果
  • 适用于 AVAssetAVAssetTrackAVMetadataItem 及其子类

同步插入合成轨道(特殊情况)

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 设置合理容差,可以在用户拖动时快速响应
  • 怎么开始:用 AVAssetImageGeneratorimages(for:) 方法,按固定间隔(如每 1 秒)生成缩略图,存到 NSCache 里复用

2. 本地加密视频播放器

  • 做什么:播放存储在自定义容器里的加密视频,比如教育 App 的离线课程包
  • 为什么值得做AVAssetResourceLoader + entireLengthAvailableOnDemand 让解密和播放无缝衔接,本地数据跳过缓存后启动更快
  • 怎么开始:实现 AVAssetResourceLoaderDelegate,在 resourceLoader(_:shouldWaitForLoadingOfRequestedResource:) 里解密数据并返回,设置 entireLengthAvailableOnDemand = true

3. 视频拼接/混剪工具

  • 做什么:让用户选择多个视频片段,按顺序拼接成一个视频,支持添加转场效果
  • 为什么值得做:异步 insertTimeRangeAVVideoComposition 的异步构造方法让多段视频合成不再卡 UI,可以实时预览合成结果
  • 怎么开始:创建 AVMutableComposition,用异步 insertTimeRange 插入各片段,用 AVVideoComposition.videoComposition(withPropertiesOf:) 生成转场,用 AVPlayerItem 预览

4. 视频信息快速扫描工具

  • 做什么:批量扫描相册或文件夹里的视频,提取时长、分辨率、码率等元数据
  • 为什么值得做asset.load(.duration, .tracks) 的类型安全 API 让批量处理代码更可靠,async/await 让并发控制更简单
  • 怎么开始:用 TaskGroup 并发加载多个视频的属性,用 load(.formatDescriptions, .naturalSize) 获取技术参数

关联 Session

评论

GitHub Issues · utterances