(三) AVFoundation — 资源(Asset)与元数据(Metadata)学习笔记(含真实代码示例)
下面是一份偏实践、可直接粘贴运行的 AVFoundation 资源与元数据学习笔记。内容覆盖:如何打开本地/远程资源、异步加载属性、读取常见元数据、处理 timed metadata、提取缩略图、在导出时写入元数据、以及常见坑/解决方法。示例均为 Swift(iOS / macOS 通用,Swift 5+)。
一、核心概念速览(一句话)
- AVAsset / AVURLAsset:表示媒体资源(文件或远程 URL)。
- AVPlayerItem:用于播放的封装(也可观察 timedMetadata)。
- AVMetadataItem:表示一个元数据项(title、artist、creationDate、ID3/TXXX 等)。
- AVAssetExportSession / AVAssetWriter:导出/重编码并可写入元数据。
- AVAssetImageGenerator:从视频中截取缩略图(关键帧)。
- loadValuesAsynchronously(forKeys:):异步加载 asset 的元属性(duration、tracks、playable 等)。必须掌握。
二、打开资源(本地 & 远程)与“安全读取”模式
要点:不要直接用
asset.duration—— 要先用loadValuesAsynchronously(forKeys:)加载需要的键并检查状态。
import AVFoundation
func openAsset(url: URL, completion: @escaping (AVAsset?) -> Void) {
let asset = AVURLAsset(url: url)
let keys = ["playable", "duration", "tracks", "commonMetadata"]
asset.loadValuesAsynchronously(forKeys: keys) {
for k in keys {
var err: NSError?
let status = asset.statusOfValue(forKey: k, error: &err)
if status == .failed || status == .cancelled {
print("加载 \(k) 失败: \(err?.localizedDescription ?? "unknown")")
DispatchQueue.main.async { completion(nil) }
return
}
}
DispatchQueue.main.async { completion(asset) }
}
}
// 本地文件示例
let fileURL = URL(fileURLWithPath: "/path/to/video.mp4")
// 远程示例
let remoteURL = URL(string: "https://example.com/video.mp4")!
openAsset(url: remoteURL) { asset in
guard let a = asset else { return }
print("duration: \(CMTimeGetSeconds(a.duration))")
}
三、读取元数据(Metadata)
1. commonMetadata(常用快捷字段)
AVAsset.commonMetadata 返回一组 AVMetadataItem。常见 commonKey:
.title/.artist/.albumName/.creationDate/.artwork/.genre/.description等。
func printCommonMetadata(from asset: AVAsset) {
let meta = asset.commonMetadata
for item in meta {
if let key = item.commonKey?.rawValue {
print("\(key): \(item.value ?? "nil")")
} else {
print("format:\(item.identifier ?? "unknown") value:\(item.value ?? "nil")")
}
}
}
2. 按格式读取(例如 ID3、iTunes)
媒体文件可能有多种格式的元数据(ID3、iTunes, QuickTime metadata 等),可用:
// 读取 ID3 格式的元数据
let id3Items = asset.metadata(forFormat: AVMetadataFormat.id3Metadata)
for i in id3Items {
print("id3: \(i.identifier ?? "id") -> \(i.value ?? "nil")")
}
3. 常用 metadata key 列表(部分)
AVMetadataKey.commonKeyTitle(.title)AVMetadataKey.commonKeyArtist(.artist)AVMetadataKey.commonKeyArtwork(.artwork) — value 为 Data(通常是图像二进制)- QuickTime keys:
com.apple.quicktime.creationdate等。 - ID3 keys:
AVMetadataKey.id3MetadataKeyTitleDescription等。
(遇到不认识的 key,打印identifier看到底是什么)
四、Timed Metadata(时间轴元数据)——通常用于直播 / HLS
AVPlayerItem有timedMetadata,当播放 HLS(.m3u8)或广播流时,流内插入的 ID3 标签或 in-band 元数据会出现在这里。- 可以使用 KVO 或
addObserver(forKeyPath:)监听timedMetadata。使用AVPlayerItem的addObserver或AVPlayerItem的publisher(Combine)。
示例(KVO):
import AVKit
var playerItem: AVPlayerItem!
func observeTimedMetadata(of url: URL) {
playerItem = AVPlayerItem(url: url)
playerItem.addObserver(self, forKeyPath: "timedMetadata", options: .new, context: nil)
let player = AVPlayer(playerItem: playerItem)
// ... 播放
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == "timedMetadata" {
if let arr = playerItem.timedMetadata {
for meta in arr {
print("timed meta: \(meta)")
// meta.value / meta.stringValue / meta.key / meta.keySpace
}
}
}
}
注意:timedMetadata 只有在流中存在时才会出现,普通 mp4 不会有。
五、提取视频缩略图(AVAssetImageGenerator)
func generateThumbnail(from asset: AVAsset, at time: CMTime = CMTime(seconds: 1, preferredTimescale: 600), completion: @escaping (UIImage?) -> Void) {
let gen = AVAssetImageGenerator(asset: asset)
gen.appliesPreferredTrackTransform = true // 修正旋转
gen.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { _, cgImage, _, result, error in
if let cg = cgImage, result == .succeeded {
completion(UIImage(cgImage: cg))
} else {
print("生成缩略图失败: \(error?.localizedDescription ?? "unknown")")
completion(nil)
}
}
}
六、在导出时写入 / 修改元数据(AVAssetExportSession)
场景:你用 AVAssetExportSession 导出视频并想写入 title、artist、cover。
func exportWithMetadata(asset: AVAsset, outputURL: URL, completion: @escaping (Bool) -> Void) {
guard let export = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
completion(false); return
}
export.outputURL = outputURL
export.outputFileType = .mp4
// 构建 AVMutableMetadataItem
let title = AVMutableMetadataItem()
title.keySpace = .common
title.key = AVMetadataKey.commonKeyTitle as NSString
title.value = "My Title" as NSString
let artist = AVMutableMetadataItem()
artist.keySpace = .common
artist.key = AVMetadataKey.commonKeyArtist as NSString
artist.value = "My Artist" as NSString
// 封面(UIImage -> Data)
if let image = UIImage(named: "cover"), let data = image.pngData() {
let artwork = AVMutableMetadataItem()
artwork.keySpace = .common
artwork.key = AVMetadataKey.commonKeyArtwork as NSString
artwork.value = data as NSData
export.metadata = [title, artist, artwork]
} else {
export.metadata = [title, artist]
}
export.exportAsynchronously {
switch export.status {
case .completed:
completion(true)
default:
print("导出失败:\(String(describing: export.error))")
completion(false)
}
}
}
注意:不同的
outputFileType对 metadata 的支持不同(.mov 强于 .mp4 某些容器)。若 metadata 无效,尝试改为.mov或使用AVAssetWriter更细致地写入。
七、写入复杂元数据 — AVAssetWriter(更底层)
AVAssetExportSession 能满足常见场景;若需更复杂控制(如逐帧注入 timed metadata、或将音频/视频重编码并写入 ID3),则使用 AVAssetWriter + AVAssetWriterInputMetadataAdaptor。
示例略复杂,核心是创建 AVAssetWriter, 添加 AVAssetWriterInput(video/audio),并创建 AVAssetWriterInputMetadataAdaptor 来写 timed-metadata(参见官方文档)。
八、常见问题与坑(务必记住)
- 同步访问 asset 的属性会阻塞或返回 0
- 必须使用
loadValuesAsynchronously(forKeys:)或AVAssetResourceLoader的回调加载。
- 必须使用
- 远程 URL(HTTP)需要 Content-Type 与 range 支持
- HLS / progressive download 对服务器支持 range 与字节服务依赖性大,若服务器不支持 Range,会导致无法播放或无法 seek。
- metadata 写入容器兼容性
- 并非所有容器都支持写入任意 metadata;例如部分 MP4 播放器不会显示所有 commonMetadata。尝试
.mov或使用atomic: true的方法。
- 并非所有容器都支持写入任意 metadata;例如部分 MP4 播放器不会显示所有 commonMetadata。尝试
- 作品水印/签名/防篡改
- 有的 App 在运行时会校验原始文件的 hash / signature,重打包或修改后会校验失败(需 patch 校验逻辑或在服务端处理)。
- timedMetadata 只有在流里存在
- 不是所有资源都包含 timed metadata,通常 HLS 的 ID3 才会出现。
- 艺术家/封面读取为 Data
- artwork 的
value常为Data或NSData,需要解码成图片格式(PNG/JPEG)。
- artwork 的
- ID3/RIFF/QuickTime KeySpace 区分
- 同样的 “Title” 可能存在于不同 keySpace(id3, iTunes, quicktime),必要时逐个格式检查:
asset.metadata(forFormat:)。
- 同样的 “Title” 可能存在于不同 keySpace(id3, iTunes, quicktime),必要时逐个格式检查:
- AVAssetExportSession 的 metadata 覆盖行为
- 有时只是合并而非覆盖,或被播放器忽略。验证导出后的文件实际包含的 metadata:可用
AVAsset(url:).commonMetadata再次读取。
- 有时只是合并而非覆盖,或被播放器忽略。验证导出后的文件实际包含的 metadata:可用
九、实用工具函数合集(快速复制)
// 读取 asset 的所有 metadata(按 format)
func dumpAllMetadata(asset: AVAsset) {
let formats = asset.availableMetadataFormats
print("formats: \(formats)")
for f in formats {
let items = asset.metadata(forFormat: f)
print("== format: \(f) count: \(items.count)")
for i in items {
print("id:\(i.identifier ?? "nil") key:\(String(describing:i.key)) keyspace:\(i.keySpace?.rawValue ?? "nil") value:\(String(describing:i.value))")
}
}
}
// 将 AVMetadataItem.value 转为 String
func metadataValueString(_ item: AVMetadataItem) -> String? {
if let s = item.stringValue { return s }
if let num = item.numberValue { return num.stringValue }
if let data = item.dataValue, let str = String(data: data, encoding: .utf8) { return str }
return nil
}
十、示例:完整工作流(读取 → 生成缩略图 → 导出并写入 metadata)
let url = URL(string: "https://example.com/video.mp4")!
openAsset(url: url) { asset in
guard let asset = asset else { return }
// 1. 打印 commonMetadata
printCommonMetadata(from: asset)
// 2. 生成缩略图
generateThumbnail(from: asset, at: CMTime(seconds: 2, preferredTimescale: 600)) { image in
// 保存临时封面
if let img = image, let data = img.pngData() {
let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("cover.png")
try? data.write(to: tmp)
// 3. 导出并写入 metadata(将封面写入)
let out = FileManager.default.temporaryDirectory.appendingPathComponent("out.mp4")
exportWithMetadata(asset: asset, outputURL: out) { ok in
print("导出完成:\(ok) -> \(out.path)")
// 验证
openAsset(url: out) { newAsset in
printCommonMetadata(from: newAsset!)
}
}
}
}
}
十一、进阶与推荐阅读
- 学习
AVAssetResourceLoaderDelegate:实现自定义资源加载(DRM / 离线缓存 / 自定义协议)。 AVAssetWriter+AVAssetReader:用于转码与更细粒度写入。- timed metadata 与 HLS 流处理(ID3 与 SCTE 标准)。
- Apple 官方 AVFoundation Programming Guide(英文)与 Sample Code(可在 Apple 开发者站点查阅)。