編碼
import UIKit
import VideoToolbox
class ZGH264Encoder: NSObject {
var videoEncodeCallback : ((Data)-> Void)?
var videoEncodeCallbackSPSAndPPS :((Data,Data)->Void)?
private var width: Int32 = 480
private var height:Int32 = 640
private var bitRate : Int32 = 480 * 640 * 3 * 4
private var fps : Int32 = 10
private var hasSpsPps = false
private var frameID:Int64 = 0
private var encodeCallBack:VTCompressionOutputCallback?
private var encodeQueue = DispatchQueue(label: "encode")
private var callBackQueue = DispatchQueue(label: "callBack")
private var encodeSession:VTCompressionSession!
init(width:Int32 = 480,height:Int32 = 640,bitRate : Int32? = nil,fps: Int32? = nil ) {
self.width = width
self.height = height
self.bitRate = bitRate != nil ? bitRate! : 480 * 640 * 3 * 4
self.fps = (fps != nil) ? fps! : 10
super.init()
setCallBack()
initVideoToolBox()
}
//開始編碼
func encodeVideo(sampleBuffer:CMSampleBuffer){
if self.encodeSession == nil {
initVideoToolBox()
}
encodeQueue.async {
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
var time = CMTime(value: self.frameID, timescale: 1000)
if #available(iOS 15, *){
time = CMTime(value: self.frameID, timescale: 100)
self.frameID += 1
}
let state = VTCompressionSessionEncodeFrame(self.encodeSession, imageBuffer: imageBuffer!, presentationTimeStamp: time, duration: .invalid, frameProperties: nil, sourceFrameRefcon: nil, infoFlagsOut: nil)
if state != 0{
print("encode filure")
}
}
}
func stop(){
hasSpsPps = false
frameID = 0
if ((encodeSession) != nil) {
VTCompressionSessionCompleteFrames(encodeSession, untilPresentationTimeStamp: .invalid)
VTCompressionSessionInvalidate(encodeSession);
encodeSession = nil;
}
}
//初始化編碼器
private func initVideoToolBox() {
//創(chuàng)建VTCompressionSession
let state = VTCompressionSessionCreate(allocator: kCFAllocatorDefault, width: width, height: height, codecType: kCMVideoCodecType_H264, encoderSpecification: nil, imageBufferAttributes: nil, compressedDataAllocator: nil, outputCallback:encodeCallBack , refcon: unsafeBitCast(self, to: UnsafeMutableRawPointer.self), compressionSessionOut: &self.encodeSession)
if state != 0{
print("creat VTCompressionSession failed")
return
}
//設置實時編碼輸出
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
//設置編碼方式
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Baseline_AutoLevel)
//設置是否產(chǎn)生B幀(因為B幀在解碼時并不是必要的,是可以拋棄B幀的)
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse)
//設置關鍵幀間隔
var frameInterval = 10
let number = CFNumberCreate(kCFAllocatorDefault, CFNumberType.intType, &frameInterval)
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: number)
//設置期望幀率做个,不是實際幀率
let fpscf = CFNumberCreate(kCFAllocatorDefault, CFNumberType.intType, &fps)
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: fpscf)
//設置碼率平均值,單位是bps融求。碼率大了話就會非常清晰囤躁,但同時文件也會比較大泽艘。碼率小的話缘厢,圖像有時會模糊保礼,但也勉強能看
//碼率計算公式參考筆記
// var bitrate = width * height * 3 * 4
let bitrateAverage = CFNumberCreate(kCFAllocatorDefault, CFNumberType.intType, &bitRate)
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_AverageBitRate, value: bitrateAverage)
//碼率限制
let bitRatesLimit :CFArray = [bitRate * 5 / 8, 1] as CFArray
VTSessionSetProperty(encodeSession, key: kVTCompressionPropertyKey_DataRateLimits, value: bitRatesLimit)
}
private func setCallBack() {
//編碼完成回調(diào)
encodeCallBack = {(outputCallbackRefCon, sourceFrameRefCon, status, flag, sampleBuffer) in
let encoder :ZGH264Encoder = unsafeBitCast(outputCallbackRefCon, to: ZGH264Encoder.self)
guard sampleBuffer != nil else {
return
}
/// 0. 原始字節(jié)數(shù)據(jù) 8字節(jié)
let buffer : [UInt8] = [0x00,0x00,0x00,0x01]
/// 1. [UInt8] -> UnsafeBufferPointer<UInt8>
let unsafeBufferPointer = buffer.withUnsafeBufferPointer {$0}
/// 2.. UnsafeBufferPointer<UInt8> -> UnsafePointer<UInt8>
let unsafePointer = unsafeBufferPointer.baseAddress
guard let startCode = unsafePointer else {return}
let attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, createIfNecessary: false)
let strkey = unsafeBitCast(kCMSampleAttachmentKey_NotSync, to: UnsafeRawPointer.self)
let cfDic = unsafeBitCast(CFArrayGetValueAtIndex(attachArray, 0), to: CFDictionary.self)
let keyFrame = !CFDictionaryContainsKey(cfDic, strkey);//沒有這個鍵就意味著同步,就是關鍵幀
// 獲取sps pps
if keyFrame && !encoder.hasSpsPps{
if let description = CMSampleBufferGetFormatDescription(sampleBuffer!){
var spsSize: Int = 0, spsCount :Int = 0,spsHeaderLength:Int32 = 0
var ppsSize: Int = 0, ppsCount: Int = 0,ppsHeaderLength:Int32 = 0
var spsDataPointer : UnsafePointer<UInt8>? = UnsafePointer(UnsafeMutablePointer<UInt8>.allocate(capacity: 0))
var ppsDataPointer : UnsafePointer<UInt8>? = UnsafePointer<UInt8>(bitPattern: 0)
let spsstatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, parameterSetIndex: 0, parameterSetPointerOut: &spsDataPointer, parameterSetSizeOut: &spsSize, parameterSetCountOut: &spsCount, nalUnitHeaderLengthOut: &spsHeaderLength)
if spsstatus != 0{
print("sps失敗")
}
let ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, parameterSetIndex: 1, parameterSetPointerOut: &ppsDataPointer, parameterSetSizeOut: &ppsSize, parameterSetCountOut: &ppsCount, nalUnitHeaderLengthOut: &ppsHeaderLength)
if ppsStatus != 0 {
print("pps失敗")
}
if let spsData = spsDataPointer,let ppsData = ppsDataPointer{
var spsDataValue = Data(capacity: 4 + spsSize)
spsDataValue.append(buffer, count: 4)
spsDataValue.append(spsData, count: spsSize)
var ppsDataValue = Data(capacity: 4 + ppsSize)
ppsDataValue.append(startCode, count: 4)
ppsDataValue.append(ppsData, count: ppsSize)
encoder.callBackQueue.async {
encoder.videoEncodeCallbackSPSAndPPS!(spsDataValue, ppsDataValue)
}
}
}
encoder.hasSpsPps = true
}
let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer!)
var dataPointer: UnsafeMutablePointer<Int8>? = nil
var totalLength :Int = 0
let blockState = CMBlockBufferGetDataPointer(dataBuffer!, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: &totalLength, dataPointerOut: &dataPointer)
if blockState != 0{
print("獲取data失敗\(blockState)")
}
//NALU
var offset :UInt32 = 0
//返回的nalu數(shù)據(jù)前四個字節(jié)不是0001的startcode(不是系統(tǒng)端的0001)沛励,而是大端模式的幀長度length
let lengthInfoSize = 4
//循環(huán)寫入nalu數(shù)據(jù)
while offset < totalLength - lengthInfoSize {
//獲取nalu 數(shù)據(jù)長度
var naluDataLength:UInt32 = 0
memcpy(&naluDataLength, dataPointer! + UnsafeMutablePointer<Int8>.Stride(offset), lengthInfoSize)
//大端轉系統(tǒng)端
naluDataLength = CFSwapInt32BigToHost(naluDataLength)
//獲取到編碼好的視頻數(shù)據(jù)
var data = Data(capacity: Int(naluDataLength) + lengthInfoSize)
data.append(buffer, count: 4)
//轉化pointer责语;UnsafeMutablePointer<Int8> -> UnsafePointer<UInt8>
let naluUnsafePoint = unsafeBitCast(dataPointer, to: UnsafePointer<UInt8>.self)
data.append(naluUnsafePoint + UnsafePointer<UInt8>.Stride(offset + UInt32(lengthInfoSize)) , count: Int(naluDataLength))
encoder.callBackQueue.async {
encoder.videoEncodeCallback!(data)
}
offset += (naluDataLength + UInt32(lengthInfoSize))
}
}
}
deinit {
stop()
}
}
解碼類型
如果需要使用
AVSampleBufferDisplayLayer
播放CMSampleBuffer
可以選擇輸出為sampleBuffer,其中sampleBuffer并未進行解碼目派,AVSampleBufferDisplayLayer支持h264的CMSampleBuffer播放
enum ZGH264DecodeType {
case imageBuffer //CVPixcelBuffer
case sampleBuffer //CMSampleBuffer(H264)
}
解碼
解碼需要使用編碼的SPS坤候、PPS
import UIKit
import VideoToolbox
class ZGH264Decoder: NSObject {
var videoDecodeCallback:((CVImageBuffer?) -> Void)?
var videoDecodeSampleBufferCallback:((CMSampleBuffer?) -> Void)?
private var width: Int32 = 480
private var height:Int32 = 640
private var spsData:Data?
private var ppsData:Data?
private var decompressionSession : VTDecompressionSession?
private var decodeDesc : CMVideoFormatDescription?
private var callback :VTDecompressionOutputCallback?
private var decodeQueue = DispatchQueue(label: "decode")
private var callBackQueue = DispatchQueue(label: "decodeCallBack")
private var returnType: ZGH264DecodeType = .sampleBuffer
init(width:Int32,height:Int32) {
self.width = width
self.height = height
}
func decode(data:Data) {
decodeQueue.async {
let length:UInt32 = UInt32(data.count)
self.decodeByte(data: data, size: length)
}
}
/// 設置解碼返回類型
/// - Parameter type:ZGH264DecodeType
func setReturnType(type: ZGH264DecodeType){
returnType = type
}
private func decodeByte(data:Data,size:UInt32) {
//數(shù)據(jù)類型:frame的前4個字節(jié)是NALU數(shù)據(jù)的開始碼,也就是00 00 00 01企蹭,
// 將NALU的開始碼轉為4字節(jié)大端NALU的長度信息
let naluSize = size - 4
let length : [UInt8] = [
UInt8(truncatingIfNeeded: naluSize >> 24),
UInt8(truncatingIfNeeded: naluSize >> 16),
UInt8(truncatingIfNeeded: naluSize >> 8),
UInt8(truncatingIfNeeded: naluSize)
]
var frameByte :[UInt8] = length
[UInt8](data).suffix(from: 4).forEach { (bb) in
frameByte.append(bb)
}
let bytes = frameByte //[UInt8](frameData)
// 第5個字節(jié)是表示數(shù)據(jù)類型白筹,轉為10進制后,7是sps, 8是pps, 5是IDR(I幀)信息
let type :Int = Int(bytes[4] & 0x1f)
switch type{
case 0x05:
if initDecoder() {
decode(frame: bytes, size: size)
}
case 0x06:
// print("增強信息")
break
case 0x07:
spsData = data
case 0x08:
ppsData = data
default:
if initDecoder() {
decode(frame: bytes, size: size)
}
}
}
private func decode(frame:[UInt8],size:UInt32) {
//
var blockBUffer :CMBlockBuffer?
var frame1 = frame
//創(chuàng)建blockBuffer
/*!
參數(shù)1: structureAllocator kCFAllocatorDefault
參數(shù)2: memoryBlock frame
參數(shù)3: frame size
參數(shù)4: blockAllocator: Pass NULL
參數(shù)5: customBlockSource Pass NULL
參數(shù)6: offsetToData 數(shù)據(jù)偏移
參數(shù)7: dataLength 數(shù)據(jù)長度
參數(shù)8: flags 功能和控制標志
參數(shù)9: newBBufOut blockBuffer地址,不能為空
*/
let blockState = CMBlockBufferCreateWithMemoryBlock(allocator: kCFAllocatorDefault,
memoryBlock: &frame1,
blockLength: Int(size),
blockAllocator: kCFAllocatorNull,
customBlockSource: nil,
offsetToData:0,
dataLength: Int(size),
flags: 0,
blockBufferOut: &blockBUffer)
if blockState != 0 {
print("創(chuàng)建blockBuffer失敗")
}
//
var sampleSizeArray :[Int] = [Int(size)]
var sampleBuffer :CMSampleBuffer?
//創(chuàng)建sampleBuffer
/*
參數(shù)1: allocator 分配器,使用默認內(nèi)存分配, kCFAllocatorDefault
參數(shù)2: blockBuffer.需要編碼的數(shù)據(jù)blockBuffer.不能為NULL
參數(shù)3: formatDescription,視頻輸出格式
參數(shù)4: numSamples.CMSampleBuffer 個數(shù).
參數(shù)5: numSampleTimingEntries 必須為0,1,numSamples
參數(shù)6: sampleTimingArray. 數(shù)組.為空
參數(shù)7: numSampleSizeEntries 默認為1
參數(shù)8: sampleSizeArray
參數(shù)9: sampleBuffer對象
*/
let readyState = CMSampleBufferCreateReady(allocator: kCFAllocatorDefault,
dataBuffer: blockBUffer,
formatDescription: decodeDesc,
sampleCount: CMItemCount(1),
sampleTimingEntryCount: CMItemCount(),
sampleTimingArray: nil,
sampleSizeEntryCount: CMItemCount(1),
sampleSizeArray: &sampleSizeArray,
sampleBufferOut: &sampleBuffer)
guard let buffer = sampleBuffer, readyState == kCMBlockBufferNoErr else {
print("解碼失敗")
return
}
if returnType == .sampleBuffer {
if let attachmentArray = CMSampleBufferGetSampleAttachmentsArray(buffer, createIfNecessary: true) {
let dic = unsafeBitCast(CFArrayGetValueAtIndex(attachmentArray, 0), to: CFMutableDictionary.self)
CFDictionarySetValue(dic,
Unmanaged.passUnretained(kCMSampleAttachmentKey_DisplayImmediately).toOpaque(),
Unmanaged.passUnretained(kCFBooleanTrue).toOpaque())
}
videoDecodeSampleBufferCallback?(sampleBuffer)
return
}
//解碼數(shù)據(jù)為CVPixcelBuffer
/*
參數(shù)1: 解碼session
參數(shù)2: 源數(shù)據(jù) 包含一個或多個視頻幀的CMsampleBuffer
參數(shù)3: 解碼標志
參數(shù)4: 解碼后數(shù)據(jù)outputPixelBuffer
參數(shù)5: 同步/異步解碼標識
*/
let sourceFrame:UnsafeMutableRawPointer? = nil
var inforFalg = VTDecodeInfoFlags.asynchronous
let decodeState = VTDecompressionSessionDecodeFrame(self.decompressionSession!, sampleBuffer: sampleBuffer!, flags:VTDecodeFrameFlags._EnableAsynchronousDecompression , frameRefcon: sourceFrame, infoFlagsOut: &inforFalg)
if decodeState != 0 {
print("解碼失敗")
}
}
private func initDecoder() -> Bool {
if decompressionSession != nil {
return true
}
guard spsData != nil,ppsData != nil else {
return false
}
//處理sps/pps
var sps : [UInt8] = []
[UInt8](spsData!).suffix(from: 4).forEach { (value) in
sps.append(value)
}
var pps : [UInt8] = []
[UInt8](ppsData!).suffix(from: 4).forEach{(value) in
pps.append(value)
}
let spsAndpps = [sps.withUnsafeBufferPointer{$0}.baseAddress!,pps.withUnsafeBufferPointer{$0}.baseAddress!]
let sizes = [sps.count,pps.count]
/**
根據(jù)sps pps設置解碼參數(shù)
param kCFAllocatorDefault 分配器
param 2 參數(shù)個數(shù)
param parameterSetPointers 參數(shù)集指針
param parameterSetSizes 參數(shù)集大小
param naluHeaderLen nalu nalu start code 的長度 4
param _decodeDesc 解碼器描述
return 狀態(tài)
*/
let descriptionState = CMVideoFormatDescriptionCreateFromH264ParameterSets(allocator: kCFAllocatorDefault, parameterSetCount: 2, parameterSetPointers: spsAndpps, parameterSetSizes: sizes, nalUnitHeaderLength: 4, formatDescriptionOut: &decodeDesc)
if descriptionState != 0 {
print("description創(chuàng)建失敗" )
return false
}
//解碼回調(diào)設置
/*
VTDecompressionOutputCallbackRecord 是一個簡單的結構體谅摄,它帶有一個指針 (decompressionOutputCallback)徒河,指向幀解壓完成后的回調(diào)方法。你需要提供可以找到這個回調(diào)方法的實例 (decompressionOutputRefCon)
*/
setCallBack()
var callbackRecord = VTDecompressionOutputCallbackRecord(decompressionOutputCallback: callback, decompressionOutputRefCon: unsafeBitCast(self, to: UnsafeMutableRawPointer.self))
/*
解碼參數(shù):
* kCVPixelBufferPixelFormatTypeKey:攝像頭的輸出數(shù)據(jù)格式
kCVPixelBufferPixelFormatTypeKey送漠,已測可用值為
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange顽照,即420v
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,即420f
kCVPixelFormatType_32BGRA闽寡,iOS在內(nèi)部進行YUV至BGRA格式轉換
YUV420一般用于標清視頻代兵,YUV422用于高清視頻,這里的限制讓人感到意外下隧。但是奢人,在相同條件下,YUV420計算耗時和傳輸壓力比YUV422都小淆院。
* kCVPixelBufferWidthKey/kCVPixelBufferHeightKey: 視頻源的分辨率 width*height
* kCVPixelBufferOpenGLCompatibilityKey : 它允許在 OpenGL 的上下文中直接繪制解碼后的圖像何乎,而不是從總線和 CPU 之間復制數(shù)據(jù)。這有時候被稱為零拷貝通道土辩,因為在繪制過程中沒有解碼的圖像被拷貝.
*/
let imageBufferAttributes = [
kCVPixelBufferPixelFormatTypeKey:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelBufferWidthKey:width,
kCVPixelBufferHeightKey:height,
// kCVPixelBufferOpenGLCompatibilityKey:true
] as [CFString : Any]
//創(chuàng)建session
/*!
@function VTDecompressionSessionCreate
@abstract 創(chuàng)建用于解壓縮視頻幀的會話支救。
@discussion 解壓后的幀將通過調(diào)用OutputCallback發(fā)出
@param allocator 內(nèi)存的會話。通過使用默認的kCFAllocatorDefault的分配器拷淘。
@param videoFormatDescription 描述源視頻幀
@param videoDecoderSpecification 指定必須使用的特定視頻解碼器.NULL
@param destinationImageBufferAttributes 描述源像素緩沖區(qū)的要求 NULL
@param outputCallback 使用已解壓縮的幀調(diào)用的回調(diào)
@param decompressionSessionOut 指向一個變量以接收新的解壓會話
*/
let state = VTDecompressionSessionCreate(allocator: kCFAllocatorDefault, formatDescription: decodeDesc!, decoderSpecification: nil, imageBufferAttributes: imageBufferAttributes as CFDictionary, outputCallback: &callbackRecord, decompressionSessionOut: &decompressionSession)
if state != 0 {
print("創(chuàng)建decodeSession失敗")
}
VTSessionSetProperty(self.decompressionSession!, key: kVTDecompressionPropertyKey_RealTime, value: kCFBooleanTrue)
return true
}
//解碼成功的回掉
private func setCallBack() {
/*
VTDecompressionOutputCallback 回調(diào)方法包括七個參數(shù):
參數(shù)1: 回調(diào)的引用
參數(shù)2: 幀的引用
參數(shù)3: 一個狀態(tài)標識 (包含未定義的代碼)
參數(shù)4: 指示同步/異步解碼各墨,或者解碼器是否打算丟幀的標識
參數(shù)5: 實際圖像的緩沖
參數(shù)6: 出現(xiàn)的時間戳
參數(shù)7: 出現(xiàn)的持續(xù)時間
*/
//(UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, OSStatus, VTDecodeInfoFlags, CVImageBuffer?, CMTime, CMTime) -> Void
callback = { decompressionOutputRefCon,sourceFrameRefCon,status,inforFlags,imageBuffer,presentationTimeStamp,presentationDuration in
let decoder :ZGH264Decoder = unsafeBitCast(decompressionOutputRefCon, to: ZGH264Decoder.self)
guard imageBuffer != nil else {
return
}
if let block = decoder.videoDecodeCallback {
decoder.callBackQueue.async {
block(imageBuffer)
}
}
}
}
deinit {
if decompressionSession != nil {
VTDecompressionSessionInvalidate(decompressionSession!)
decompressionSession = nil
}
}
}
使用示例
let h264Encoder = ZGH264Encoder(width: 360, height: 640, bitRate: 2*1024*1024)
let h264Decoder = ZGH264Decoder(width: 360, height: 640)
h264Encoder.videoEncodeCallback = {[weak self] (data) in
self?.h264Decoder.decode(data: data)
}
h264Encoder.videoEncodeCallbackSPSAndPPS = {[weak self] (sps, pps) in
self?.h264Decoder.decode(data: sps)
self?.h264Decoder.decode(data: pps)
}
h264Decoder.videoDecodeSampleBufferCallback = { [weak self] sampleBuffer in
guard let sampleBuffer = sampleBuffer else { return }
self?.otherPreView.enqueue(sampleBuffer: sampleBuffer)
}
// h264Decoder.setReturnType(type: .imageBuffer)
// h264Decoder.videoDecodeCallback = { pixelBuffer in
//
// }
相關資料
VideoEncodeH264