https://www.jianshu.com/p/3c13017ad492
https://github.com/tomisacat/VideoToolboxCompression
https://blog.csdn.net/meiwenjie110/article/details/69524432
从iOS8开始,苹果将VideoToolbox.framework开放了出来,使开发者可以使用iOS设备内置的硬件设备来进行视频的编码和解码工作。硬件编解码的好处是,复杂的计算由专门的硬件电路完成,往往比使用cpu计算更高效,速度更快,功耗也更低。H.264是目前很流行的编码层视频压缩格式,目前项目中的协议层有rtmp与http,但是视频的编码层都是使用的H.264。
关于H.264编码格式相关知识请看深入浅出理解视频编码H264结构本篇不再赘述。在iOS平台上对视频数据进行H.264编码有两种方式:
- 软件编码:用ffmpeg等开源库进行编码,他是用cpu进行相关计算的,效率比较低,但是比较通用,是跨平台的。
- 硬件编码:用VideoToolbox今天编码,他是用GPU进行相关计算的,效率很高。
在熟悉H.264的过程中,为更好的了解H.264,尝试用VideoToolbox硬编码与硬解码H.264的原始码流。
今天我们主要来看看使用VideoToolbox硬编码H.264。
用VideoToolbox硬编码H.264步骤如下:
1.初始化摄像头,output设定的时候,需要设置delegate和输出队列。在delegate方法,处理采集好的图像。
2.初始化VideoToolbox,设置各种属性。
3.获取每一帧数并编码。
4.每一帧数据编码完成后,在回调方法中判断是不是关键帧,如果是关键帧需要用CMSampleBufferGetFormatDescription获取CMFormatDescriptionRef,然后用
CMVideoFormatDescriptionGetH264ParameterSetAtIndex取得PPS和SPS;最后把每一帧的所有NALU数据前四个字节变成0x00 00 00 01之后再写入文件。
5.循环步骤3步骤4。
6.调用VTCompressionSessionCompleteFrames完成编码,然后销毁session:VTCompressionSessionInvalidate,释放session。


Sample代码
事实上,使用 VideoToolbox 硬编码的用途大多是推流编码后的 NAL Unit 而不是写入到本地一个 H.264 文件// 如果你想保存到本地,使用 AVAssetWriter 是一个更好的选择,它内部也是会硬编码的。
ViewController.swift
// // ViewController.swift // VideoToolboxCompression // // Created by tomisacat on 12/08/2017. // Copyright © 2017 tomisacat. All rights reserved. // import UIKit import AVFoundation import VideoToolbox fileprivate var NALUHeader: [UInt8] = [0, 0, 0, 1] // 事实上,使用 VideoToolbox 硬编码的用途大多是推流编码后的 NAL Unit 而不是写入到本地一个 H.264 文件 // 如果你想保存到本地,使用 AVAssetWriter 是一个更好的选择,它内部也是会硬编码的 func compressionOutputCallback(outputCallbackRefCon: UnsafeMutableRawPointer?, sourceFrameRefCon: UnsafeMutableRawPointer?, status: OSStatus, infoFlags: VTEncodeInfoFlags, sampleBuffer: CMSampleBuffer?) -> Swift.Void { guard status == noErr else { print("error: \(status)") return } if infoFlags == .frameDropped { print("frame dropped") return } guard let sampleBuffer = sampleBuffer else { print("sampleBuffer is nil") return } if CMSampleBufferDataIsReady(sampleBuffer) != true { print("sampleBuffer data is not ready") return } // let desc = CMSampleBufferGetFormatDescription(sampleBuffer) // let extensions = CMFormatDescriptionGetExtensions(desc!) // print("extensions: \(extensions!)") // // let sampleCount = CMSampleBufferGetNumSamples(sampleBuffer) // print("sample count: \(sampleCount)") // // let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer)! // var length: Int = 0 // var dataPointer: UnsafeMutablePointer<Int8>? // CMBlockBufferGetDataPointer(dataBuffer, 0, nil, &length, &dataPointer) // print("length: \(length), dataPointer: \(dataPointer!)") let vc: ViewController = Unmanaged.fromOpaque(outputCallbackRefCon!).takeUnretainedValue() if let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true) { print("attachments: \(attachments)") let rawDic: UnsafeRawPointer = CFArrayGetValueAtIndex(attachments, 0) let dic: CFDictionary = Unmanaged.fromOpaque(rawDic).takeUnretainedValue() // if not contains means it's an IDR frame let keyFrame = !CFDictionaryContainsKey(dic, Unmanaged.passUnretained(kCMSampleAttachmentKey_NotSync).toOpaque()) if keyFrame { print("IDR frame") // sps let format = CMSampleBufferGetFormatDescription(sampleBuffer) var spsSize: Int = 0 var spsCount: Int = 0 var nalHeaderLength: Int32 = 0 var sps: UnsafePointer<UInt8>? if CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format!, 0, &sps, &spsSize, &spsCount, &nalHeaderLength) == noErr { print("sps: \(String(describing: sps)), spsSize: \(spsSize), spsCount: \(spsCount), NAL header length: \(nalHeaderLength)") // pps var ppsSize: Int = 0 var ppsCount: Int = 0 var pps: UnsafePointer<UInt8>? if CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format!, 1, &pps, &ppsSize, &ppsCount, &nalHeaderLength) == noErr { print("sps: \(String(describing: pps)), spsSize: \(ppsSize), spsCount: \(ppsCount), NAL header length: \(nalHeaderLength)") let spsData: NSData = NSData(bytes: sps, length: spsSize) let ppsData: NSData = NSData(bytes: pps, length: ppsSize) // save sps/pps to file // NOTE: 事实上,大多数情况下 sps/pps 不变/变化不大 或者 变化对视频数据产生的影响很小, // 因此,多数情况下你都可以只在文件头写入或视频流开头传输 sps/pps 数据 vc.handle(sps: spsData, pps: ppsData) } } } // end of handle sps/pps // handle frame data guard let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return } var lengthAtOffset: Int = 0 var totalLength: Int = 0 var dataPointer: UnsafeMutablePointer<Int8>? if CMBlockBufferGetDataPointer(dataBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer) == noErr { var bufferOffset: Int = 0 let AVCCHeaderLength = 4 while bufferOffset < (totalLength - AVCCHeaderLength) { var NALUnitLength: UInt32 = 0 // first four character is NALUnit length memcpy(&NALUnitLength, dataPointer?.advanced(by: bufferOffset), AVCCHeaderLength) // big endian to host endian. in iOS it's little endian NALUnitLength = CFSwapInt32BigToHost(NALUnitLength) let data: NSData = NSData(bytes: dataPointer?.advanced(by: bufferOffset + AVCCHeaderLength), length: Int(NALUnitLength)) vc.encode(data: data, isKeyFrame: keyFrame) // move forward to the next NAL Unit bufferOffset += Int(AVCCHeaderLength) bufferOffset += Int(NALUnitLength) } } } } class ViewController: UIViewController { let captureSession = AVCaptureSession() let captureQueue = DispatchQueue(label: "videotoolbox.compression.capture") let compressionQueue = DispatchQueue(label: "videotoolbox.compression.compression") lazy var preview: AVCaptureVideoPreviewLayer = { let preview = AVCaptureVideoPreviewLayer(session: self.captureSession) preview.videoGravity = .resizeAspectFill view.layer.addSublayer(preview) return preview }() var compressionSession: VTCompressionSession? var fileHandler: FileHandle? var isCapturing: Bool = false override func viewDidLoad() { super.viewDidLoad() let path = NSTemporaryDirectory() + "/temp.h264" try? FileManager.default.removeItem(atPath: path) if FileManager.default.createFile(atPath: path, contents: nil, attributes: nil) { fileHandler = FileHandle(forWritingAtPath: path) } let device = AVCaptureDevice.default(for: .video)! let input = try! AVCaptureDeviceInput(device: device) if captureSession.canAddInput(input) { captureSession.addInput(input) } captureSession.sessionPreset = .high let output = AVCaptureVideoDataOutput() // YUV 420v output.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] output.setSampleBufferDelegate(self, queue: captureQueue) if captureSession.canAddOutput(output) { captureSession.addOutput(output) } // not a good method if let connection = output.connection(with: .video) { if connection.isVideoOrientationSupported { connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIApplication.shared.statusBarOrientation.rawValue)! } } captureSession.startRunning() } override func viewDidLayoutSubviews() { preview.frame = view.bounds let button = UIButton(type: .roundedRect) button.setTitle("Click Me", for: .normal) button.backgroundColor = .red button.addTarget(self, action: #selector(startOrNot), for: .touchUpInside) button.frame = CGRect(x: 100, y: 200, width: 100, height: 40) view.addSubview(button) } } extension ViewController { @objc func startOrNot() { if isCapturing { stopCapture() } else { startCapture() } } func startCapture() { isCapturing = true } func stopCapture() { isCapturing = false guard let compressionSession = compressionSession else { return } VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid) VTCompressionSessionInvalidate(compressionSession) self.compressionSession = nil } } extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelbuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } // if CVPixelBufferIsPlanar(pixelbuffer) { // print("planar: \(CVPixelBufferGetPixelFormatType(pixelbuffer))") // } // // var desc: CMFormatDescription? // CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelbuffer, &desc) // let extensions = CMFormatDescriptionGetExtensions(desc!) // print("extensions: \(extensions!)") if compressionSession == nil { let width = CVPixelBufferGetWidth(pixelbuffer) let height = CVPixelBufferGetHeight(pixelbuffer) print("width: \(width), height: \(height)") VTCompressionSessionCreate(kCFAllocatorDefault, Int32(width), Int32(height), kCMVideoCodecType_H264, nil, nil, nil, compressionOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), &compressionSession) guard let c = compressionSession else { return } // set profile to Main VTSessionSetProperty(c, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel) // capture from camera, so it's real time VTSessionSetProperty(c, kVTCompressionPropertyKey_RealTime, true as CFTypeRef) // 关键帧间隔 VTSessionSetProperty(c, kVTCompressionPropertyKey_MaxKeyFrameInterval, 10 as CFTypeRef) // 比特率和速率 VTSessionSetProperty(c, kVTCompressionPropertyKey_AverageBitRate, width * height * 2 * 32 as CFTypeRef) VTSessionSetProperty(c, kVTCompressionPropertyKey_DataRateLimits, [width * height * 2 * 4, 1] as CFArray) VTCompressionSessionPrepareToEncodeFrames(c) } guard let c = compressionSession else { return } guard isCapturing else { return } compressionQueue.sync { pixelbuffer.lock(.readwrite) { let presentationTimestamp = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer) let duration = CMSampleBufferGetOutputDuration(sampleBuffer) VTCompressionSessionEncodeFrame(c, pixelbuffer, presentationTimestamp, duration, nil, nil, nil) } } } func handle(sps: NSData, pps: NSData) { guard let fh = fileHandler else { return } let headerData: NSData = NSData(bytes: NALUHeader, length: NALUHeader.count) fh.write(headerData as Data) fh.write(sps as Data) fh.write(headerData as Data) fh.write(pps as Data) } func encode(data: NSData, isKeyFrame: Bool) { guard let fh = fileHandler else { return } let headerData: NSData = NSData(bytes: NALUHeader, length: NALUHeader.count) fh.write(headerData as Data) fh.write(data as Data) } }
CVPixelBuffer+Extension.swift
// // CVPixelBuffer+Extension.swift // VideoToolboxCompression // // Created by tomisacat on 14/08/2017. // Copyright © 2017 tomisacat. All rights reserved. // import Foundation import VideoToolbox import CoreVideo extension CVPixelBuffer { public enum LockFlag { case readwrite case readonly func flag() -> CVPixelBufferLockFlags { switch self { case .readonly: return .readOnly default: return CVPixelBufferLockFlags.init(rawValue: 0) } } } public func lock(_ flag: LockFlag, closure: (() -> Void)?) { if CVPixelBufferLockBaseAddress(self, flag.flag()) == kCVReturnSuccess { if let c = closure { c() } } CVPixelBufferUnlockBaseAddress(self, flag.flag()) } }