我们如何从 iPhone 相机中获取 H.264 编码的视频流?

Posted

技术标签:

【中文标题】我们如何从 iPhone 相机中获取 H.264 编码的视频流?【英文标题】:How can we get H.264 encoded video stream from iPhone Camera? 【发布时间】:2013-06-10 06:51:47 【问题描述】:

我正在使用以下获取视频样本缓冲区:

- (void) writeSampleBufferStream:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType

现在我的问题是如何从 sampleBuffer 上方获取 h.264 编码的 NSData。请提出建议。

【问题讨论】:

【参考方案1】:

2017 年更新:

您现在可以使用VideoToolbox API 进行流式视频和音频。 在此处阅读文档:VTCompressionSession

原始答案(2013 年):

简短:你不能,你收到的样本缓冲区是未压缩的。

获得硬件加速h264压缩的方法:

AVAssetWriter AVCaptureMovieFileOutput

如您所见,两者都写入文件,写入管道不起作用,因为编码器会在帧或 GOP 完全写入后更新标头信息。所以你最好不要在编码器写入文件时触摸文件,因为它会随机重写标题信息。如果没有此标头信息,视频文件将无法播放(它会更新大小字段,因此写入的第一个标头表示文件为 0 字节)。目前不支持直接写入内存区域。但是您可以打开编码的视频文件并对流进行解复用以获取 h264 数据(编码器当然关闭文件之后)

【讨论】:

【参考方案2】:

您只能从 AVFoundation 获取 BGRA 或 YUV 颜色格式的原始视频图像。但是,当您通过 AVAssetWriter 将这些帧写入 mp4 时,它们将使用 H264 编码进行编码。

RosyWriter 是一个很好的示例,其中包含有关如何执行此操作的代码

请注意,在每次 AVAssetWriter 写入后,您将知道一个完整的 H264 NAL 已写入 mp4。您可以编写在 AVAssetWriter 每次写入后读取完整 H264 NAL 的代码,这将使您能够访问 H264 编码帧。以不错的速度完成它可能需要一点时间,但它是可行的(我成功地做到了)。

顺便说一句,为了成功解码这些编码的视频帧,您将需要位于 mp4 文件中不同位置的 H264 SPS 和 PPS 信息。就我而言,我实际上创建了几个测试 mp4 文件,然后手动将它们提取出来。由于这些不会改变,除非您更改 H264 编码规范,否则您可以在代码中使用它们。

查看我发给 SPS values for H 264 stream in iPhone 的帖子,了解我在代码中使用的一些 SPS/PPS。

最后一点,就我而言,我必须将 h264 编码帧流式传输到另一个端点以进行解码/查看;所以我的代码必须快速完成。就我而言,它相对较快。但最终我切换到 VP8 进行编码/解码只是因为它更快,因为一切都在内存中完成,无需文件读取/写入。

祝你好运,希望这些信息有所帮助。

【讨论】:

【参考方案3】:

使用 VideoToolbox API。参考:https://developer.apple.com/videos/play/wwdc2014/513/

import Foundation
import AVFoundation
import VideoToolbox

public class LiveStreamSession 
    
    let compressionSession: VTCompressionSession
        
    var index = -1
    
    var lastInputPTS = CMTime.zero
    
    public init?(width: Int32, height: Int32)
        var compressionSessionOrNil: VTCompressionSession? = nil
        let status = VTCompressionSessionCreate(allocator: kCFAllocatorDefault,
                                                width: width,
                                                height: height,
                                                codecType: kCMVideoCodecType_H264,
                                                encoderSpecification: nil, // let the video toolbox choose a encoder
                                                imageBufferAttributes: nil,
                                                compressedDataAllocator: kCFAllocatorDefault,
                                                outputCallback: nil,
                                                refcon: nil,
                                                compressionSessionOut: &compressionSessionOrNil)
        guard status == noErr,
            let compressionSession = compressionSessionOrNil else 
            return nil
        
        VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue);
        VTCompressionSessionPrepareToEncodeFrames(compressionSession)
        
        self.compressionSession = compressionSession
        
    
    
    public func pushVideoBuffer(buffer: CMSampleBuffer) 
        // image buffer
        guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else 
            assertionFailure()
            return
        
        
        // pts
        let pts = CMSampleBufferGetPresentationTimeStamp(buffer)
        guard CMTIME_IS_VALID(pts) else 
            assertionFailure()
            return
        
        
        // duration
        var duration = CMSampleBufferGetDuration(buffer);
        if CMTIME_IS_INVALID(duration) && CMTIME_IS_VALID(self.lastInputPTS) 
            duration = CMTimeSubtract(pts, self.lastInputPTS)
        
                
        index += 1
        self.lastInputPTS = pts
        print("[\(Date())]: pushVideoBuffer \(index)")
        
        let currentIndex = index
        VTCompressionSessionEncodeFrame(compressionSession, imageBuffer: imageBuffer, presentationTimeStamp: pts, duration: duration, frameProperties: nil, infoFlagsOut: nil) [weak self] status, encodeInfoFlags, sampleBuffer in
            print("[\(Date())]: compressed \(currentIndex)")
            if let sampleBuffer = sampleBuffer 
                self?.didEncodeFrameBuffer(buffer: sampleBuffer, id: currentIndex)
            
        
    
    
    deinit 
        VTCompressionSessionInvalidate(compressionSession)
    
    
    private func didEncodeFrameBuffer(buffer: CMSampleBuffer, id: Int) 
        guard let attachments = CMSampleBufferGetSampleAttachmentsArray(buffer, createIfNecessary: true)
               else 
            return
        
        let dic = Unmanaged<CFDictionary>.fromOpaque(CFArrayGetValueAtIndex(attachments, 0)).takeUnretainedValue()
        let keyframe = !CFDictionaryContainsKey(dic, Unmanaged.passRetained(kCMSampleAttachmentKey_NotSync).toOpaque())
//        print("[\(Date())]: didEncodeFrameBuffer \(id) is I frame: \(keyframe)")
        if keyframe,
           let formatDescription = CMSampleBufferGetFormatDescription(buffer) 
            // https://www.slideshare.net/instinctools_EE_Labs/videostream-compression-in-ios
            var number = 0
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescription, parameterSetIndex: 0, parameterSetPointerOut: nil, parameterSetSizeOut: nil, parameterSetCountOut: &number, nalUnitHeaderLengthOut: nil)
            // SPS and PPS and so on...
            let parameterSets = NSMutableData()
            for index in 0 ... number - 1 
                var parameterSetPointer: UnsafePointer<UInt8>?
                var parameterSetLength = 0
                CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescription, parameterSetIndex: index, parameterSetPointerOut: &parameterSetPointer, parameterSetSizeOut: &parameterSetLength, parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil)
//                parameterSets.append(startCode, length: startCodeLength)
                if let parameterSetPointer = parameterSetPointer 
                    parameterSets.append(parameterSetPointer, length: parameterSetLength)
                
                
                //
                if index == 0 
                    print("SPS is \(parameterSetPointer) with length \(parameterSetLength)")
                 else if index == 1 
                    print("PPS is \(parameterSetPointer) with length \(parameterSetLength)")
                
            
            print("[\(Date())]: parameterSets \(parameterSets.length)")
        
    

【讨论】:

以上是关于我们如何从 iPhone 相机中获取 H.264 编码的视频流?的主要内容,如果未能解决你的问题,请参考以下文章

如何从iphone相机胶卷中获取专辑列表[重复]

iPhone:如何使用 iPhone 照片应用程序等“编辑”功能从照片库或相机中获取图像

从 iPhone 相机胶卷中获取图像

如何获取 h264 视频信息?

检测 iPhone 相机方向

在 UIWebView 中显示来自相机的图像