音视频 iOS 面试题 | 音视频面试题集锦 49 期

来自”关键帧Keyframe”整理的音视频面试题集锦第 49 期之音视频 iOS 面试题。

1、【原理篇】请详述音视频同步(AVSync)的策略。如果视频播放比音频慢(视频滞后),在代码层面应该如何进行“追赶”?

考察点: PTS/DTS 理解、同步时钟选择、丢帧策略。

参考答案:

核心策略:通常有三种同步策略:

  1. 将视频同步到音频(Audio Master): 这是业界最常用的方案。因为音频是由硬件声卡时钟驱动的,采样率固定,回放不连续会被人耳轻易察觉(爆音或静音),而人眼对视频帧率的细微抖动不敏感。
  2. 将音频同步到视频: 极少使用,因为视频帧渲染耗时波动大。
  3. 两者同步到系统时钟: 较复杂,需处理两个流的漂移。

实现逻辑(基于 Audio Master):

  1. 获取时钟差 (Diff):Diff = CurrentAudioPTS - CurrentVideoPTS
    • CurrentAudioPTS:当前正在播放的音频帧的时间戳。
    • CurrentVideoPTS:当前即将渲染的视频帧的时间戳。
  2. 阈值判断: 设定一个同步阈值(如 0.05s)。
    • 如果 |Diff| < Threshold:同步良好,直接渲染。
    • 如果 Diff > 0 (视频落后音频):视频需要追赶
    • 如果 Diff < 0 (视频超前音频):视频需要等待

“追赶”的具体代码策略:如果视频滞后(Diff > 0),即音频已经播到第 10 秒,视频还在第 9 秒,需要加快视频消耗:

  1. 丢帧 (Drop Frame):
    • 直接丢弃解码后的 CVPixelBuffer,不送入 OpenGL/Metal 渲染管线。
    • 注意关键帧 (I-Frame): 尽量丢弃非参考帧(P/B帧)。如果滞后严重,需要丢弃整个 GOP (Group of Pictures) 直到下一个 I 帧,否则会花屏。
  2. 加速播放: 减少视频帧之间的 sleep 时间,快速渲染当前缓冲队列中的帧。

2、【底层视频篇】在使用 VideoToolbox 进行 H.264 硬解码时,如何处理 SPS/PPS 信息?解码出的 CVPixelBuffer 数据格式通常是什么?如何优化渲染性能?

考察点: VideoToolbox 流程、H.264 码流结构、YUV 格式、纹理缓存。

参考答案:

1. SPS/PPS 处理:VideoToolbox 的解码器 (VTDecompressionSession) 需要根据 SPS (Sequence Parameter Set) and PPS (Picture Parameter Set) 来初始化 CMVideoFormatDescriptionRef

  • 在 H.264 码流(Annex-B 格式)中,SPS 和 PPS 通常位于 IDR 帧之前。
  • 需要将 SPS/PPS 数据提取出来,封装成 CMVideoFormatDescriptionCreateFromH264ParameterSets 函数所需的参数。
  • 注意: VideoToolbox 接受的 NALU 数据通常要求是 AVCC 格式(前 4 字节为长度 Header),而不是 Annex-B(前 4 字节为 00 00 00 01 Start Code)。喂给解码器前需要做转换。

2. 数据格式:iOS 硬解码输出的 CVPixelBuffer 格式通常是 **kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange (NV12)**。

  • BiPlanar (双平面): Y 数据为一个平面,UV 数据交错存储在另一个平面。
  • 相比 YUV420P(三个平面),NV12 在内存读取和 Metal/OpenGL 上传时更高效。

3. 渲染性能优化 (Zero-Copy):不要使用 glReadPixels 或手动内存拷贝将 YUV 转 RGB。

  • OpenGL ES: 使用 CVOpenGLESTextureCacheCreateTextureFromImage。这利用了 iOS 的纹理缓存机制,直接将 CVPixelBuffer 映射为 OpenGL 纹理,实现 0 拷贝 上传。
  • Metal: 使用 CVMetalTextureCacheCreateTextureFromImage 创建 MTLTexture,然后在 Shader 中直接采样 Y 和 UV 纹理进行颜色空间转换。

3、【底层音频篇】在 Audio Unit 的 Render Callback 回调函数中,为什么不能使用 Objective-C 的 [self method] 调用?如何安全地将数据传递给 OC 层?

考察点: 实时音频线程限制、锁竞争、C与OC混编技巧。

参考答案:

为什么不能调用 OC 方法:Audio Unit 的 AURenderCallback 运行在高优先级的实时音频线程上。

  1. Obj-C 消息发送开销:objc_msgSend 虽然快,但涉及运行时查找,且可能会触发缓存填充等内存操作,存在不确定性耗时。
  2. 系统锁与内存分配: 绝对禁止在回调中进行 malloc/free(Obj-C 对象创建和销毁通常涉及这些)、加锁(@synchronized / NSLock)或任何可能导致线程阻塞的操作(I/O)。如果回调处理时间超过音频缓冲区时长(如 44.1kHz 下 1024 帧约 23ms),会导致音频 Glitch(爆音/卡顿)

如何安全交互:

  1. C 上下文传参:在注册回调时,将 self 转换为 void * 指针传入:// 注册回调
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = RenderCallback;
    callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self); // 传入 self 指针
  2. C 函数内部访问:在 C 函数内部强转回对象指针,直接访问预先分配好内存的 C 结构体或成员变量(如 struct 或 C++ 成员),避免直接调用 OC 方法。static OSStatus RenderCallback(void *inRefCon, ...) {
        // 危险操作: [(__bridge MyClass *)inRefCon doSomething]; // 尽量避免
        
        // 推荐操作:访问预分配的 C 结构体
        MyContext *context = (MyContext *)inRefCon;
        // 使用 TPCircularBuffer 存取数据
        TPCircularBufferProduceBytes(&context->circularBuffer, ...);
        return noErr;
    }
  3. 无锁环形缓冲区 (Lock-free Circular Buffer):如果要将录制的 PCM 数据抛给 OC 层(如写入文件),应使用 TPCircularBuffer 将数据写入 Buffer,然后在非实时线程中去读取并处理,实现生产者-消费者模型。

4、【内存管理篇】使用 AVAssetReader + AVAssetWriter 进行视频转码或逐帧处理时,内存暴涨直至 Crash 的常见原因是什么?请给出 OC 代码层面的解决方案。

考察点: 自动释放池、CVPixelBufferPool、循环引用。

参考答案:

核心原因:在使用 copyNextSampleBuffer 逐帧读取视频数据时,系统会生成大量的临时对象(CMSampleBufferCVPixelBuffer 以及内部的元数据字典)。 虽然 ARC 会管理内存,但在一个紧凑的 while 循环中,RunLoop 没有机会切换,导致 autorelease 对象无法及时释放,堆积在内存中。

OC 解决方案:必须在循环体内部显式添加 @autoreleasepool

[reader startReading];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];

dispatch_queue_t processingQueue = dispatch_queue_create("video_processing", NULL);
dispatch_async(processingQueue, ^{
    while (reader.status == AVAssetReaderStatusReading) {
        // 【关键点】添加自动释放池
        @autoreleasepool {
            // 1. 读取 SampleBuffer
            CMSampleBufferRef sampleBuffer = [readerOutput copyNextSampleBuffer];
            if (sampleBuffer) {
                // 2. 处理图像 (例如滤镜)
                CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);

                // ... 对 pixelBuffer 进行 OpenGL/Metal 处理 ...

                // 3. 写入 (假设 writerInput 准备好了)
                if (writerInput.readyForMoreMediaData) {
                    [writerInput appendSampleBuffer:sampleBuffer];
                }

                // 4. 【关键点】手动 Release CoreFoundation 对象
                CFRelease(sampleBuffer); 
            } else {
                [writerInput markAsFinished];
                [writer finishWritingWithCompletionHandler:^{...}];
                break;
            }
        }
    }
});

进阶补充:如果涉及大量的 CVPixelBuffer 创建(例如滤镜处理后生成新帧),务必使用 CVPixelBufferPool 来复用内存,避免反复的 alloc/dealloc 造成内存碎片和 CPU 消耗。

5、【架构设计篇】如何设计一个支持秒开、低延时的 iOS 直播播放器?请从网络层、解码层、渲染层三个维度简述优化点。

考察点: 全链路优化思维、协议理解、队列管理。

参考答案:

1. 网络层 (Jitter Buffer & Protocol):

  • 协议选择: 优先使用 FLV (HTTP-FLV) 或 RTMP,避免使用 HLS (切片延时大)。WebRTC 是终极低延时方案。
  • DNS 预解析: App 启动时预先解析推流/拉流域名 IP,减少首次连接耗时。
  • 动态缓冲策略 (Jitter Buffer):
    • 首帧秒开: 服务端下发 GOP 缓存(通常是最近一个关键帧),客户端收到数据后立即解码渲染,不等待缓冲区填满。
    • 追帧策略: 当检测到缓冲区堆积过高(延时变大)时,通过倍速播放(如 1.2x 播放音频)或丢弃非关键帧来消耗缓冲区。

2. 解码层 (Hardware & Preload):

  • 硬解优先: 使用 VideoToolbox 硬解码,降低 CPU 占用。
  • 异步解码: 网络接收线程只负责收包,放入队列;解码线程异步从队列取包解码,避免网络抖动阻塞解码。
  • 首帧优化: 解析到第一个 I 帧(关键帧)后,立即触发渲染逻辑,不需要等待 B 帧或音频包(此时可能音画暂时不同步,但用户看到了画面,体感“秒开”)。

3. 渲染层 (0-Copy & Pre-render):

  • 纹理缓存: 如第 2 题所述,利用 CVOpenGLESTextureCache 或 Metal 纹理缓存,避免内存拷贝。
  • YUV 直接渲染: Shader 直接处理 YUV 数据,避免 CPU 转 RGB。
  • 预渲染 (Pre-render): 在 UI 层面,播放器 View 初始化时先展示封面图(Cover Image),底层视频流准备好第一帧后,做一个平滑的淡入效果切换,视觉上消除黑屏等待感。

学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

音视频 iOS 面试题 | 音视频面试题集锦 49 期

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论