大多数浏览器和
Developer App 均支持流媒体播放。
-
AVFoundation 的新功能
探索 AVFoundation、Apple 框架用于检查、播放和创作视听内容的最新更新。我们将探索如何使用 AVFoundation 查询视听资源属性,使用定时元数据进一步定制您的自定义视频作品,并创作字幕文件。
资源
相关视频
WWDC22
WWDC21
-
下载
♪低音音乐播放♪ ♪ 亚当桑南斯廷:你好! 我的名字是亚当 我今天在这里向你展示 AVFoundation的新功能 我们今天要讨论三个新功能 我们会花大部分时间讨论 AVAsset检查领域的新功能 然后我们会快速介绍 另外两个功能 使用元数据的视频合成 与字幕文件创作 事不宜迟 让我们进入我们的第一个主题 即AVAsset异步检查 但首先 先了解一下 AVAsset的背景知识 从复习开始 AVAsset是AVFoundation的 核心模型对象 用于表示存储在用户设备上的 电影文件等内容 存储在其他地方的电影文件 例如远程服务器 以及其他形式的视听内容 例如HTTP Live Streams和作品 当你拥有资源时 你会想播放它 但同样的 你也会想要检查它 你想问它一些问题 比如它的持续时间 或者它包含的音频 和视频的格式是什么? 这就是我们在本主题中真正 要讨论的内容:资源检查 每当你检查资源时 都要牢记两件重要的事情 首先是资源检查按需进行 这主要是因为电影文件可能非常大 一部长篇电影的大小可能有几GB 你不希望资源 迫不及待地下载整个文件 以防万一你稍后询问其持续时间 相反 资源会等待 直到你要求它加载属性值 然后它只下载所需的信息 来提供该值 要记住的第二件事是资源检查 是一个异步过程 这非常重要 因为网络I/O 可能需要一些时间 如果资源是跨网络存储的 你会不希望app的主线程被阻塞 而AVAsset发出同步网络请求 相反 AVAsset会在准备好时 异步交付结果 考虑到这两点 我们就有了一个 用于检查资源属性的新API 它看起来有点像这样 需要注意的主要事情 是这个新的加载方法 它接收一个属性标识符 在本例中是.duration 以便你告诉它要加载哪个属性值 每个属性标识符在编译时都与一个 结果类型相关联 该结果类型 决定了加载方法的返回类型 在本例中 持续时间是一个CMTime 所以结果是一个CMTime 你以前可能没有见过 await这个关键词 这是Swift中的一项新功能 用于在调用网站 标记load方法是异步的 有关async/await的所有详细信息 以及Swift中更广泛的并行性工作 我鼓励你查看名为 《在Swift中 认识async/await》的课程 现在 为了快速了解 如何使用我们的新属性加载方法 我喜欢将await关键词 视为将调用函数分为两部分 首先是异步操作开始之前 发生的部分 在本例中 我们创建一个资源 并要求它加载其持续时间 此时 资源开始运行 并执行 必要的I/O和解析以确定其持续时间 然后我们等待其结果 当我们在等待时 调用函数被挂起 这代表在await之后编写的代码 不会立即执行 但是 我们正在执行的线程 并没有被阻塞 相反 我们可以在等待时 做更多的工作 一旦异步持续时间加载完成 就计划执行该函数的后半部分 在本例中 如果持续时间加载成功 我们将持续时间存储到局部常量中 并将其发送给另一个函数 或者 如果操作失败 一旦调用函数恢复 就会抛出一个错误 这就是异步加载属性值的基础知识 你还可以一次加载多个属性的值 只需将多个属性标识符 传递给load方法 即可完成此操作 在这种情况下 我们同时加载持续时间 和音轨 这样不仅方便 而且效率更高 如果资源知道你感兴趣的所有属性 它可以批量加载 加载它们的值所需的工作 加载多个属性值的结果是一个元组 所加载的值与用于属性标识符的 顺序相同 就像加载单个属性值一样 这是类型安全的 在这种情况下 结果元组的 第一个元素是CMTime 第二个元素是AVAssetTracks数组 当然 就像加载单个值一样 这是一个异步操作 除了异步加载属性值之外 你还可以随时使用 新的status(of:)方法 检查属性的状态 而无需等待值加载 你传入用于加载方法的 相同属性标识符 会返回一个包含四种可能情况的枚举 每个属性都以.notYetLoaded开头 请记住 资源检查是按需进行的 所以在你要求加载属性值之前 资源不会进行任何加载工作 如果你碰巧在加载过程中 检查状态 你会获得.loading 或者 如果该属性已加载 你将获得.loaded 它与作为关联值 加载的值捆绑在一起 最后 如果发生故障 也许是因为网络出现故障 你会得到.failed 它带有一个错误 用来描述哪里出了问题 请注意 这与调用load方法 引发失败的加载请求时 所引发的错误相同 这就是加载异步属性 和检查其状态的新API AVAsset有很多属性 它们的值可以异步加载 这些属性大多数提供了一个独立的值 但.tracks和.metadata属性 提供了更复杂的对象 你可以使用 它们来深入到资源的层次结构中 对于.tracks属性 你会获得一组AVAssetTracks AVAssetTrack有自己的属性集合 这些属性的值可以使用相同的 加载方法异步加载 同样地 .metadata属性 为你提供了一组AVMetadataItem 并且多个AVMetadataItem属性 也可以使用load方法 异步加载 这个领域的最后一个新API 是一个异步方法的集合 你可以用来 获取特定属性值的特定子集 因此 你不需要加载所有音轨 例如 你可以使用前三种方法之一 只加载其中的一些音轨 例如 仅加载音频音轨 在AVAsset和AVAssetTrack上 都有一些类似的新方法 这就是我们用于异步检查资源的 所有新API 但在这一点上 我要坦白一点 这些功能实际上都不是新的 这些API是新的 但这些类总是能够 异步加载它们的属性值 只是 使用旧的API 你必须编写类似这样的代码 这个过程分为三个步骤 首先必须调用 loadValuesAsynchronously方法 给它一个字符串 以告诉它要加载哪些属性 然后 你需要确保每个属性 实际上都成功加载 没有失败 然后 一旦完成 就可以通过查询相应的同步属性 或调用其中一种同步过滤方法 来获取加载的值 这不仅冗长和重复 而且很容易被误用 例如 很容易忘记 执行这些基本的加载和状态检查步骤 剩下的就是这些可以随时调用的 同步属性和方法 但如果你在没有首先 加载属性值的情况下 调用它们 你最终会阻塞I/O操作 如果你在主线程上这么做 这代表你的app最后可能会在 不可预测的时间挂起 因此 除了新API 更易于使用之外 它们还消除了这些常见的误用 这代表我们计划在未来版本中 弃用Swift客户端的旧同步API 现在是转移到这些接口的 新异步版本的最佳时机 为了帮助你做到这一点 我们准备了一个简短的转移指南 所以 如果你正在执行加载值 检查它的状态 然后获取同步属性这三步工作 那么你现在可以简单地调用load方法 并在一个异步步骤中完成所有操作 同样地 如果你正在执行这三步骤 但使用同步过滤方法 而不是属性 你现在可以调用该过滤方法的 异步等效方法 并在一步骤中完成 如果你切换的状态 使用旧的 statusOfValue(forKey:)方法 然后获取同步属性值.loaded 当你看到你的情况 现在你可以利用以下事实 新状态枚举的.loaded案例 与该.loaded值捆绑在一起 如果你的app 正在做一些更有趣的事情 比方在代码的一部分中加载属性的值 然后在代码的不同部分中 获取加载的值 有几种方法 可以用新的界面来做 我建议再次调用load方法 这是最简单和最安全的方法 如果属性已经加载 这不会重复已经完成的工作 相反 它只会返回一个缓存值 然而 这里有一个警告 那就是 因为load方法是一种异步方法 它只能从异步上下文中调用 因此 如果你真的需要 从纯同步上下文中 获取属性的值 你可以执行一些操作 比方说获取属性的状态 并断言它已加载 以便同步获取属性的值 但这样做还是要小心 因为即使已经加载了属性 它也有可能失败 最后 如果你跳过加载 和状态检查步骤 而仅仅依赖属性和方法的当前行为 因为它们会阻塞直到结果可用 那么我们实际上并没有为此提供 这个替换 这从来不是使用API的推荐方式 所以我们一直不鼓励这么做 我们将新的属性加载API设计为 与获取简单属性一样容易使用 因此转移到新API应该很简单 以上就是我们的第一个主题的 所有内容 我对我们使用Swift的新异步功能 检查资源的新方法 感到非常兴奋 我希望你会像我一样喜欢使用它们 现在让我们继续讨论 两个简短主题中的第一个 使用元数据的视频合成 这里我们谈论的是视频合成 也就是将多个视频轨道 合成为一个视频帧流的过程 特别是 我们对自定义视频合成器进行了改善 你可以在其中提供 进行合成的代码 今年新增 你可以在自定义合成器的 框架合成回调中 获得每帧元数据 例如 假设你有一个GPS数据序列 该数据带有时间戳 并与你的视频同步 你希望使用该GPS数据 来影响帧的组合方式 现在就可以这样做了 第一步是将GPS数据 写入源影片中的定时元数据轨道 为了使用AVAssetWriter做到这一点 请查看现有分类 AVAssetWriter InputMetadataAdaptor 现在让我们看看新的API 假设你从一个具有特定音轨集合的 源电影开始 也许它有一个音频轨道 两个视频轨道 和三个定时元数据轨道 但是 我们假设轨道四和轨道五包含 对你的视频合成有用的元数据 但轨道六不相关 你需要执行两个设置步骤 第一个步骤 是使用新的 源SampleDataTrackIDs属性 来告诉视频合成对象 与整个视频合成相关的 所有定时元数据轨道的ID 完成后 第二步 是采取每个视频合成指令 并执行类似操作 但这次你设置 requiredSourceSampleData TrackIDs属性 以告诉它与该相关的轨道ID 或多个ID 这是相关的特定指令 执行这两个设置步骤很重要 否则你将无法在合成回调中 获得任何元数据 现在让我们转到回调本身 当你在回调中获取异步视频合成 请求对象时 你可以使用两个新API 来获取视频合成的元数据 第一个是 源SampleDataTrackIDs属性 它重播与该请求相关的 元数据轨道ID 然后对于每个轨道ID 你可以使用 sourceTimedMetadata (byTrackID:)方法 来获取该轨道的当前计时 元数据组 现在 AVTimedMetadataGroup 是元数据的 高阶表示 其值被解析为 字符串、日期或其他高阶对象 如果你更喜欢使用元数据的原始字节 你可以使用sourceSampleBuffer (byTrackID:)方法 来获取CMSampleBuffer 而不是AVTimedMetadataGroup 一旦有了元数据 你就可以使用元数据和源视频帧 来生成输出视频帧 并完成请求 这就是把元数据 放入你的自定义视频合成器 回调中所需的全部内容 这样你就可以用视频合成 做更多有趣的事情 现在进入我们的最后一个主题 即字幕文件创作 今年macOS的新功能 AVFoundation增加了 对两种文件格式的支持 首先 我们有iTunes Timed Text 或称为.itt文件 其中包含字幕 另一种文件格式是 Scenarist Closed Captions 或.scc文件 包含隐藏式字幕 AVFoundation正在添加 对创作这两种文件格式的支持 从这些类型的文件中提取字幕 以及在执行时预览字幕 以查看它们在播放期间的样子 在编写方面 我们有一些新的API 从AVCaption开始 它是代表单个字幕的模型对象 它具有诸如文本、位置 样式和单个字幕的其他属性等属性 你可以自己创建AVCaption 并使用AVAssetWriterInput CaptionAdaptor 将它们写入这两种文件格式之一 此外 我们在AVCaption ConversionValidator类中 有一个新的验证服务 它可以帮助你确保你编写的字幕 实际上与你选择的文件格式兼容 作为说明为什么这很重要的示范 请考虑.scc文件 它们包含CEA-608字幕 这是一种格式 对在给定时间内 可以拥有多少字幕 具有非常具体的限制 一直到对表示单个字符 及其样式的数据具有 固定的位预算 因此 验证器不仅会帮助你确保 字幕流与文件格式兼容 还会建议你可以对字幕进行调整 例如调整其时间戳 以使其兼容 接收字幕的新API 是AVAssetReaderOutput CaptionAdaptor 它允许你获取这些文件之一 并从中读取AVCaption对象 最后 我们有一个 AVCaptionRenderer分类 它允许你获取单个字幕 或一组字幕 并将它们渲染到CGContext 以便预览它们在播放期间的 样子 因此 这只是我们 新的字幕文件创作API的冰山一角 如果你有兴趣采用它们 我们鼓励你与我们联系 无论是在论坛上 还是在会议实验室 我们可以帮助回答你的任何问题 这就是我们的最后一个话题 让我们总结一下 我们今天的主题是检查AVAsset属性 按需和异步执行此操作的重要性 该领域的新API 以及从旧API搬移的一些技巧 然后我们讨论了使用定时元数据 来进一步自定义你的自定义视频作品 最后 我简单介绍了 字幕文件创作和该领域的新API 今天就到这里 感谢收看 请享受WWDC21 ♪
-
-
2:16 - AVAsset property loading
func inspectAsset() async throws { let asset = AVAsset(url: movieURL) let duration = try await asset.load(.duration) myFunction(thatUses: duration) }
-
4:02 - Load multiple properties
func inspectAsset() async throws { let asset = AVAsset(url: movieURL) let (duration, tracks) = try await asset.load(.duration, .tracks) myFunction(thatUses: duration, and: tracks) }
-
4:52 - Check status
switch asset.status(of: .duration) { case .notYetLoaded: // This is the initial state after creating an asset. case .loading: // This means the asset is actively doing work. case .loaded(let duration): // Use the asset's property value. case .failed(let error): // Handle the error. }
-
6:32 - Async filtering methods
let asset: AVAsset let trk1 = try await asset.loadTrack(withTrackID: 1) let atrs = try await asset.loadTracks(withMediaType: .audio) let ltrs = try await asset.loadTracks(withMediaCharacteristic: .legible) let qtmd = try await asset.loadMetadata(for: .quickTimeMetadata) let chcl = try await asset.loadChapterMetadataGroups(withTitleLocale: .current) let chpl = try await asset.loadChapterMetadataGroups(bestMatchingPreferredLanguages: ["en-US"]) let amsg = try await asset.loadMediaSelectionGroup(for: .audible) let track: AVAssetTrack let seg0 = try await track.loadSegment(forTrackTime: .zero) let spts = try await track.loadSamplePresentationTime(forTrackTime: .zero) let ismd = try await track.loadMetadata(for: .isoUserData) let fbtr = try await track.loadAssociatedTracks(ofType: .audioFallback)
-
7:16 - Async loading: Old API
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) { var error: NSError? guard asset.statusOfValue(forKey: "duration", error: &error) == .loaded else { ... } guard asset.statusOfValue(forKey: "tracks", error: &error) == .loaded else { ... } let duration = asset.duration let audioTracks = asset.tracks(withMediaType: .audio) // Use duration and audioTracks. }
-
8:09 - This is the equivalent using the new API:
let duration = try await asset.load(.duration) let audioTracks = try await asset.loadTracks(withMediaType: .audio) // Use duration and audioTracks.
-
8:36 - load(_:)
let tracks = try await asset.load(.tracks)
-
8:51 - Async filtering method
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
-
8:58 - status(of:)
switch status(of: .tracks) { case .loaded(let tracks): // Use tracks.
-
9:18 - load(_:) again (returns cached value)
let tracks = try await asset.load(.tracks)
-
9:49 - Assert status is .loaded()
guard case .loaded (let tracks) = asset.status(of: .tracks) else { ... }
-
11:49 - Custom video composition with metadata: Setup
/* Source movie: - Track 1: Audio - Track 2: Video - Track 3: Video - Track 4: Metadata - Track 5: Metadata - Track 6: Metadata */ // Tell AVMutableVideoComposition about all the metadata tracks. videoComposition.sourceSampleDataTrackIDs = [4, 5] // For each AVMutableVideoCompositionInstruction, specify the metadata track ID(s) to include. instruction1.requiredSourceSampleDataTrackIDs = [4] instruction2.requiredSourceSampleDataTrackIDs = [4, 5]
-
12:44 - Custom video composition with metadata: Compositing
// This is an implementation of a AVVideoCompositing method: func startRequest(_ request: AVAsynchronousVideoCompositionRequest) { for trackID in request.sourceSampleDataTrackIDs { let metadata: AVTimedMetadataGroup? = request.sourceTimedMetadata(byTrackID: trackID) // To get CMSampleBuffers instead, use sourceSampleBuffer(byTrackID:). } // Compose input video frames, using metadata, here. request.finish(withComposedVideoFrame: composedFrame) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。