iOS直播視頻數(shù)據(jù)采集拙徽、硬編碼保存h264文件

Video Live.png

關于iOS端視頻采集和硬編碼的資料很少,官方也沒有看到詳細的相關介紹诗宣,github上有不少demo膘怕,注釋寫的很少,對于整個iOS 視頻采集到硬編碼出h.264都沒有做很好的說明梧田。查閱了很多資料淳蔼,現(xiàn)在我把這個過程知道的細節(jié)都記錄下來侧蘸,供同樣做直播的朋友參考:Demo地址
注:

  • 文章代碼都是swift 2.2 編寫裁眯,OC視頻采集的demo不少,可自行github 上搜索讳癌,swift在視頻壓縮部分的demo 比較少穿稳。
  • 剛接觸iOS開發(fā)和swift, 代碼挺爛晌坤,有錯誤不詳?shù)牡胤较Mu論指正逢艘。

相機數(shù)據(jù)采集

攝像頭的數(shù)據(jù)采集通過 AVFoundation.frameworkAVCaptureSession 類完成旦袋,它負責調配影音輸入與輸出之間的數(shù)據(jù)流:

AVCaptureSession.png

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可能不支持較高質量的輸出。sessionsessionPreset枚舉的值很多失都,可以跟入源碼看一下柏蘑,這里設定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!)

VTCompressionSessionCreateencode 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 存儲結構广恢,下圖比較了二者的差異

CMSampleBuffer.png

編碼前和解碼后的視頻數(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碼流結構.png

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播放弹灭,后來也是問在一個群里問別人才知道的。

H264 幀分片.png

可以看到充第#10起揪垄,每幀的編號變成兩個鲤屡,正常的是1,2福侈,3酒来,4...... 這樣沒有重復下來的。幀分片出現(xiàn)在輸出的視頻質量較高的情況下肪凛,一幀會拆成多個片堰汉,我們只要知道這樣沒有錯就可以了,有興趣可以查看下面參考 h264 ES流文件經(jīng)過計算first_mb_in_slice區(qū)分幀邊界

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伟墙,一起剝皮案震驚了整個濱河市翘鸭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌戳葵,老刑警劉巖就乓,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡生蚁,警方通過查閱死者的電腦和手機噩翠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邦投,“玉大人伤锚,你說我怎么就攤上這事≈疽拢” “怎么了屯援?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長念脯。 經(jīng)常有香客問我狞洋,道長,這世上最難降的妖魔是什么绿店? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任徘铝,我火速辦了婚禮,結果婚禮上惯吕,老公的妹妹穿的比我還像新娘惕它。我一直安慰自己,他們只是感情好废登,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布淹魄。 她就那樣靜靜地躺著,像睡著了一般堡距。 火紅的嫁衣襯著肌膚如雪甲锡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天羽戒,我揣著相機與錄音缤沦,去河邊找鬼。 笑死易稠,一個胖子當著我的面吹牛缸废,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播驶社,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼企量,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了亡电?” 一聲冷哼從身側響起届巩,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎份乒,沒想到半個月后恕汇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腕唧,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年瘾英,在試婚紗的時候發(fā)現(xiàn)自己被綠了枣接。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡方咆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蟀架,到底是詐尸還是另有隱情瓣赂,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布片拍,位于F島的核電站煌集,受9級特大地震影響,放射性物質發(fā)生泄漏捌省。R本人自食惡果不足惜苫纤,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纲缓。 院中可真熱鬧卷拘,春花似錦、人聲如沸祝高。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽工闺。三九已至乍赫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陆蟆,已是汗流浹背雷厂。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叠殷,地道東北人改鲫。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像林束,于是被迫代替她去往敵國和親钩杰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容