菜鸟-创作你的创作

(三)AVFoundation 之 资源和元数据学习笔记_av真实资源

(三) AVFoundation — 资源(Asset)与元数据(Metadata)学习笔记(含真实代码示例)

下面是一份偏实践、可直接粘贴运行的 AVFoundation 资源与元数据学习笔记。内容覆盖:如何打开本地/远程资源、异步加载属性、读取常见元数据、处理 timed metadata、提取缩略图、在导出时写入元数据、以及常见坑/解决方法。示例均为 Swift(iOS / macOS 通用,Swift 5+)。


一、核心概念速览(一句话)


二、打开资源(本地 & 远程)与“安全读取”模式

要点:不要直接用 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

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 列表(部分)


四、Timed Metadata(时间轴元数据)——通常用于直播 / HLS

示例(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(参见官方文档)。


八、常见问题与坑(务必记住)

  1. 同步访问 asset 的属性会阻塞或返回 0
    • 必须使用 loadValuesAsynchronously(forKeys:)AVAssetResourceLoader 的回调加载。
  2. 远程 URL(HTTP)需要 Content-Type 与 range 支持
    • HLS / progressive download 对服务器支持 range 与字节服务依赖性大,若服务器不支持 Range,会导致无法播放或无法 seek。
  3. metadata 写入容器兼容性
    • 并非所有容器都支持写入任意 metadata;例如部分 MP4 播放器不会显示所有 commonMetadata。尝试 .mov 或使用 atomic: true 的方法。
  4. 作品水印/签名/防篡改
    • 有的 App 在运行时会校验原始文件的 hash / signature,重打包或修改后会校验失败(需 patch 校验逻辑或在服务端处理)。
  5. timedMetadata 只有在流里存在
    • 不是所有资源都包含 timed metadata,通常 HLS 的 ID3 才会出现。
  6. 艺术家/封面读取为 Data
    • artwork 的 value 常为 DataNSData,需要解码成图片格式(PNG/JPEG)。
  7. ID3/RIFF/QuickTime KeySpace 区分
    • 同样的 “Title” 可能存在于不同 keySpace(id3, iTunes, quicktime),必要时逐个格式检查:asset.metadata(forFormat:)
  8. AVAssetExportSession 的 metadata 覆盖行为
    • 有时只是合并而非覆盖,或被播放器忽略。验证导出后的文件实际包含的 metadata:可用 AVAsset(url:).commonMetadata 再次读取。

九、实用工具函数合集(快速复制)

// 读取 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!)
                }
            }
        }
    }
}


十一、进阶与推荐阅读

退出移动版