iOS 直播流程概述
寫在前面
本文目的在于帶大家了解一場直播背后,需要經歷哪些階段,以及每個階段都做了哪些工作,才能夠把主播的聲音畫面送到觀眾的面前。我們把直播的流程劃分為以下六個階段:
- 采集
- 處理
- 編碼
- 封裝
- 網絡傳輸
- 播放
下面來一一介紹。
采集
采集又分為視頻采集、音頻采集。
一般來說,我們會借助系統 api 來實現這一部分的工作。以 iOS 為例,需要用到 AVFoundation 框架來獲取手機攝像頭拍到的視頻數據,或者使用 ReplayKit 錄制屏幕,以及麥克風收集到的音頻數據。
視頻采集:攝像頭
核心類 AVCaptureXXX
使用攝像頭采集視頻的幾個核心類如下圖所示:
具體代碼如下:
// 1. 創建一個 session
var session = AVCaptureSession.init()
// 2. 獲取硬件設備:攝像頭
guard let device = AVCaptureDevice.default(for: .video) else {
print("獲取后置攝像頭失敗")
return
}
// 3. 創建 input
let input = try AVCaptureDeviceInput.init(device: device)
if session.canAddInput(input) {
session.addInput(input)
}
// 4. 創建 output
let videoOutput = AVCaptureVideoDataOutput.init()
let pixelBufferFormat = kCVPixelBufferPixelFormatTypeKey as String
// 設置 yuv 視頻格式
videoOutput.videoSettings = [pixelBufferFormat: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]
videoOutput.setSampleBufferDelegate(self, queue: outputQueue)
if session.canAddOutput(videoOutput) {
session.addOutput(videoOutput)
}
// 5. 設置預覽 layer:AVCaptureVideoPreviewLayer
let previewViewLayer = videoConfig.previewView.layer
previewViewLayer.backgroundColor = UIColor.black.cgColor
let layerFrame = previewViewLayer.bounds
let videoPreviewLayer = AVCaptureVideoPreviewLayer.init(session: session)
videoPreviewLayer.frame = layerFrame
videoConfig.previewView.layer.insertSublayer(videoPreviewLayer, at: 0)
// 6. 在 output 回調里處理視頻幀:AVCaptureVideoDataOutputSampleBufferDelegate
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// todo: sampleBuffer 視頻幀
}
色彩二次抽樣:YUV
一般來說,我們看到的媒體內容,都經過了一定程度的壓縮。包括直接從 iPhone 攝像頭采集的圖像數據,也會經過色彩二次抽樣這一壓縮過程。
在上一步中創建 output
的時候,我們設置了視頻的輸出格式是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
的。在這句代碼中,我們需要注意到兩個地方:420
和YpCbCr
。
YpCbCr
:代表YUV
(Y-Prime-C-B-C-R) 格式。- Y 指的是亮度信息
- UV 是色彩信息。
人眼對亮點信息更敏感,單靠 Y 數據,可以完美呈現黑白圖像;也就是說可以壓縮 UV 信息,而人眼難以發現。
?下右圖:單靠黑白亮度信息,已經足以描述整個照片的紋理。加上 uv 色彩信息后,就成了下左圖的彩色圖片的效果。
420
:代表的是設備取樣時色彩二次抽樣的參數
4:2:0 中,第一個數,代表幾個關聯的色塊(一般是4);第二個數,代表第一行中包含色彩 uv 信息的像素個數;第三個數,代表第二行中包含色彩 uv 信息的像素個數。(每個像素里都包含亮度信息 Y)
?取樣的時候,一些專業的相機會以 4:4:4 的參數捕捉圖像,面向消費者的 iPhone 相機,通常用 4:2:0 的參數,也能拍出來高質量的視頻或圖片。!
視頻采集:錄屏
錄屏又分為兩種:
- 應用內采集:只能采集當前 app 的屏幕內容
- 應用外采集:可以采集這個手機屏幕的內容,包括退后臺之后,整個手機界面的錄制。一般用來做游戲直播、會議 app 分享屏幕功能。
1. 應用內采集
// iOS 錄屏使用的框架是 ReplayKit
import ReplayKit
// 開始錄屏
RPScreenRecorder.shared().startCapture { sampleBuffer, bufferType, err in
} completionHandler: { err in
}
// 結束錄屏
RPScreenRecorder.shared().stopCapture { err in
}
針對應用內錄屏,有以下兩個 Tip:
- 不想要被錄制進去的 UI ,可以放到自定義 UIWindow 上
- 錄屏同時開啟前置攝像頭,可以獲取
RPScreenRecorder.shared().cameraPreviewView
,并將其添加到當前視圖上。
2. 應用外采集
應用外采集需要創建一個 broadcast upload extension,創建完成后會生成一個 SampleHander
類,在這個類里面可以獲取到采集的視頻數據。
class SampleHandler: RPBroadcastSampleHandler {
func sohuSportUserDefaults() -> UserDefaults? {
return UserDefaults.init(suiteName: "com.xxx.xx")
}
override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
// 開始錄屏,setupInfo 是從 UI extension 傳遞過來的參數
}
override func broadcastPaused() {
// 暫停錄屏
}
override func broadcastResumed() {
// 繼續錄屏
}
override func broadcastFinished() {
// 錄屏結束
}
// 錄屏回調
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
// sampleBuffer
switch sampleBufferType {
case .video:
// 視頻
case .audioApp:
// 應用內聲音
case .audioMic:
// 麥克風聲音
}
}
}
extension 進程和主 app 進程間通信,可以通過以下幾種方式:
- App Group:User Default
- 使用
socket
往 host app 傳輸數據 CFNotification
音頻采集:Audio Unit
iOS 直播中的音頻采集,我們一般會用到 Audio Unit
這一底層框架,這一框架允許我們在采集的時候對錄制的音頻進行一些參數設置,以便獲取到最高質量與最低延遲的音頻。核心代碼如下:
// 創建 audio unit
self.component = AudioComponentFindNext(NULL, &acd);
OSStatus status = AudioComponentInstanceNew(self.component, &_audio_unit);
if (status != noErr) {
[self handleAudiounitCreateFail];
}
// asbd
AudioStreamBasicDescription desc = {0};
desc.mSampleRate = 44100; // 采樣率
desc.mFormatID = kAudioFormatLinearPCM; // 格式
desc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
desc.mChannelsPerFrame = 1; // 聲道數量
desc.mFramesPerPacket = 1; // 每個包中有多少幀, 對于PCM數據而言,因為其未壓縮,所以每個包中僅有1幀數據
desc.mBitsPerChannel = 16;
desc.mBytesPerFrame = desc.mBitsPerChannel / 8 * desc.mChannelsPerFrame;
desc.mBytesPerPacket = desc.mBytesPerFrame * desc.mFramesPerPacket;
// 回調函數
AURenderCallbackStruct callback;
callback.inputProcRefCon = (__bridge void *)(self);
callback.inputProc = handleVideoInputBuffer;
// 設置屬性
AudioUnitSetProperty(self.audio_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &desc, sizeof((desc)));
AudioUnitSetProperty(self.audio_unit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &callback, sizeof((callback)));
UInt32 flagOne = 1;
AudioUnitSetProperty(self.audio_unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne));
// 配置 AVAudioSession
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers error:nil];
[session setActive:YES withOptions:kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation error:nil];
[session setActive:YES error:nil];
#pragma mark - 音頻回調函數
static OSStatus handleVideoInputBuffer(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
//
}
處理
對視頻來說,這一階段的主要工作是拿到 SampleBuffer
,做一下美白、磨皮、濾鏡等效果。本質上來說,這些操作都是在修改每一幀像素點的坐標和顏色變化,流程如下:
?這一階段,常用到的一個三方庫是 GPUImage,這個庫提供了常見的 100+ 濾鏡的算法。它有三個版本:
- GPUImage 1:OC + OpenGL
- GPUImage 2:Swift + OpenGL
- GPUImage 3:Swift + Metal
編碼
在拿到采集處理后的音視頻原數據之后,還要經過編碼壓縮才能往外傳輸數據。
壓縮分為兩種,有損和無損,區別如下:
- 有損壓縮:解壓縮后的數據和壓縮前的不一致,壓縮過程中會丟失一些人眼人耳不敏感的圖像或音頻信息,丟失的信息不可恢復。
- 無損壓縮:壓縮前和壓縮后的數據一致,優化數據的排列等。
視頻的編碼,是為了壓縮它的大小,以便于能夠更快的在網絡上傳輸。很明顯,這是一個有損壓縮過程。在這個過程中,會丟棄掉一些冗余信息,常見的冗余信息如下:
- 空間冗余:圖像相鄰像素之間有較強的相關性
- 時間冗余:視頻序列的相鄰圖像之間內容相似
- 視覺冗余:人的視覺系統對某些細節不敏感
- 知識冗余:規律性的結構可由先驗知識和背景知識得到
- 結構冗余:某些圖片中固定存在的分布模式
總結來說:編碼就是一個丟棄冗余信息的壓縮過程。
視頻編碼過程
具體的編碼過程如下:
- 找到冗余信息:每一幀原始采樣分塊
- 把圖片分組:有差別的像素只有 10% 以內的點,亮度差值變化不超過 2%,而色度差值的變化只有 1% 以內一組稱為 GOP (包括一個 I 幀,多個 P/B 幀)
- 逐幀進行編碼
這個是剪映的一個截圖,我在里面放了一個30幀的視頻。
先看左下角紅框里,我框了5幀圖片出來,這幾幀圖片,內容差別很小,我們可以把他們分成一個組。來處理我們上面說過的時間冗余信息。每一組圖片叫做 GOP 。
再看右邊這個小箭頭,我把箭頭尾部,肩膀這部分放大了,可以看到一個個像素,每個小紅框里假如說是有16*16個像素,就是一個分塊。在這個分塊,我們處理上面說過的空間冗余。
分組,分塊之后。一幀幀的去處理圖片。這就是編碼的大概流程。
I P B 幀
幀的編碼方式:
- 幀內壓縮:壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息。
- 幀間壓縮:相鄰幾幀的數據有很大相關性,連續的視頻相鄰幀之間有冗余信息,又稱作時間壓縮。
在對視頻幀編碼后,原始視頻數據會被壓縮成三種不同類型的視頻幀:I幀、P幀、B幀
- 如下圖所示,第一張圖片是 I 幀,第二個是 P 幀。P 幀相對于 I 幀,三個豆豆往左移動了一點,那么在編碼 P 幀的時候,可以只記錄這個偏移量,其他的信息就參考 I 幀來就行。
- 第三個 B 幀,他的豆豆數量和第四個 I 幀一樣。那我可以直接記錄前三個豆豆相對于P幀的位移,以及最后一個豆豆相對于后面I幀的位移,就可以編解碼這一幀數據了。
- I 幀:關鍵幀,完整編碼的幀??梢岳斫獬梢粡埻暾嬅?,不依賴其他幀。
- P幀:預測幀,參考前面的 I 幀或 P 幀進行編解碼。
- B幀:雙向幀,參考前面的 I 幀或 P 幀,和后面的 P 幀進行編解碼。
H.264、H.265
H.264 的壓縮方式,是在兩方面對視頻幀進行了壓縮:
- 空間:壓縮獨立視頻幀(幀內壓縮)
- 時間:通過以組(GOP)為單位的視頻幀壓縮冗余數據(幀間壓縮)
H.265 是基于 H.264 基礎上,做了些改進,本質上是一樣的。
核心方法如下:
// 創建編碼器
OSStatus status = VTCompressionSessionCreate(NULL, _configuration.videoSize.width, _configuration.videoSize.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoCompressonOutputCallback, (__bridge void *)self, &compressionSession);
// 配置編碼器屬性
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(_videoMaxKeyframeInterval));
//...
// 編碼前資源配置
VTCompressionSessionPrepareToEncodeFrames(compressionSession);
// 編碼
OSStatus status = VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);
音頻編碼
數字音頻壓縮編碼是在保證信號在聽覺方面不產生失真的前提下,對音頻數據信號進行盡可能的壓縮。 去除聲音中冗余成分(不能被人耳察覺的信號,他們對聲音的音色、音調等信息沒有任何幫助)。
音頻冗余信息如下:
- 人耳能聽到的頻率范圍是 20Hz ~ 20kHz,超過這個范圍的聲音都可以丟棄。
- 當一個強音信號和弱音信號同時存在時,弱音信號將被強音信號所掩蔽而聽不見,這樣弱音信號就可以視為冗余信息不用傳送
音頻編碼核心方法如下:
#import <AudioToolbox/AudioToolbox.h>
// 創建編碼器
OSStatus result = AudioConverterNewSpecific(&inputFormat, &outputFormat, 2, requestedCodecs, &m_converter);;
// 編碼
AudioConverterFillComplexBuffer(m_converter, inputDataProc, &buffers, &outputDataPacketSize, &outBufferList, NULL)
封裝
封裝就是把編碼后的音視頻數據,打包放到一個容器格式里。例如 mp4、flv、mov 等
每一種封裝格式有它適合的領域。比方說avi這種格式,它不支持流媒體播放,只能說是有一個完整的打包好的視頻文件,那它就是適合在 bt下載領域應用,而不適合直播這種場景了。
直播中比較常用的兩種封裝格式是 flv 和 ts,他們的區別在于編碼器類型不一樣。
FLV
flv 支持 h.264 & AAC 編碼器,我們這里就以他為例,看一下flv的文件結構是怎樣的:
首先是有一個 flv header,里面包含 flv 的文件表示,以及flv版本信息等等。然后是flv body。body又分為一個個 tag,在 tag 里面才是具體的音頻數據,或者視頻數據信息。
網絡傳輸
在編碼、封裝完之后,就可以進行傳輸數據了。這一階段,通常使用 RTMP 協議傳輸數據。這是一個應用層協議,基于 TCP。
RTMP 協議
?RTMP 協議:https://www.adobe.com/devnet/rtmp.html
在傳輸過程中,rtmp 的報文格式叫做 message 消息。如下圖,這是一個消息的圖示??梢钥吹?,消息又分為 message header 和 message body。
在消息首部,有表示消息類型的 type,有消息的長度信息,有時間戳等信息。
需要關注的是 type 這個字段,rtmp里有十多個消息類型,通過type區分,1到7 是用于協議控制的,8代表這是一個音頻消息,9代表這是一個視頻消息。15到20 負責客戶端服務端之間的交互,比如播放暫停等操作。
右邊是 message body,里面包含具體的數據信息。
在傳遞的過程中,會把消息體再拆分成更小的消息快 chunk。每一個chunk都是128 字節,只有最后一個chunk長度可以小于128。這個過程叫做消息分塊。
總結下整個網絡傳輸流程:
- rtmp 傳輸媒體數據過程中,發送端首先把媒體數據封裝成消息,然后把消息分割成消息塊。最后將分割后的消息快通過 tcp 協議發送出去。
- 接收端在通過 tcp 協議收到數據后,首先把消息塊重新組合成消息,然后通過對消息進行解封裝處理就可以恢復出媒體數據。
播放
最后一步是觀眾端拉流播放:
- 拉流完先解協議,比方說是用rtmp協議,那我就知道了,傳遞過來的是一個個的消息塊 chunk,那我就把拉取到的 chunk 合成消息,就獲取到封裝好的視頻數據了。
- 下一步就是解封裝,判斷這是個 flv 流,還是其他流。解封裝后,就能拿到編碼過的音視頻數據。
- 再接著分別對音視頻進行解碼操作,拿到音頻原始數據,和視頻原始數據。
- 接著做一個音視頻同步的操作,然后把視頻渲染到屏幕上,同時使用麥克風播放音頻 流程圖如下: