Chapter 117: Apple Video Media Pipeline
Apple 的视频媒体管线可以用一个贯穿行为读清楚:一个 App 打开本地视频文件,读取视频轨道,把压缩样本解码成像素帧,交给 Metal 做一层轻量滤镜,同时把预览画面显示出来,最后再编码写成新的文件。这个行为横跨 AVFoundation、Core Media、Core Video、VideoToolbox、Core Animation 和 Metal。每一层处理的对象不同:有的层负责资产和轨道,有的层负责时间和样本,有的层负责像素内存,有的层负责硬件编解码,有的层负责显示和渲染。
本章要建立的判断动作是:看到 Apple 平台上的播放、转码、导出、录制或自定义渲染需求时,先定位数据处在压缩样本、解码像素、显示 layer、Metal texture 还是输出文件这几个阶段,再判断该由 AVFoundation、Core Media、Core Video、VideoToolbox、Core Animation 或 Metal 中哪一层承担责任。这个判断顺序比记忆类名更稳定,因为媒体问题通常表现为画面黑屏、时间戳错乱、编码失败、颜色错误、内存压力或渲染延迟,真正的原因经常发生在对象边界转换处。
本章使用公开 API 和公开文档能确认的层级关系来讲解。Apple 平台内部的媒体 daemon、硬件 codec 队列、驱动调度和受保护内容路径有大量私有实现,本章只在公开对象、可观察行为和合理边界上建立模型。涉及硬件 decode / encode 时,正文只说明 App 通过 VideoToolbox session 请求系统编解码能力,不声称掌握具体设备内部调度细节。
117.1 Apple Media Stack 总路径
Apple media stack 的总路径可以从“文件中的压缩视频”开始理解。文件层看到的是 MOV、MP4、M4V、HLS segment 这类容器;容器内部有 video track、audio track、metadata 和时间索引;视频 track 中的压缩 sample 需要经过 decoder 变成 CVPixelBuffer;像素帧可以交给 Core Animation 的 layer 显示,也可以被 Metal 包装成 texture 参与 GPU 渲染;导出时,像素帧再进入 encoder 变成压缩 sample,最后由 writer 写回容器。
在这个路径里,AVFoundation 是 App 最常接触的总入口。它把 asset、track、player、reader、writer、capture session 组织成高级媒体对象,解决“这个视频在哪里、有哪些轨道、从哪里读、写到哪里、如何播放”的问题。Core Media 提供时间和值语义,解决“这个 sample 属于哪个时间点、持续多久、格式是什么、携带哪些附件信息”的问题。Core Video 提供像素 buffer 与 buffer pool,解决“解码后的图像数据放在哪里、用什么像素格式、如何被多个框架共享”的问题。VideoToolbox 提供编解码 session,解决“压缩 bitstream 如何进入系统 codec、像素帧如何变回压缩 sample”的问题。Core Animation 和 Metal 分别解决“按时间显示到屏幕”和“把像素帧作为 GPU 资源处理”的问题。
贯穿本章的短视频滤镜导出路径可以表示为下图。图中只保留公开 API 层能观察到的对象边界,硬件 codec、驱动和显示控制器被折叠成系统资源边界。
这条链路里最容易混淆的是“播放”和“处理”的分工。AVPlayer 适合把 asset 播放给用户,系统替 App 管理读取、缓存、解码、同步和显示;AVAssetReader 适合 App 自己拿到 sample,建立分析、滤镜、转码或导出流程;AVSampleBufferDisplayLayer 适合 App 已经持有按时间排列的 sample buffer,并希望把这些 sample 按媒体时间显示。三者都处理视频,但控制权粒度不同。
跨层排查时应先看数据形态。文件无法打开,多半处在 asset 或容器解析阶段;某段时间无法 seek,通常要看 track time range、keyframe 和索引;解码失败,要看 sample 格式描述、codec profile 和 VideoToolbox session;显示黑屏,要看 sample timing、pixel format、layer 状态和主线程之外的队列关系;导出文件损坏,要看 writer 状态、时间戳单调性、输入结束标记和 finishWriting。同一个“视频失败”表象,落点可能在完全不同的层。
117.2 AVAsset、AVPlayer、AVSampleBufferDisplayLayer 的播放模型
AVAsset 是 Apple 媒体模型中的资产入口。资产表示一个基于时间的媒体资源,可以来自本地文件、网络资源或组合资源。它本身解决“媒体资源是什么”和“有哪些可读取的轨道、时长、格式和 metadata”的问题。AVAssetTrack 表示其中一条轨道,例如视频轨、音频轨、字幕轨或 metadata 轨。App 想做播放、读取、导出或编辑,通常先从 asset 和 track 建立媒体的静态结构。
AVPlayer 是面向播放状态机的高级对象。实际播放时,开发者通常把 AVAsset 放入 AVPlayerItem,再交给 AVPlayer。AVPlayerItem 持有当前播放项的 timing、状态、buffering、seek 和 track 选择信息;AVPlayer 持有播放控制和时钟推进;显示层通常由 AVPlayerLayer 承担。这个模型适合普通播放器,因为 App 只提出播放意图,系统接管读取、缓存、解码、音画同步和显示调度。
AVSampleBufferDisplayLayer 提供更低一级的显示入口。它接收 CMSampleBuffer,根据 sample 中的 timing 信息把画面排进显示队列。这个 layer 适合 App 自己负责读取、接收或生成 sample 的场景,例如低延迟流、解码后重封装显示、自定义 demux 后显示、WebRTC 类场景或采集预览中的特殊路径。使用它时,App 需要保证 sample 的时间戳、格式描述和队列状态正确;系统仍负责 layer 合成和屏幕输出,但 sample 排布责任已经向 App 侧移动。
这三个对象的差异可以用同一组维度比较:
| 对象 | 输入 | 系统接管范围 | App 控制范围 | 典型失败表现 |
|---|---|---|---|---|
AVAsset | URL、组合资源、媒体描述 | 解析媒体结构和轨道信息 | 选择轨道、时间范围、metadata | 时长未知、track 缺失、格式无法识别 |
AVPlayer / AVPlayerItem | asset 或 player item | 读取、缓冲、解码、同步、播放状态 | 播放、暂停、seek、速率、track selection | buffering、stall、seek 延迟、播放失败 |
AVSampleBufferDisplayLayer | CMSampleBuffer 队列 | layer 合成、按 timing 显示 | sample 生产、时间戳、flush、队列节奏 | 黑屏、乱序、延迟堆积、格式不匹配 |
在贯穿示例中,如果 App 只是预览源视频,AVPlayer 是稳定入口;如果 App 要逐帧送入 Metal 做滤镜,再把处理后画面预览出来,AVAssetReader 加 AVSampleBufferDisplayLayer 或自定义 Metal view 更合适。关键判断是:App 是否需要拿到每一帧的像素数据。需要逐帧处理时,播放模型要从 player 状态机切到 sample 流模型。
这里还要区分 asset 的“媒体时间”和屏幕的“显示时间”。asset 中的 track 使用媒体时间描述 sample 顺序;AVPlayer 会把媒体时间映射到播放时钟;AVSampleBufferDisplayLayer 依赖 sample buffer 的 timing 信息排队显示。画面处理后如果保留原 sample 的 presentation time,就能维持原视频节奏;如果 App 重新生成时间戳,就必须保证时间单调递增并和音频策略一致。
117.3 Core Media:CMTime、CMSampleBuffer、CMFormatDescription
Core Media 是 Apple 视频管线的时间和样本类型层。它不负责把画面画到屏幕上,也不直接表示 GPU texture;它解决媒体系统最底层的语义问题:一个样本对应什么格式、什么时间、持续多久、是否依赖别的 sample、是否携带颜色、旋转、同步或解码附件。理解 Core Media 后,播放、读取、编码和显示就会变成同一套 sample 流问题。
CMTime 是媒体时间的基础表示。它通常由 value 和 timescale 组成,表达“以某个时间刻度计数的有理数时间”。视频使用有理数时间比使用浮点秒数更稳定,因为 29.97 fps、44.1 kHz 音频采样、可变帧率、编辑 time range 都需要精确时间。CMTimeRange 用开始时间和持续时间表示片段范围。CMTimeMapping 用来描述源时间和目标时间之间的映射关系,常见于剪辑和组合资产。
CMSampleBuffer 是样本容器。对于压缩视频,它可以携带一段 encoded bitstream,例如 H.264 或 HEVC 的 access unit;对于解码后视频,它可以引用一个 CVImageBuffer,常见具体对象是 CVPixelBuffer。同一个类型可以出现在 reader 输出、decoder 输入输出、display layer 输入、writer 输入等位置,所以排查时要先判断它当前携带的是压缩数据还是像素数据。
CMFormatDescription 描述 sample 的格式。视频格式描述会包含 codec 类型、尺寸、扩展字段、颜色信息和 codec-specific data;音频格式描述会包含采样率、声道、编码格式等信息。VideoToolbox 创建 decode session 时需要根据 format description 判断 decoder 能力;AVSampleBufferDisplayLayer 接收 sample 时也依赖格式描述理解帧内容。格式描述缺失或不匹配,经常导致 session 创建失败、layer 不显示或 writer 拒绝 sample。
Core Media attachments 是跨层传递补充语义的通道。sample 或 image buffer 上的 attachment 可以携带帧依赖、显示立即性、颜色属性、HDR 相关信息、时间戳修正和编码控制提示。正文不需要展开每个键名,但要记住 attachment 具有边界作用:它让上游对象把“这帧如何解释、如何显示、如何编码”的信息交给下游。颜色错误、HDR 丢失、旋转异常、关键帧策略异常,常常和 attachment 或 metadata 处理相关。
下面的简化 Swift 骨架只展示 Core Media 对象如何在读取路径中出现。代码省去异步属性加载和错误分支,用于标出对象关系。
let asset = AVAsset(url: sourceURL)
let videoTrack = try await asset.loadTracks(withMediaType: .video).first!
let reader = try AVAssetReader(asset: asset)
let output = AVAssetReaderTrackOutput(
track: videoTrack,
outputSettings: [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
]
)
reader.add(output)
reader.startReading()
while let sampleBuffer = output.copyNextSampleBuffer() {
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let duration = CMSampleBufferGetDuration(sampleBuffer)
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
handleFrame(time: presentationTime, duration: duration, pixelBuffer: imageBuffer)
}
这段代码说明三件事。第一,reader output 可以被配置成输出像素 buffer,这意味着解码动作由 AVFoundation 内部路径接入系统 codec 完成。第二,App 拿到的仍然是 CMSampleBuffer,时间信息仍然在 Core Media 层。第三,真正交给 Metal 或显示层的图像数据在 image buffer 里,通常是 CVPixelBuffer。因此,逐帧处理不能把 sample buffer 当成单纯图片对象,它同时携带图像、时间和格式语义。
117.4 VideoToolbox:硬件 Decode / Encode 访问边界
VideoToolbox 是公开 API 中最接近系统 codec 能力的一层。它提供 VTDecompressionSession 和 VTCompressionSession,让 App 以 session 的方式请求解码和编码。session 的输入输出仍然使用 Core Media 和 Core Video 类型:解码输入通常是携带压缩数据的 CMSampleBuffer,解码输出通常是 CVImageBuffer / CVPixelBuffer;编码输入通常是 CVPixelBuffer 加 presentation time,编码输出通常是携带压缩数据的 CMSampleBuffer。
VTDecompressionSession 的边界可以这样理解:App 提供视频格式描述、解码规格和输出像素属性,系统根据 codec、profile、level、分辨率、像素格式、硬件能力和系统状态创建解码会话。解码调用提交 compressed sample,回调返回 image buffer、时间戳和状态。App 能控制输出 pixel format、是否实时、是否要求硬件加速等属性,但系统保留具体 decoder 选择、硬件资源调度、功耗策略和失败返回的控制权。
VTCompressionSession 的边界相反:App 提供宽高、codec 类型、编码属性和输入像素帧,系统返回压缩 sample。常见属性包括 bitrate、profile level、keyframe interval、real-time hint、allow frame reordering 等。编码器会把帧重排、码率控制、关键帧插入和压缩格式封装成 output sample。Writer 接收这些 sample 后才能形成容器文件。编码阶段对时间戳要求很严格:presentation time 需要单调推进,duration 和帧率策略要一致,结束时还要完整 drain session。
硬件加速边界需要使用保守表述。App 可以通过 VideoToolbox 请求硬件 codec 路径,也可以设置偏好或约束;系统会根据设备能力、codec 格式、分辨率、profile、受保护内容、后台状态、热状态和并发资源作出选择。公开 API 不提供“某一帧实际落在哪个硬件队列”的稳定承诺。工程判断应落到可观察结果:session 创建状态、回调状态码、输出 pixel buffer 属性、编码吞吐、功耗和发热表现。
下面这个顺序适合排查 VideoToolbox 失败:先确认 CMFormatDescription 和 codec data 完整,再确认输入 sample 时间戳可用,接着确认输出 pixel buffer 属性和下游兼容,之后观察 session 创建或 encode/decode 回调状态,最后把分辨率、profile、HDR、alpha、实时属性和后台状态作为能力约束检查。这个顺序把问题限定在“格式、时间、内存、能力、系统状态”五类边界里。
在贯穿示例中,如果源视频是 HEVC HDR,App 要输出 H.264 SDR,VideoToolbox 只负责 codec 转换本身。HDR 到 SDR 的 tone mapping、色彩空间转换、像素格式选择和 metadata 重写需要由 App 明确设计或交给更高层导出 API。把“解码成功”直接等同于“输出视觉正确”会漏掉颜色、range、matrix、transfer function 和 metadata 边界。
117.5 AVAssetReader:从 Asset 读取 Track 和 Sample
AVAssetReader 把 asset 变成可拉取的 sample 流。它适合离线分析、转码、导出、自定义滤镜、缩略图批处理和逐帧算法。Reader 的核心对象关系是:一个 reader 绑定一个 asset;每个 reader output 绑定一个 track 或组合输出;App 调用 startReading 后,通过 output 拉取 CMSampleBuffer,直到读取完成、失败或取消。
Reader 的输出形态由 output settings 决定。对于视频轨,AVAssetReaderTrackOutput 可以输出压缩 sample,也可以输出解码后的 pixel buffer。输出压缩 sample 时,App 拿到 encoded bitstream,后续可以自己交给 VideoToolbox 解码或直接重封装到 writer,但必须处理格式描述和 sample timing。输出 pixel buffer 时,系统在 reader 路径中完成解码,App 更容易接入 Core Image、Metal 或 CPU 图像处理。
Reader 的 time range 决定读取片段。App 可以把读取范围限制在 asset 的某段时间,例如从第 10 秒到第 20 秒。这个范围使用 CMTimeRange 表达。读取时还要注意 track 自身的时间映射、编辑列表、方向 transform 和可变帧率。导出裁剪片段时,保持 sample presentation time 的一致性比简单按帧计数可靠,因为视频帧率可能采用非整数或可变节奏。
Reader 状态是排查入口。reading 表示正在拉取;completed 表示输出已经耗尽;failed 表示 reader 或 output 出错;cancelled 表示被取消。拉取 sample 的循环必须观察这些状态,因为 copyNextSampleBuffer 返回空并不总是同一种原因。读取完成、读取失败、等待更多数据、上游取消,在后续 writer 行为上会产生不同结果。
在滤镜导出场景中,reader 需要和处理队列、writer 输入背压协作。writer input 准备接收时再拉取下一帧,可以控制内存占用;一次性把所有 sample 拉进内存会造成 pixel buffer 堆积。视频帧通常是大对象,4K 10-bit YUV 或 BGRA 帧会迅速放大内存压力。合理路径是 reader 拉一帧,处理一帧,writer 接一帧,依赖队列和 buffer pool 保持稳定吞吐。
下面的对象流说明 reader 不等于播放器。Reader 只提供 sample 获取能力;音画同步、实时播放时钟、缓冲策略和 UI 状态都要由更高层或 App 自己处理。
这张图的关键点是控制权位置。Reader 不主动推送帧,App 拉取 sample;writer 是否准备好接收也由 App 协调;处理管线不能丢掉 sample 的 presentation time。只要这三个控制点清楚,reader 场景中的卡顿、内存增长和导出时间错乱就能按边界排查。
117.6 AVAssetWriter:编码 Sample 到输出文件
AVAssetWriter 把 sample 流写成容器文件。它解决“输出到哪个 URL、生成什么文件类型、有哪些输出轨、每条轨道的格式和 metadata 如何写入”的问题。Writer 通常配合 AVAssetWriterInput 使用:视频 input 接收压缩 video sample 或通过 pixel buffer adaptor 接收像素帧;音频 input 接收 audio sample;metadata input 写入 metadata sample 或静态 metadata。
Writer 的基本顺序是创建 writer,添加 input,调用 startWriting,设置 startSession(atSourceTime:),不断 append sample,标记 input finished,最后调用 finishWriting。这个顺序体现了容器生成的状态机。startSession 决定输出文件时间轴起点;append 决定轨道 sample;finish 阶段写入容器尾部结构和索引。MP4 / MOV 这类容器常常需要最终化步骤,进程崩溃或 finish 未完成会导致文件无法正常播放。
Writer 有两种常见输入路径。第一种是 sample 直写:reader 输出压缩 sample,writer input 接收这些 sample,适合剪切、重封装或不改画面的转封装路径。第二种是像素写入:reader 输出 pixel buffer,App 处理后通过 AVAssetWriterInputPixelBufferAdaptor 把 pixel buffer 和 presentation time append 给 writer,writer 再通过编码设置生成压缩视频。滤镜、缩放、水印、色彩转换通常走第二种路径。
时间戳是 writer 的核心约束。每个 sample 或 pixel buffer append 时都带有 presentation time;同一轨道的时间通常要单调推进;音频和视频要共享可解释的时间基准。裁剪视频时,App 可以把第一帧时间归零,也可以保留源时间,但 writer session 和所有轨道必须使用一致策略。很多“导出文件开头黑屏”“音画偏移”“写入失败”的原因,是视频帧、音频帧和 writer session 起点没有对齐。
Writer input 的背压决定生产节奏。isReadyForMoreMediaData 表示当前 input 是否可以接收更多 sample。高分辨率编码、慢存储、热状态和后台状态都可能降低接收速度。稳定的离线导出流程应让 reader 和处理管线跟随 writer input 的 ready 信号推进。这个模型把内存压力控制在有限的 frame queue 内,也能让编码器按系统节奏消化输入。
下面的简化代码展示像素帧写入的关键对象。代码属于精简骨架,用来标出 writer input、pixel buffer adaptor 和 timestamp 的关系。
let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mov)
let input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
input.expectsMediaDataInRealTime = false
let adaptor = AVAssetWriterInputPixelBufferAdaptor(
assetWriterInput: input,
sourcePixelBufferAttributes: pixelBufferAttributes
)
writer.add(input)
writer.startWriting()
writer.startSession(atSourceTime: .zero)
input.requestMediaDataWhenReady(on: encodeQueue) {
while input.isReadyForMoreMediaData {
guard let frame = nextProcessedFrame() else {
input.markAsFinished()
writer.finishWriting {}
return
}
let outputTime = frame.presentationTime - firstFrameTime
adaptor.append(frame.pixelBuffer, withPresentationTime: outputTime)
}
}
这段代码的关键点是 writer 驱动节奏。App 在 input ready 时提供下一帧;像素和时间戳一起提交;输出时间被统一到从零开始的时间轴。实际工程还要处理失败状态、音频轨对齐、取消、后台任务、临时文件替换和 metadata 写入,但这些细节都围绕同一个边界:writer 负责容器和编码输入状态,App 负责样本顺序和时间一致性。
117.7 Core Video、Pixel Buffer 与 Render Path
Core Video 的核心对象是 CVPixelBuffer。它表示一帧可被视频系统、图像处理系统和图形系统共享的像素内存。Pixel buffer 包含宽高、像素格式、plane 布局、bytes per row、attachment 和底层内存属性。视频系统常见格式包括 bi-planar YUV、BGRA、10-bit 格式和 HDR 相关格式。像素格式选择会影响解码输出、Metal shader、颜色转换、编码输入和内存带宽。
CVPixelBufferPool 用来复用 pixel buffer。视频处理是持续帧流,频繁分配释放大块图像内存会增加延迟和内存波动。Pool 通过固定属性创建一组可复用 buffer,让 decoder、processor 或 writer adaptor 复用同类内存。实时预览和导出都受益于稳定的 buffer 生命周期,尤其是 4K、HDR、慢动作或多路流处理场景。
IOSurface 是 Apple 平台中跨框架共享图像内存的重要底层机制。很多 CVPixelBuffer 可以由 IOSurface 支持,从而被 VideoToolbox、Core Image、Metal、Core Animation 等框架共享。公开层面可以把它理解为“可跨进程或跨框架引用的图像内存容器”。具体是否零拷贝取决于 pixel format、alignment、texture 兼容属性、颜色转换、处理链和系统资源状态;工程判断应看是否需要格式转换和额外中间 buffer。
Metal 接入 pixel buffer 通常通过 CVMetalTextureCache。它把 CVPixelBuffer 的 plane 包装成 CVMetalTexture,再取得 MTLTexture 供 shader 使用。YUV buffer 往往有多个 plane,例如亮度 plane 和色度 plane;Metal shader 需要按 plane 采样并做颜色转换。BGRA buffer 可以直接作为单个 texture 处理,但可能增加解码后的带宽和内存占用。选择 YUV 还是 BGRA,要根据 shader 需求、编码输入、显示路径和性能预算决定。
显示路径有两类常见做法。第一类是把 sample buffer 交给 AVSampleBufferDisplayLayer,让 layer 按 timing 显示。第二类是把 pixel buffer 转成 Metal texture,在 MTKView 或自定义渲染层里绘制。前者适合 sample timing 已经完整且渲染需求简单的路径;后者适合滤镜、合成、特效、画中画、多纹理混合和自定义 tone mapping。两者都要处理颜色空间、range、rotation 和 display timing。
在贯穿示例中,reader 输出 CVPixelBuffer,Metal shader 叠加滤镜,处理后的 buffer 再送 writer。这里有两个稳定做法。第一,输入 pixel buffer 和输出 pixel buffer 分开管理,输出使用 writer adaptor 兼容的 pool 创建。第二,保留原始 presentation time,只修改像素内容。这样可以把渲染问题和时间问题分开:画面效果出错时查 Metal 和颜色转换,音画时间出错时查 Core Media timing 和 writer session。
Render path 中的“零拷贝”应当被当成目标和假设检查项,而非默认事实。只要某一层要求不同像素格式、不同存储模式、CPU 访问、颜色转换或尺寸变化,系统可能插入拷贝或转换。排查性能时,先固定输入输出格式,再检查 pixel buffer attributes、IOSurface 兼容、Metal texture cache 使用方式、shader 采样格式和 writer 输入格式,最后通过 Instruments 或实际帧耗时观察瓶颈位置。
117.8 Apple Video Path:Asset → Reader / Decoder → Pixel Buffer / Layer → Writer
Apple video path 可以收束成四个连续边界:asset 边界回答“媒体资源和轨道是什么”;reader / decoder 边界回答“压缩 sample 如何变成可处理帧”;pixel buffer / layer 边界回答“图像内存如何显示或被 GPU 处理”;writer 边界回答“处理后的 sample 如何形成输出文件”。播放、导出、转码、录制虽然入口不同,但都会在这些边界之间移动数据。
播放路径偏向系统接管。AVPlayer 从 asset 或 item 开始,系统内部完成读取、缓存、解码、时钟同步和 layer 显示。App 主要控制播放状态、seek、rate、track selection 和 UI。普通播放器的问题应先从 player item 状态、buffering、network、seek、DRM 和输出 layer 查起。
导出和转码路径偏向 sample 控制。AVAssetReader 从 asset 拉取 sample,App 可以选择压缩 sample 或 pixel buffer;需要改画面时进入 pixel buffer 和 Metal;需要改编码时进入 VideoToolbox 或 writer 的编码路径;最后 AVAssetWriter 生成新文件。这个路径的关键证据是 sample timing、format description、pixel format、writer status 和 finish 状态。
录制路径从采集源进入同一模型。相机、屏幕或麦克风产生 sample buffer;视频帧可能已经是 CVPixelBuffer;编码器把 pixel buffer 变成压缩 sample;writer 把视频、音频和 metadata 写入文件。录制和离线转码共用 writer 边界,但录制多了实时性、权限、采集 session、音视频 drift 和后台策略。本章不展开相机系统,只把录制理解为“实时 sample 进入 writer”的同类路径。
统一判断顺序可以这样执行:先确认当前对象是 asset、track、sample buffer、pixel buffer、texture、layer 还是 output file;再确认它处在读取、解码、处理、显示、编码或写入哪个阶段;然后检查该阶段最关键的元数据:asset 的 track 和 time range、sample 的 timing 和 format description、pixel buffer 的 format 和 attachment、VideoToolbox session 的状态、layer 的队列和 flush 状态、writer 的 input ready 和 finish 状态;最后根据用户可见表现定位失败边界。
下面这张表把常见现象映射到责任边界:
| 用户可见现象 | 优先检查对象 | 责任边界 | 典型原因 |
|---|---|---|---|
| 文件无法读取 | AVAsset、track、URL 权限 | asset / container | 文件权限、容器损坏、track 缺失、格式不受支持 |
| 播放卡住 | AVPlayerItem、buffer 状态 | player / buffering | 网络抖动、seek 后缓冲重建、解码能力受限 |
| 自定义显示黑屏 | CMSampleBuffer、display layer | sample timing / layer | 时间戳无效、格式描述缺失、layer flush 状态错误 |
| 滤镜后颜色异常 | CVPixelBuffer、Metal shader | pixel buffer / render | YUV range、色彩矩阵、HDR metadata、plane 采样错误 |
| 编码失败 | VTCompressionSession、writer input | codec / writer | profile 不支持、时间戳乱序、pixel format 不匹配 |
| 导出文件打不开 | AVAssetWriter、finish 状态 | container finalization | 输入未结束、finish 失败、进程中断、临时文件替换失败 |
这个统一模型也说明 Apple media stack 的平台边界。App 使用公开 API 操作资产、sample、pixel buffer、texture、layer 和 writer;系统负责权限、sandbox 文件访问、硬件 codec 调度、内存共享、显示合成和功耗约束。App 能稳定控制的是对象关系、时间戳、格式属性、队列节奏和错误处理;系统保留的是硬件选择、后台策略、受保护内容路径和具体驱动实现。
本章开头的短视频滤镜导出行为,现在可以被完整复盘:源文件进入 AVAsset;video track 进入 AVAssetReader;reader output 产生带 timing 的 CMSampleBuffer;解码后图像作为 CVPixelBuffer 出现;Metal 通过 texture cache 读取并写出处理结果;预览可以交给 display layer 或自定义 Metal view;输出帧带着重建后的 presentation time 进入 writer;writer 完成编码和容器 finalization。读者只要沿着这个对象链定位,就能把大多数 Apple 视频问题归入明确责任边界。
最小自检任务
一个 iOS App 要实现“选择一个本地 MOV 文件,截取中间 10 秒,给视频加一层 Metal 滤镜,预览处理结果,并导出为新的 H.264 MOV 文件”。请写出从源文件到输出文件的对象路径,并说明每个阶段的责任主体、最关键的时间或格式信息、可能失败的边界以及用户能看到的结果。
答案要点
路径应从 AVAsset(url:) 开始,先读取 video track 和目标 CMTimeRange,再用 AVAssetReader 建立 track output。需要逐帧滤镜时,reader output 应输出可处理的 CVPixelBuffer,每个输出 sample 仍要保留 CMSampleBuffer 中的 presentation time 和 duration。这个阶段的责任主体是 AVFoundation reader,关键检查点是 track 是否存在、time range 是否落在有效范围内、reader 状态是否进入 failed 或 completed。
处理阶段以 Core Video 和 Metal 为边界。CVPixelBuffer 提供图像内存,必要时通过 CVMetalTextureCache 转成 MTLTexture,shader 处理后写入与 writer adaptor 兼容的输出 pixel buffer。关键格式信息包括 pixel format、YUV plane、color range、color matrix、HDR attachment 和 buffer pool 属性。失败表现通常是预览黑屏、颜色异常、帧耗时过高或内存增长。
预览阶段可以使用 AVSampleBufferDisplayLayer 或自定义 Metal view。使用 display layer 时,App 需要提交带有效 timing 和 format description 的 sample buffer;使用 Metal view 时,App 自己负责显示节奏和纹理绘制。用户看到的结果是实时或准实时预览;失败边界集中在 sample timing、layer flush 状态、texture 创建和渲染队列。
导出阶段使用 AVAssetWriter、video input 和 AVAssetWriterInputPixelBufferAdaptor。Writer session 可以从 .zero 开始,因此每帧输出时间应使用源 presentation time 减去截取片段第一帧时间。Writer input 的 isReadyForMoreMediaData 决定 App 何时提交下一帧。结束时要标记 input finished 并调用 finishWriting。失败表现包括编码失败、音画偏移、导出文件无法打开、文件开头黑屏和取消后临时文件残留。
完整判断顺序是:先定位当前对象形态,再定位阶段,再检查该阶段的核心元数据。Asset 阶段看 track 和 time range;sample 阶段看 timing 和 format description;pixel 阶段看 pixel format 和 attachments;Metal 阶段看 texture plane 和颜色转换;writer 阶段看时间戳单调性、input ready、编码属性和 finish 状态。这个顺序能把同一个“导出失败”拆成可验证的责任边界。
本章知识点总结
- 总路径:Apple 视频管线可以按 asset、sample、pixel buffer、texture、layer、writer 这些对象边界追踪。
- AVFoundation:AVFoundation 负责把媒体资源、轨道、播放、读取和写入组织成 App 可用的高级对象。
- 播放模型:
AVPlayer适合系统接管播放状态机,AVSampleBufferDisplayLayer适合 App 已经持有 sample 流的显示路径。 - 媒体时间:
CMTime和CMTimeRange用精确时间描述帧位置、片段范围和输出时间轴。 - 样本容器:
CMSampleBuffer同时携带数据、时间、格式和附件语义,需要先判断其中是压缩数据还是像素数据。 - 格式描述:
CMFormatDescription是 decoder、display layer 和 writer 理解 sample 内容的关键依据。 - 编解码边界:VideoToolbox 用 session 暴露系统 codec 能力,具体硬件调度仍由系统控制。
- Reader 边界:
AVAssetReader把 asset 拉成 sample 流,App 负责拉取节奏、处理队列和读取状态检查。 - Writer 边界:
AVAssetWriter把 sample 或 pixel buffer 写成容器文件,时间戳、input ready 和 finish 状态决定输出稳定性。 - Pixel Buffer:
CVPixelBuffer是解码、图像处理、Metal 渲染和编码之间共享图像内存的核心对象。 - Metal 接入:
CVMetalTextureCache把 pixel buffer plane 包装成 Metal texture,颜色转换和 plane 采样由渲染路径承担。 - 零拷贝判断:零拷贝依赖像素格式、IOSurface 兼容、texture 属性和下游需求,需要用实际格式和耗时验证。
- 失败定位:黑屏、卡顿、颜色异常、编码失败和文件损坏应分别落到 timing、buffer、format、codec、writer finalization 等边界。
- 统一模型:播放、导出、转码和录制都可以被整理为 Asset → Reader / Decoder → Pixel Buffer / Layer → Writer 的样本流模型。