關于iOS端視頻采集和硬編碼的資料很少,官方也沒有看到詳細的相關介紹诗宣,github上有不少demo膘怕,注釋寫的很少,對于整個iOS 視頻采集到硬編碼出h.264都沒有做很好的說明梧田。查閱了很多資料淳蔼,現(xiàn)在我把這個過程知道的細節(jié)都記錄下來侧蘸,供同樣做直播的朋友參考:Demo地址
注:
- 文章代碼都是swift 2.2 編寫裁眯,OC視頻采集的demo不少,可自行github 上搜索讳癌,swift在視頻壓縮部分的demo 比較少穿稳。
- 剛接觸iOS開發(fā)和swift, 代碼挺爛晌坤,有錯誤不詳?shù)牡胤较Mu論指正逢艘。
相機數(shù)據(jù)采集
攝像頭的數(shù)據(jù)采集通過 AVFoundation.framework
的 AVCaptureSession
類完成旦袋,它負責調配影音輸入與輸出之間的數(shù)據(jù)流:
AVCaptureSession
的工作流程:實例化一個AVCaptureSession
,添加配置輸入和輸出(輸入其實就是一個或多個的 AVCaptureDevice
對象它改,這些對象通過AVCaptureDeviceInput
連接上 CaptureSession
疤孕,輸出是 AVCaptureVideoDataOutput
,可以通過它拿到采集到的視頻數(shù)據(jù))央拖,啟動AVCaptureSession
祭阀。
- 設置Capture Device
// 定義相機設備
var cameraDevice: AVCaptureDevice?
let devices = AVCaptureDevice.devices()
// 遍歷相機設備,找到后置攝像頭
for device in devices {
if (device.hasMediaType(AVMediaTypeVideo)) {
if (device.position == AVCaptureDevicePosition.Back) {
cameraDevice = device as? AVCaptureDevice
if cameraDevice != nil {
print("Capture Device found.")
}
}
}
}
前后置相機可以通過 AVCaptureDevicePosition
這個枚舉選擇鲜戒。
- 設置和添加輸入輸出
do {
// 為output添加 代理专控,在代理中就可以拿到采集到的原始視頻數(shù)據(jù)
output.setSampleBufferDelegate(self, queue: lockQueue)
// 將cameraDevice 與 Input 關聯(lián),添加到會話中
input = try AVCaptureDeviceInput(device: cameraDevice)
if session.canAddInput(input) {
session.addInput(input)
}
// 添加輸出
if session.canAddOutput(output) {
session.addOutput(output)
}
// 配置 session
session.beginConfiguration()
// 指定視頻輸出質量等級
if session.canSetSessionPreset(AVCaptureSessionPreset1280x720) {
session.sessionPreset = AVCaptureSessionPreset1280x720
}
// 設置攝像頭的方向
let connection:AVCaptureConnection = output.connectionWithMediaType(AVMediaTypeVideo)
connection.videoOrientation = .Portrait
session.commitConfiguration()
} catch let error as NSError {
print(error)
}
// 開始采集
session.startRunning()
視頻輸出等級:
指定了視頻的輸出質量遏餐,設置前最好判斷設配社否支持所設的輸出等級伦腐,一些老的iPhone可能不支持較高質量的輸出。session
的sessionPreset
枚舉的值很多失都,可以跟入源碼看一下柏蘑,這里設定1280x720的輸出。
相機方向:
相機的方向是這樣嗅剖,當屏幕設置為可旋轉的情況下辩越,若不設置相機方向,屏幕變?yōu)闄M屏信粮,預覽圖像還是按豎屏顯示黔攒。
輸出代理:
output
需要設置遵循AVCaptureVideoDataOutputSampleBufferDelegate
的代理來拿到采集到的視頻數(shù)據(jù)。CMSampleBuffer
存放編解碼前后的視頻圖像的容器數(shù)據(jù)結構强缘,這里存放的就是未經(jīng)編碼的攝像機數(shù)據(jù)督惰。
通過CMSampleBufferGetImageBuffer()
接口就可以拿到CVImageBuffer
編碼前的數(shù)據(jù),可以送去編碼器進行編碼的數(shù)據(jù)旅掂。
extension VideoIOComponent: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(captureOutput:AVCaptureOutput!, didOutputSampleBuffer sampleBuffer:CMSampleBuffer!, fromConnection connection:AVCaptureConnection!) {
guard let image:CVImageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
// 送去IOS的硬編碼器編碼
avcEncoder.encodeImageBuffer(image, presentationTimeStamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer), presentationDuration: CMSampleBufferGetDuration(sampleBuffer))
// print("get camera image data! Yeh!")
}
}
相機圖像預覽:
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspect
previewLayer?.frame = viewController.view.bounds
viewController.view.layer.addSublayer(previewLayer!)
AVCaptureVideoPreviewLayer
可以在視頻采集的同時預覽攝像頭圖像赏胚。
參考資料:
視頻數(shù)據(jù)硬編碼
視頻壓縮編碼通過Video Toolbox
框架下的VTCompressionSession
完成。Video ToolBox 是一個基于 CoreMedia商虐,CoreVideo觉阅,CoreFoundation 框架的 C 語言 API,來處理硬件的編碼和解碼秘车,在iOS 8.0后典勇,蘋果將該框架引入iOS系統(tǒng),蘋果在iOS 8.0系統(tǒng)之前叮趴,沒有開放系統(tǒng)的硬件編碼解碼功能割笙。
由于Video ToolBox
提供的是 C 的API,所以編碼時會用到一些swift的指針操作眯亦。
- ** 定義創(chuàng)建配置CompressionSession:**
private var session:VTCompressionSessionRef?
// 將
VTCompressionSessionCreate(
kCFAllocatorDefault,
480, // encode height
640,// encode width
kCMVideoCodecType_H264, //encode type伤溉,h.264
nil,
attributes,
nil,
callback,
unsafeBitCast(self, UnsafeMutablePointer<Void>.self),
&session)
// 設置編碼的屬性
VTSessionSetProperties(session!, properties)
VTCompressionSessionPrepareToEncodeFrames(session!)
VTCompressionSessionCreate
的encode height
, encode width
設置編碼輸出的h.264文件的寬高般码,單位px(像素)。
編碼類型主要用的就是kCMVideoCodecType_H264
(h.264),其他的類型還有h.263等乱顾,好像最新的 iphone 6s 已經(jīng)支持 h.265的編碼板祝,但還沒有放出接口。
- ** attributes 和 properties**
這個地方先給出一個參考的設置走净,具體的參數(shù)意義我只是了解個大概扔字。
為了高效率的輸出,硬件解碼器輸出偏向于選擇本機的色度格式温技,也就是 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
革为。然而,有許多其他的視頻格式可以用舵鳞,并且轉碼過程中有 GPU 參與震檩,效率非常高◎讯椋可以通過設置kCVPixelBufferPixelFormatTypeKey
鍵來啟用抛虏,我們還需要通過 kCVPixelBufferWidthKey
和kCVPixelBufferHeightKey
來設置整數(shù)的輸出尺寸。有一個可選的鍵也值得一提套才,kCVPixelBufferOpenGLCompatibilityKey
迂猴,它允許在 OpenGL 的上下文中直接繪制解碼后的圖像,而不是從總線和 CPU 之間復制數(shù)據(jù)背伴。這有時候被稱為零拷貝通道沸毁,因為在繪制過程中沒有解碼的圖像被拷貝
參考文章 視頻工具箱和硬件加速 視頻輸出格式一節(jié)
let defaultAttributes:[NSString: AnyObject] = [
kCVPixelBufferPixelFormatTypeKey: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange), // 指定像素的輸出格式
kCVPixelBufferIOSurfacePropertiesKey: [:],
kCVPixelBufferOpenGLESCompatibilityKey: true, // about openGL
]
private var attributes:[NSString: AnyObject] {
var attributes:[NSString: AnyObject] = defaultAttributes
attributes[kCVPixelBufferHeightKey] = 480 //output height
attributes[kCVPixelBufferWidthKey] = 640 // output width
return attributes
}
我測試中,這個地方的寬高好像不起什么作用傻寂,輸出的寬高和VTCompressionSessionCreate
傳入的寬高相同息尺。
properties詳細指定了編碼的具體參數(shù),這個地方牽扯一些關于編碼比較專業(yè)的東西疾掰,只把我明白的添加了注釋搂誉,具體的查閱資料后,慢慢完善静檬。
var profileLevel:String = kVTProfileLevel_H264_Baseline_3_1 as String
private var properties:[NSString: NSObject] {
let isBaseline:Bool = profileLevel.containsString("Baseline")
var properties:[NSString: NSObject] = [
kVTCompressionPropertyKey_RealTime: kCFBooleanTrue,
// h264 profile level
kVTCompressionPropertyKey_ProfileLevel: profileLevel,
// 視頻碼率
kVTCompressionPropertyKey_AverageBitRate: Int(640*480),
// 視頻幀率
kVTCompressionPropertyKey_ExpectedFrameRate: NSNumber(double: 30.0),
// 關鍵幀間隔炭懊,單位秒
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration: NSNumber(double: 2.0),
kVTCompressionPropertyKey_AllowFrameReordering: !isBaseline,
kVTCompressionPropertyKey_PixelTransferProperties: [
"ScalingMode": "Trim"
]
]
if (!isBaseline) {
properties[kVTCompressionPropertyKey_H264EntropyMode] = kVTH264EntropyMode_CABAC
}
return properties
}
- 編碼輸出:
VTCompressionOutputCallback
對象 callback,是 session 的回調拂檩,通過 callback 拿到編碼后的h.264視頻數(shù)據(jù)侮腹。CMSampleBuffer對象 sampleBuffer 存有編碼后的視頻數(shù)據(jù),可以發(fā)現(xiàn)編碼前后都是使用的 CMSampleBuffer 存儲結構广恢,下圖比較了二者的差異
編碼前和解碼后的視頻數(shù)據(jù)存儲在
CPixelBuffer
結構中凯旋,和上面的CVImageBuffer
相同的數(shù)據(jù)結構呀潭。編碼后和解碼前的視頻數(shù)據(jù)存儲在CMBlockBuffer
的數(shù)據(jù)結構中钉迷。
private var callback:VTCompressionOutputCallback = {(
outputCallbackRefCon:UnsafeMutablePointer<Void>,
sourceFrameRefCon:UnsafeMutablePointer<Void>,
status:OSStatus,
infoFlags:VTEncodeInfoFlags,
sampleBuffer:CMSampleBuffer?
) in
guard let sampleBuffer:CMSampleBuffer = sampleBuffer where status == noErr else {
return
}
// print("get h.264 data!")
let encoder:AVCEncoder = unsafeBitCast(outputCallbackRefCon, AVCEncoder.self)
// 是否是h264的關鍵幀
let isKeyframe = !CFDictionaryContainsKey(unsafeBitCast(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), CFDictionary.self), unsafeBitCast(kCMSampleAttachmentKey_NotSync, UnsafePointer<Void>.self))
if isKeyframe {
// h264的 pps至非、sps
encoder.formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
}
// h264具體視頻幀內(nèi)容
encoder.sampleOutput(video: sampleBuffer)
}
保存h264碼流文件:
H264碼流文件結構如下:
H264的碼流由NALU單元組成,NALU單元包含視頻圖像數(shù)據(jù)和H264的參數(shù)信息糠聪。其中視頻圖像數(shù)據(jù)就是CMBlockBuffer荒椭,而H264的參數(shù)信息則可以組合成FormatDesc。具體來說參數(shù)信息包含SPS(Sequence Parameter Set)和PPS(Picture Parameter Set)
h264 文件在保存的時候可以在每一個I幀前都添加SPS和PPS信息舰蟆,也可以只在整個文件開始時只添加一組SPS和PPS趣惠。每個NALU都是以0x00,0x00,0x00,0x01
開始。
- 保存SPS,PPS
let sampleData = NSMutableData()
// let formatDesrciption :CMFormatDescriptionRef = CMSampleBufferGetFormatDescription(sampleBuffer!)!
let sps = UnsafeMutablePointer<UnsafePointer<UInt8>>.alloc(1)
let pps = UnsafeMutablePointer<UnsafePointer<UInt8>>.alloc(1)
let spsLength = UnsafeMutablePointer<Int>.alloc(1)
let ppsLength = UnsafeMutablePointer<Int>.alloc(1)
let spsCount = UnsafeMutablePointer<Int>.alloc(1)
let ppsCount = UnsafeMutablePointer<Int>.alloc(1)
sps.initialize(nil)
pps.initialize(nil)
spsLength.initialize(0)
ppsLength.initialize(0)
spsCount.initialize(0)
ppsCount.initialize(0)
var err : OSStatus
// 獲取 psp
err = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescription!, 0, sps, spsLength, spsCount, nil )
if (err != noErr) {
NSLog("An Error occured while getting h264 parameter")
}
// 獲取pps
err = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescription!, 1, pps, ppsLength, ppsCount, nil )
if (err != noErr) {
NSLog("An Error occured while getting h264 parameter")
}
// 添加NALU開始碼
let naluStart:[UInt8] = [0x00, 0x00, 0x00, 0x01]
sampleData.appendBytes(naluStart, length: naluStart.count)
sampleData.appendBytes(sps.memory, length: spsLength.memory)
sampleData.appendBytes(naluStart, length: naluStart.count)
sampleData.appendBytes(pps.memory, length: ppsLength.memory)
// 寫入文件
fileHandle.writeData(sampleData)
- ** 保存視頻數(shù)據(jù)**
print("get slice data!")
// todo : write to h264 file
let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer)
var totalLength = Int()
var length = Int()
var dataPointer: UnsafeMutablePointer<Int8> = nil
let state = CMBlockBufferGetDataPointer(blockBuffer!, 0, &length, &totalLength, &dataPointer)
if state == noErr {
var bufferOffset = 0;
let AVCCHeaderLength = 4
// 在輸出較高質量的視頻時身害,比如720p會有幀分片的情況味悄,循環(huán)取出,這段的變量命名可能不太準確,但功能是實現(xiàn)了塌鸯。
while bufferOffset < totalLength - AVCCHeaderLength {
var NALUnitLength:UInt32 = 0
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength)
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength)
var naluStart:[UInt8] = [UInt8](count: 4, repeatedValue: 0x00)
naluStart[3] = 0x01
let buffer:NSMutableData = NSMutableData()
// NALU 起始碼
buffer.appendBytes(&naluStart, length: naluStart.count)
// 視頻幀數(shù)據(jù)
buffer.appendBytes(dataPointer + bufferOffset + AVCCHeaderLength, length: Int(NALUnitLength))
// 視頻數(shù)據(jù)寫入文件
fileHandle.writeData(buffer)
bufferOffset += (AVCCHeaderLength + Int(NALUnitLength))
}
}
這樣從采集攝像頭數(shù)據(jù)侍瑟,到保存h264文件整個流程就走通了,主要為后面通過RTMP協(xié)議推流做準備丙猬。在進行視頻推流時涨颜,需要將H264視頻數(shù)據(jù)進一步按照FLV Tag的格式封包,這些有時間再寫寫茧球。
關于幀分片
幀分片是我在用h264分析工具的時候注意到的庭瑰,有的視頻幀會有重復,開始不懂以為 保存的文件有問題抢埋,但可以用VLC播放弹灭,后來也是問在一個群里問別人才知道的。
可以看到充第#10
起揪垄,每幀的編號變成兩個鲤屡,正常的是1,2福侈,3酒来,4...... 這樣沒有重復下來的。幀分片出現(xiàn)在輸出的視頻質量較高的情況下肪凛,一幀會拆成多個片堰汉,我們只要知道這樣沒有錯就可以了,有興趣可以查看下面參考 h264 ES流文件經(jīng)過計算first_mb_in_slice區(qū)分幀邊界
參考資料: