扣丁書屋

iOS 直播流程概述

iOS 直播流程概述

寫在前面

本文目的在于帶大家了解一場直播背后,需要經歷哪些階段,以及每個階段都做了哪些工作,才能夠把主播的聲音畫面送到觀眾的面前。我們把直播的流程劃分為以下六個階段:

  1. 采集
  2. 處理
  3. 編碼
  4. 封裝
  5. 網絡傳輸
  6. 播放

下面來一一介紹。

采集

采集又分為視頻采集、音頻采集。

一般來說,我們會借助系統 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 的。在這句代碼中,我們需要注意到兩個地方:420YpCbCr。

  • 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

編碼

在拿到采集處理后的音視頻原數據之后,還要經過編碼壓縮才能往外傳輸數據。

壓縮分為兩種,有損和無損,區別如下:

  • 有損壓縮:解壓縮后的數據和壓縮前的不一致,壓縮過程中會丟失一些人眼人耳不敏感的圖像或音頻信息,丟失的信息不可恢復。
  • 無損壓縮:壓縮前和壓縮后的數據一致,優化數據的排列等。

視頻的編碼,是為了壓縮它的大小,以便于能夠更快的在網絡上傳輸。很明顯,這是一個有損壓縮過程。在這個過程中,會丟棄掉一些冗余信息,常見的冗余信息如下:

  • 空間冗余:圖像相鄰像素之間有較強的相關性
  • 時間冗余:視頻序列的相鄰圖像之間內容相似
  • 視覺冗余:人的視覺系統對某些細節不敏感
  • 知識冗余:規律性的結構可由先驗知識和背景知識得到
  • 結構冗余:某些圖片中固定存在的分布模式

總結來說:編碼就是一個丟棄冗余信息的壓縮過程。

視頻編碼過程

具體的編碼過程如下:

  1. 找到冗余信息:每一幀原始采樣分塊
  2. 把圖片分組:有差別的像素只有 10% 以內的點,亮度差值變化不超過 2%,而色度差值的變化只有 1% 以內一組稱為 GOP (包括一個 I 幀,多個 P/B 幀)
  3. 逐幀進行編碼

這個是剪映的一個截圖,我在里面放了一個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 協議收到數據后,首先把消息塊重新組合成消息,然后通過對消息進行解封裝處理就可以恢復出媒體數據。

播放

最后一步是觀眾端拉流播放:

  1. 拉流完先解協議,比方說是用rtmp協議,那我就知道了,傳遞過來的是一個個的消息塊 chunk,那我就把拉取到的 chunk 合成消息,就獲取到封裝好的視頻數據了。
  2. 下一步就是解封裝,判斷這是個 flv 流,還是其他流。解封裝后,就能拿到編碼過的音視頻數據。
  3. 再接著分別對音視頻進行解碼操作,拿到音頻原始數據,和視頻原始數據。
  4. 接著做一個音視頻同步的操作,然后把視頻渲染到屏幕上,同時使用麥克風播放音頻 流程圖如下:


https://mp.weixin.qq.com/s/n5ImksCKgUwtl0VdCRSvtw

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 7月以前  |  15813次閱讀
快速配置 Sign In with Apple 2年以前  |  5436次閱讀
APP適配iOS11 3年以前  |  4415次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4241次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4161次閱讀
使用 GPUImage 實現一個簡單相機 2年以前  |  3884次閱讀
開篇 關于iOS越獄開發 3年以前  |  3783次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3707次閱讀
給數組NSMutableArray排序 3年以前  |  3633次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3337次閱讀
UITableViewCell高亮效果實現 3年以前  |  3336次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3232次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3073次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2847次閱讀
使用最高權限操作iPhone手機 3年以前  |  2818次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>