iOS - 完整搭建直播步驟及具體實現

前言

好記性不如爛筆頭,最近有點空把一些知識也整理了一遍,后面陸續寫一些總結吧!先從這個不太熟悉的音視頻這塊開始吧,2016年可謂是直播元年,這塊的技術也沒什么很陌生了,后知后覺的自己最近也開始學習學習,有挺多調用 C 函數,如果是進行編碼封裝格式最好還是用c語言寫跨平臺且效率也更佳,后面有時間嘗試用c寫下,此文只是直播技術的一部分,未完待續,如有誤歡迎指正,當然如果看完覺得有幫助也請幫忙點個喜歡??。

技術整體概覽

  • 直播的技術

    • 直播技術總體來說分為采集、前處理、編碼、傳輸、解碼、渲染這幾個環節
  • 流程圖例:


    直播流程.jpeg

音視頻采集

采集介紹

  • 音視頻的采集是直播架構的第一個環節,也是直播的視頻來源,視頻采集有多個應用場景,比如二維碼開發。音視頻采集包括兩部分:

  • 視頻采集

  • 音頻采集

  • iOS 開發中,是可以同步采集音視頻,相關的采集 API 封裝在 AVFoundation 框架中,使用方式簡單

采集步驟

  • 導入框架 AVFoundation 框架
  • 創建捕捉會話(AVCaptureSession)
    • 該會話用于連接之后的輸入源&輸出源
    • 輸入源:攝像頭&話筒
    • 輸出源:拿到對應的音頻&視頻數據的出口
    • 會話:用于將輸入源&輸出源連接起來
  • 設置視頻輸入源&輸出源相關屬性
    • 輸入源(AVCaptureDeviceInput):從攝像頭輸入
    • 輸出源(AVCaptureVideoDataOutput):可以設置代理,在代理中處理對應輸入后得到的數據,以及設置例如丟幀等情況的處理
    • 將輸入&輸出添加到會話中

采集代碼

  • 自定義一個繼承 NSObjectCCVideoCapture 類,用用于處理音視頻的采集
  • CCVideoCapture 代碼如下:
//
//  CCVideoCapture.m
//  01.視頻采集
//
//  Created by zerocc on 2017/3/29.
//  Copyright ? 2017年 zerocc. All rights reserved.
//

#import "CCVideoCapture.h"
#import <AVFoundation/AVFoundation.h>

@interface CCVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;

@end

@implementation CCVideoCapture

- (void)startCapturing:(UIView *)preView
{    
    // ===============  采集視頻  ===========================
    // 1. 創建 session 會話
    AVCaptureSession *session = [[AVCaptureSession alloc] init];
    session.sessionPreset = AVCaptureSessionPreset1280x720;
    self.session = session;
    
    
    // 2. 設置音視頻的輸入
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
 
    NSError *error;
    AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error];
    [self.session addInput:videoInput];
    
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
    [self.session addInput:audioInput];

    
    // 3. 設置音視頻的輸出
    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    dispatch_queue_t videoQueue = dispatch_queue_create("Video Capture Queue", DISPATCH_QUEUE_SERIAL);
    [videoOutput setSampleBufferDelegate:self queue:videoQueue];
    [videoOutput setAlwaysDiscardsLateVideoFrames:YES];       
    if ([session canAddOutput:videoOutput]) {
        [self.session addOutput:videoOutput];
    }
    
    AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    dispatch_queue_t audioQueue = dispatch_queue_create("Audio Capture Queue", DISPATCH_QUEUE_SERIAL);
    [audioOutput setSampleBufferDelegate:self queue:audioQueue];
    if ([session canAddOutput:audioOutput]) {
        [session addOutput:audioOutput];
    }
    
    // 4. 獲取視頻輸入與輸出連接,用于分辨音視頻數據
      // 視頻輸出方向 默認方向是相反設置方向,必須在將 output 添加到 session 之后
    AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
    self.videoConnection = videoConnection;
    if (videoConnection.isVideoOrientationSupported) {
        videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
        [self.session commitConfiguration];
    }else {
        NSLog(@"不支持設置方向");
    }
    
    // 5. 添加預覽圖層
    AVCaptureVideoPreviewLayer *layer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    self.layer = layer;
    layer.frame = preView.bounds;
    [preView.layer insertSublayer:layer atIndex:0];
    
    // 6. 開始采集
    [self.session startRunning];
}

// 丟幀視頻情況
- (void)captureOutput:(AVCaptureOutput *)captureOutput
  didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
    
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    if (self.videoConnection == connection) {
        dispatch_sync(queue, ^{

        });
        
        NSLog(@"采集到視頻數據");
    } else {
        dispatch_sync(queue, ^{
            
        });

        NSLog(@"采集到音頻數據");
    }

    NSLog(@"采集到視頻畫面");
}

- (void)stopCapturing
{
    [self.encoder endEncode];
    [self.session stopRunning];
    [self.layer removeFromSuperlayer];
}

@end

音視頻編碼

音視頻壓縮編碼介紹

  • 不經編碼的視頻非常龐大,存儲起來都麻煩,更何況網絡傳輸

    • 編碼通過壓縮音視頻數據來減少數據體積,方便音視頻數據的推流、拉流和存儲,能大大提高存儲傳輸效率
    • 以錄制視頻一秒鐘為例,需要裸流視頻多大空間呢?
    • 視頻無卡頓效果一秒鐘至少16幀(正常開發一般 30FPS )
    • 假如該視頻是一個1280*720分辨率的視頻
    • 對應像素用RGB,3*8
    • 結果多大:(1280 x 720 x 30 x 24) / (1024 x 1024) = 79MB
  • 錄制一秒鐘音頻,又需要多大空間?

    • 取樣頻率若為44.1KHz
    • 樣本的量化比特數為16
    • 普通立體聲的信號通道數為2
    • 音頻信號的傳輸率 = 取樣頻率 x 樣本的量化比特數 x 通道數;那么計算一秒鐘音頻數據量大概為1.4MB
  • 音視頻中的冗余信息

  • 視頻(裸流)存在冗余信息,空間冗余、時間冗余、視覺冗余等等,經過一系列的去除冗余信息,可以大大的降低視頻的數據量,更利用視頻的保存、傳輸。去除冗余信息的過程,我們就稱之為視頻圖像壓縮編碼。

  • 數字音頻信號包含了對人感受可以忽略的冗余信息,時域冗余、頻域冗余、聽覺冗余等等。

  • 音視頻編碼方式:

    • 硬編碼:使用非 CPU 進行編碼,如利用系統提供的顯卡 GPU、專用 DSP 芯片等
    • 軟編碼:使用 CPU 進行編碼(手機容易發熱)
  • 各個平臺處理:

    • iOS端:硬編碼兼容性較好,可以直接進行硬編碼,蘋果在iOS 8.0后,開放系統的硬件編碼解碼功能,Video ToolBox 的框架來處理硬件的編碼和解碼,蘋果將該框架引入iOS系統。
    • Android端:硬編碼較難,難找到統一的庫兼容各個平臺(推薦使用軟編碼)
  • 編碼標準:

    • 視頻編碼:H.26X 系列、MPEG 系列(由ISO[國際標準組織機構]下屬的MPEG[運動圖像專家組]開發)、C-1、VP8、VP9等

    • H.261:主要在老的視頻會議和視頻電話產品中使用

    • H.263:主要用在視頻會議、視頻電話和網絡視頻上

    • H.264:H.264/MPEG-4 第十部分。或稱AVC (Advanced Video Coding, 高級視頻編碼),是一種視頻壓縮標準,一種被廣泛使用的高精度視頻錄制、壓縮和發布格式。

    • H.265:高效率視頻編碼(High Efficency Video Coding, 簡稱HEVC) 是一種視頻壓縮標準,H.264/MPEG-4 AVC 的繼承者。可支持4K 分辨率甚至到超高畫質電視,最高分辨率可達到8192*4320(8K分辨率),這是目前發展的趨勢,尚未有大眾化編碼軟件出現

    • MPEG-1第二部分:MPEG-1第二部分主要使用在VCD 上,有寫在有線視頻也是用這種格式

    • MPEG-2第二部分:MPEG-2第二部分等同于H.262,使用在DVD、SVCD和大多數數字視頻廣播系統中

    • MPEG-4第二部分:MPEG-4第二部分標準可以使用在網絡傳輸、廣播和媒體存儲上

    • 音頻編碼:AAC、Opus、MP3、AC-3等

視頻壓縮編碼 H.264 (AVC)

  • 序列(GOP - Group of picture)

    • 在H264中圖像以序列為單位進行組織,一個序列是一段圖像編碼后的數據流
    • 一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是I幀圖像
      • H.264 引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列
      • 這樣,如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會
      • IDR 圖像之后的圖像永遠不會使用 IDR 之前的圖像的數據來解碼
    • 一個序列就是一段內容差異不太大的圖像編碼后生成的一串數據流
      • 當運動變化較少時,一個序列可以很長,因為運動變化少就代表圖像畫面的內容變動很小,所以就可以編一個I幀,I幀做為隨機訪問的參考點,然后一直P幀、B幀了
      • 當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀
  • 在H264協議里定義了三種幀

    • I幀:完整編碼的叫做I幀,關鍵幀
    • P幀:參考之前的I幀生成的只包含差異部分編碼的幀叫做P幀
    • B幀:參考前后的幀(I&P)編碼的幀叫B幀
  • H264的壓縮方法

    • 分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化幀數不宜取多
    • 定義幀:將每組內容幀圖像定義為三種類型:既I幀、P幀、B幀
    • 預測幀:以I幀做為基礎幀,以I幀預測P幀,再由P幀預測B幀
    • 數據傳輸:最后將I幀數據與預測的差值信息進行存儲和傳輸
    • H264采用的核心算法是幀內壓縮和幀間壓縮
      • 幀內壓縮也稱為空間壓縮是生成I幀的算法,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。
      • 幀間壓縮也稱為時間壓縮是生成P幀和B幀的算法,它通過比較時間軸上不同幀之間的數據進行壓縮,通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。
  • 分層設計

    • H264算法在概念上分為兩層:視頻編碼層(VCL:Video Coding Layer)負責高效的視頻內容表示;網絡提取層(NAL:Network Abstraction Layer)負責以網絡所要求的恰當的方式對數據進行打包和傳送。這樣,高效編碼和網絡友好性分別VCL 和 NAL 分別完成
    • NAL設計目的,根據不同的網絡把數據打包成相應的格式,將VCL產生的比特字符串適配到各種各樣的網絡和多元環境中
  • NAL 的封裝方式

    • H.264流數據正是由一系列的 NALU 單元(NAL Unit, 簡稱NALU)組成的。

    • NALU 是將每一幀數據寫入到一個 NALU 單元中,進行傳輸或存儲的

    • NALU 分為 NALU 頭和 NALU 體

    • NALU 頭通常為00 00 00 01,作為要給新的 NALU 的起始標識

    • NALU體封裝著VCL編碼后的信息或者其他信息

      h264碼流結構.png
  • 封裝過程

    • 對于流數據來說,一個NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭,
      0x00 00 01因此被稱為開始碼(Start code)。
    • 提取 SPS (Picture Parameter Sets) 圖像參數集和 PPS (Sequence Parameter Set)序列參數集 生成 FormatDesc 非VCL的NAL單元,一般來說編碼器編出的首幀數據為 PPS 與 SPS 。
    • 每個NALU的開始碼是0x00 00 01,按照開始碼定位NALU
    • 通過類型信息找到sps和pps并提取,開始碼后第一個byte的后5位,7代表sps,8代表pps
    • 使用 CMSampleBufferGetFormatDescription函數來構建 CMVideoFormatDescriptionRef
  • 提取視頻圖像數據生成 CMBlockBuffer,編碼后的I幀、后續B幀、P幀數據

  • 自定義一個CCH264Encoder類處理 視頻(裸流) h264 硬編碼,代碼如下:

//
//  CCH264Encoder.h
//  01.視頻采集
//
//  Created by zerocc on 2017/4/4.
//  Copyright ? 2017年 zerocc. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>

@interface CCH264Encoder : NSObject

- (void)prepareEncodeWithWidth:(int)width height:(int)height;

- (void)encoderFram:(CMSampleBufferRef)sampleBuffer;

- (void)endEncode;

@end
//
//  CCH264Encoder.m
//  01.視頻采集
//
//  Created by zerocc on 2017/4/4.
//  Copyright ? 2017年 zerocc. All rights reserved.
//

#import "CCH264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>
#import <AudioToolbox/AudioToolbox.h>

@interface CCH264Encoder () {
    int     _spsppsFound;
}

@property (nonatomic, assign) VTCompressionSessionRef compressionSession;  // coreFoundation中的對象 == c語言對象 不用*,strong只用來修飾oc對象,VTCompressionSessionRef對象本身就是指針  用assign修飾
@property (nonatomic, assign) int frameIndex;

@property (nonatomic, strong) NSFileHandle *fileHandle;
@property (nonatomic, strong) NSString *documentDictionary;

@end

@implementation CCH264Encoder

#pragma mark - lazyload

- (NSFileHandle *)fileHandle {
    if (!_fileHandle) {
        // 這里只寫一個裸流,只有視頻流沒有音頻流
        NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) firstObject] stringByAppendingPathComponent:@"video.h264"];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL isExistPath = [fileManager isExecutableFileAtPath:filePath];
        if (isExistPath) {
            [fileManager removeItemAtPath:filePath error:nil];
        }
        
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
        _fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
    }
    
    return _fileHandle;
}


/**
 準備編碼

 @param width 采集的寬度
 @param height 采集的高度
 */
- (void)prepareEncodeWithWidth:(int)width height:(int)height
{
    // 0. 設置默認是第0幀
    self.frameIndex = 0;
    
    // 1. 創建 VTCompressionSessionRef
    
    /**
     VTCompressionSessionRef 

     @param NULL CFAllocatorRef - CoreFoundation分配內存的模式,NULL默認
     @param width int32_t - 視頻寬度
     @param height 視頻高度
     @param kCMVideoCodecType_H264 CMVideoCodecType - 編碼的標準
     @param NULL CFDictionaryRef       encoderSpecification,
     @param NULL CFDictionaryRef       sourceImageBufferAttributes
     @param NULL CFAllocatorRef    compressedDataAllocator
     @param didComparessionCallback  VTCompressionOutputCallback - 編碼成功后的回調函數c函數
     @param _Nullable  void * - 傳遞到回調函數中參數
     @return session
     */
    VTCompressionSessionCreate(NULL,
                               width,
                               height,
                               kCMVideoCodecType_H264,
                               NULL, NULL, NULL,
                               didComparessionCallback,
                               (__bridge void * _Nullable)(self),
                               &_compressionSession);
    
    // 2. 設置屬性
      // 2.1 設置實時編碼
    VTSessionSetProperty(_compressionSession,
                         kVTCompressionPropertyKey_RealTime,
                         kCFBooleanTrue);
      // 2.2 設置幀率
    VTSessionSetProperty(_compressionSession,
                         kVTCompressionPropertyKey_ExpectedFrameRate,
                         (__bridge CFTypeRef _Nonnull)(@24));
      // 2.3 設置比特率(碼率) 1500000/s
    VTSessionSetProperty(_compressionSession,
                         kVTCompressionPropertyKey_AverageBitRate,
                         (__bridge CFTypeRef _Nonnull)(@1500000)); // 每秒有150萬比特  bit
      // 2.4 關鍵幀最大間隔,也就是I幀。
    VTSessionSetProperty(_compressionSession,
                         kVTCompressionPropertyKey_DataRateLimits,
                         (__bridge CFTypeRef _Nonnull)(@[@(1500000/8), @1]));  // 單位是 8 byte
      // 2.5 設置GOP的大小
    VTSessionSetProperty(_compressionSession,
                         kVTCompressionPropertyKey_MaxKeyFrameInterval,
                         (__bridge CFTypeRef _Nonnull)(@20));

    // 3. 準備編碼
    VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
}


/**
 開始編碼

 @param sampleBuffer CMSampleBufferRef
 */
- (void)encoderFram:(CMSampleBufferRef)sampleBuffer
{
    
    // 2. 開始編碼
    // 將CMSampleBufferRef 轉換成 CVImageBufferRef,
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CMTime pts = CMTimeMake(self.frameIndex, 24);
    CMTime duration = kCMTimeInvalid;
    VTEncodeInfoFlags flags;
    
    /**
     硬編碼器 - 將攜帶視頻幀數據的imageBuffer送到硬編碼器中,進行編碼。
     
     @param session#> VTCompressionSessionRef
     @param imageBuffer#> CVImageBufferRef
     @param presentationTimeStamp#> CMTime - pts
     @param duration#> CMTime - 傳一個固定的無效的時間
     @param frameProperties#> CFDictionaryRef
     @param sourceFrameRefCon#> void - 編碼后回調函數中第二個參數
     @param infoFlagsOut#> VTEncodeInfoFlags - 編碼后回調函數中第四個參數
     @return
     */
    VTCompressionSessionEncodeFrame(self.compressionSession,
                                    imageBuffer,
                                    pts,
                                    duration,
                                    NULL,
                                    NULL,
                                    &flags);
    NSLog(@"開始編碼一幀數據");
}

#pragma mark - 獲取編碼后的數據  c語言函數 - 編碼后的回調函數

void didComparessionCallback (void * CM_NULLABLE outputCallbackRefCon,
                              void * CM_NULLABLE sourceFrameRefCon,
                              OSStatus status,
                              VTEncodeInfoFlags infoFlags,
                              CM_NULLABLE CMSampleBufferRef sampleBuffer)
{
    // c語言中不能調用當前對象self.語法不行,只有通過指針去做相應操作
    // 獲取當前 CCH264Encoder 對象,通過傳入的 self 參數(VTCompressionSessionCreate中傳入了self)
    CCH264Encoder *encoder = (__bridge CCH264Encoder *)(outputCallbackRefCon);

    // 1. 判斷該幀是否為關鍵幀
    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
    BOOL isKeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
    
    // 2. 如果是關鍵幀 -> 獲取 SPS/PPS 數據 其保存了h264視頻的一些必要信息方便解析 -> 并且寫入文件
    if (isKeyFrame && !encoder->_spsppsFound) {
        encoder->_spsppsFound = 1;
        // 2.1. 從CMSampleBufferRef獲取CMFormatDescriptionRef
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 2.2. 獲取 SPS /pps的信息
        const uint8_t *spsOut;
        size_t spsSize, spsCount;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
                                                           0,
                                                           &spsOut,
                                                           &spsSize,
                                                           &spsCount,
                                                           NULL);
        const uint8_t *ppsOut;
        size_t ppsSize, ppsCount;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
                                                           1,
                                                           &ppsOut,
                                                           &ppsSize,
                                                           &ppsCount,
                                                           NULL);
        
        // 2.3. 將SPS/PPS 轉成NSData,并且寫入文件
        NSData *spsData = [NSData dataWithBytes:spsOut length:spsSize];
        NSData *ppsData = [NSData dataWithBytes:ppsOut length:ppsSize];
        
        // 2.4. 寫入文件 (NALU單元特點:起始都是有 0x00 00 00 01 每個NALU需拼接)
        [encoder writeH264Data:spsData];
        [encoder writeH264Data:ppsData];
    }
    
    // 3. 獲取編碼后的數據,寫入文件
     // 3.1. 獲取 CMBlockBufferRef
    CMBlockBufferRef blcokBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    
     // 3.2. 從blockBuffer 中獲取起始位置的內存地址
    size_t totalLength = 0;
    char *dataPointer;
    CMBlockBufferGetDataPointer(blcokBuffer, 0, NULL, &totalLength, &dataPointer);
    
     // 3.3. 一幀的圖像可能需要寫入多個NALU 單元 --> Slice切換
    static const int H264HeaderLength = 4;  // 頭部長度一般為 4
    size_t bufferOffset = 0;
    while (bufferOffset < totalLength - H264HeaderLength) {
        // 3.4 從起始位置拷貝H264HeaderLength長度的地址,計算NALULength
        int NALULength = 0;
        memcpy(&NALULength, dataPointer + bufferOffset, H264HeaderLength);
        
        // H264 編碼的數據是大端模式(字節序),轉化為iOS系統的模式,計算機內一般都是小端,而網絡和文件中一般都是大端
        NALULength = CFSwapInt32BigToHost(NALULength);
        
        // 3.5 從dataPointer開始,根據長度創建NSData
        NSData *data = [NSData dataWithBytes:(dataPointer + bufferOffset + H264HeaderLength) length:NALULength];
        
        // 3.6 寫入文件
        [encoder writeH264Data:data];
        
        // 3.7 重新設置 bufferOffset
        bufferOffset += NALULength + H264HeaderLength;
    }
    NSLog(@"。。。。。。。。編碼出一幀數據");
};

- (void)writeH264Data:(NSData *)data
{
    // 1. 先獲取 startCode
    const char bytes[] = "\x00\x00\x00\x01";
    
    // 2. 獲取headerData
    // 減一的原因: byts 拼接的是字符串,而字符串最后一位有個 \0;所以減一才是其正確長度
    NSData *headerData = [NSData dataWithBytes:bytes length:(sizeof(bytes) - 1)];
    [self.fileHandle writeData:headerData];
    [self.fileHandle writeData:data];
}

- (void)endEncode
{
    VTCompressionSessionInvalidate(self.compressionSession);
    CFRelease(_compressionSession);
}
@end

  • 調試步驟

  • 下載 VLC 播放器

    VLC下載.png
點擊Devices
dowload 到桌面
右鍵顯示包內容.png
沙盒文件將這些拖入VLC進行測試播放即可.png

音頻編碼 AAC

  • AAC音頻格式有ADIF和ADTS:
    • ADIF:Audio Data Interchange Format 音頻數據交換格式。這種格式的特征是可以確定的找到這個音頻數據的開始,ADIF只有一個統一的頭,所以必須得到所有的數據后解碼,即它的解碼必須在明確定義的開始處進行。故這種格式常用在磁盤文件中。
    • ADTS:Audio Data Transport Stream 音頻數據傳輸流。這種格式的特征是它是一個有同步字的比特流,解碼可以在這個流中任何位置開始。它的特征類似于mp3數據流格式。語音系統對實時性要求較高,基本流程是采集音頻數據,本地編碼,數據上傳,服務器處理,數據下發,本地解碼,下面學習分析也都以這個格式為例進行。
  • AAC原始碼流(又稱為“裸流”)是由一個一個的ADTS frame組成的。其中每個ADTS frame之間通過syncword(同步字)進行分隔。同步字為0xFFF(二進制“111111111111”)。AAC碼流解析的步驟就是首先從碼流中搜索0x0FFF,分離出ADTS frame;然后再分析ADTS frame的首部各個字段。ADTS frame的組成:
    • ADTS幀頭包含固定幀頭、可變幀頭,其中定義了音頻采樣率、音頻聲道數、幀長度等關鍵信息,用于幀凈荷數據的解析和解碼。
    • ADTS幀凈荷主要由原始幀組成。
    • 下圖中表示出了ADTS一幀的簡明結構,其兩邊的空白矩形表示一幀前后的數據。
ADTS結構.jpg
  • ADTS 內容及結構

  • ADTS的頭信息,一般都是7個字節 (也有是9字節的),分為2部分:adts_fixed_header()adts_variable_header()

    • ADTS 的固定頭 adts_fixed_header() 結構組成:
      adts_fixed_header().jpg
      • syncword:同步字,12比特的 "1111 1111 1111"
      • ID:MPEG 標志符,0表示MPEG-4, 1表示MPEG-2
      • layer:表示音頻編碼的層
      • protection_absent:表示是否誤碼校驗
      • profile:表示使用哪個級別的 AAC
      • sampling_frequency_index:表示使用的采樣率索引
      • channel_configuration:表示聲道數
    • ADTS 的可變頭adts_variable_header()結構組成:
      adts_variable_header().jpg
    • aac_frame_lenth:ADTS幀的長度
    • adts_buffer_fullness:0x7FF 說明是碼流可變的碼流
    • number_of_raw_data_blocks_in_frame:每一個ADTS幀中原始幀的數量
  • 自定義一個CCAACEncoder類處理 音頻(裸流) AAC 硬編碼,代碼如下:

//
//  CCH264Encoder.h
//  01.視頻采集
//
//  Created by zerocc on 2017/4/4.
//  Copyright ? 2017年 zerocc. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>

@interface CCAACEncoder : NSObject
- (void)encodeAAC:(CMSampleBufferRef)sampleBuffer;
- (void)endEncodeAAC;

@end

//
//  CCH264Encoder.h
//  01.視頻采集
//
//  Created by zerocc on 2017/4/4.
//  Copyright ? 2017年 zerocc. All rights reserved.
//

#import "CCAACEncoder.h"

@interface CCAACEncoder()
@property (nonatomic, assign) AudioConverterRef audioConverter;
@property (nonatomic, assign) uint8_t *aacBuffer;
@property (nonatomic, assign) NSUInteger aacBufferSize;
@property (nonatomic, assign) char *pcmBuffer;
@property (nonatomic, assign) size_t pcmBufferSize;
@property (nonatomic, strong) NSFileHandle *audioFileHandle;

@end

@implementation CCAACEncoder

- (void) dealloc {
    AudioConverterDispose(_audioConverter);
    free(_aacBuffer);
}

- (NSFileHandle *)audioFileHandle {
    if (!_audioFileHandle) {
        NSString *audioFile = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"audio.aac"];
        [[NSFileManager defaultManager] removeItemAtPath:audioFile error:nil];
        [[NSFileManager defaultManager] createFileAtPath:audioFile contents:nil attributes:nil];
        _audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:audioFile];
    }
    
    return _audioFileHandle;
}

- (id)init {
    if (self = [super init]) {
        _audioConverter = NULL;
        _pcmBufferSize = 0;
        _pcmBuffer = NULL;
        _aacBufferSize = 1024;
        _aacBuffer = malloc(_aacBufferSize * sizeof(uint8_t));
        memset(_aacBuffer, 0, _aacBufferSize);
    }
    
    return self;
}

- (void)encodeAAC:(CMSampleBufferRef)sampleBuffer
{
    CFRetain(sampleBuffer);
    // 1. 創建audio encode converter
    if (!_audioConverter) {
        // 1.1 設置編碼參數
        AudioStreamBasicDescription inputAudioStreamBasicDescription = *CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)CMSampleBufferGetFormatDescription(sampleBuffer));
    
        AudioStreamBasicDescription outputAudioStreamBasicDescription = {
            .mSampleRate = inputAudioStreamBasicDescription.mSampleRate,
            .mFormatID = kAudioFormatMPEG4AAC,
            .mFormatFlags = kMPEG4Object_AAC_LC,
            .mBytesPerPacket = 0,
            .mFramesPerPacket = 1024,
            .mBytesPerFrame = 0,
            .mChannelsPerFrame = 1,
            .mBitsPerChannel = 0,
            .mReserved = 0
        };
        
        static AudioClassDescription description;
        UInt32 encoderSpecifier = kAudioFormatMPEG4AAC;
        
        UInt32 size;
        AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                        sizeof(encoderSpecifier),
                                        &encoderSpecifier,
                                        &size);
        
        unsigned int count = size / sizeof(AudioClassDescription);
        AudioClassDescription descriptions[count];
        AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                                    sizeof(encoderSpecifier),
                                    &encoderSpecifier,
                                    &size,
                                    descriptions);
        
        for (unsigned int i = 0; i < count; i++) {
            if ((kAudioFormatMPEG4AAC == descriptions[i].mSubType) &&
                (kAppleSoftwareAudioCodecManufacturer == descriptions[i].mManufacturer)) {
                memcpy(&description , &(descriptions[i]), sizeof(description));
            }
        }
        
        AudioConverterNewSpecific(&inputAudioStreamBasicDescription,
                                                    &outputAudioStreamBasicDescription,
                                                    1,
                                                    &description,
                                                    &_audioConverter);
    }
    
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    CFRetain(blockBuffer);
    OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &_pcmBufferSize, &_pcmBuffer);
    NSError *error = nil;
    if (status != kCMBlockBufferNoErr) {
        error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
    }
    memset(_aacBuffer, 0, _aacBufferSize);
    
    AudioBufferList outAudioBufferList = {0};
    outAudioBufferList.mNumberBuffers = 1;
    outAudioBufferList.mBuffers[0].mNumberChannels = 1;
    outAudioBufferList.mBuffers[0].mDataByteSize = (int)_aacBufferSize;
    outAudioBufferList.mBuffers[0].mData = _aacBuffer;
    AudioStreamPacketDescription *outPacketDescription = NULL;
    UInt32 ioOutputDataPacketSize = 1;

    status = AudioConverterFillComplexBuffer(_audioConverter, inInputDataProc, (__bridge void *)(self), &ioOutputDataPacketSize, &outAudioBufferList, outPacketDescription);
    if (status == 0) {
        NSData *rawAAC = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData
                                        length:outAudioBufferList.mBuffers[0].mDataByteSize];
        NSData *adtsHeader = [self adtsDataForPacketLength:rawAAC.length];
        NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
        [fullData appendData:rawAAC];
        [self.audioFileHandle writeData:fullData];
    } else {
        error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
    }
    
    CFRelease(sampleBuffer);
    CFRelease(blockBuffer);
}

/**
 編碼器回調 C函數
 
 @param inAudioConverter xx
 @param ioNumberDataPackets xx
 @param ioData xx
 @param outDataPacketDescription xx
 @param inUserData xx
 @return xx
 */
OSStatus inInputDataProc(AudioConverterRef inAudioConverter,
                         UInt32 *ioNumberDataPackets,
                         AudioBufferList *ioData,
                         AudioStreamPacketDescription **outDataPacketDescription,
                         void *inUserData)
{
    CCAACEncoder *encoder = (__bridge CCAACEncoder *)(inUserData);
    UInt32 requestedPackets = *ioNumberDataPackets;
    
    // 填充PCM到緩沖區
    size_t copiedSamples = encoder.pcmBufferSize;
    ioData->mBuffers[0].mData = encoder.pcmBuffer;
    ioData->mBuffers[0].mDataByteSize = (int)encoder.pcmBufferSize;
    encoder.pcmBuffer = NULL;
    encoder.pcmBufferSize = 0;

    if (copiedSamples < requestedPackets) {
        //PCM 緩沖區還沒滿
        *ioNumberDataPackets = 0;
        return -1;
    }
    *ioNumberDataPackets = 1;
    
    return noErr;
}

/**
 *  Add ADTS header at the beginning of each and every AAC packet.
 *  This is needed as MediaCodec encoder generates a packet of raw
 *  AAC data.
 */
- (NSData *) adtsDataForPacketLength:(NSUInteger)packetLength {
    int adtsLength = 7;
    char *packet = malloc(sizeof(char) * adtsLength);
    // Variables Recycled by addADTStoPacket
    int profile = 2;  //AAC LC
    //39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
    int freqIdx = 4;  //44.1KHz
    int chanCfg = 1;  //MPEG-4 Audio Channel Configuration. 1 Channel front-center
    NSUInteger fullLength = adtsLength + packetLength;
    // fill in ADTS data
    packet[0] = (char)0xFF; // 11111111     = syncword
    packet[1] = (char)0xF9; // 1111 1 00 1  = syncword MPEG-2 Layer CRC
    packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
    packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));
    packet[4] = (char)((fullLength&0x7FF) >> 3);
    packet[5] = (char)(((fullLength&7)<<5) + 0x1F);
    packet[6] = (char)0xFC;
    NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];
    
    return data;
}

- (void)endEncodeAAC
{
    AudioConverterDispose(_audioConverter);
    _audioConverter = nil;
}

@end

  • 測試音視頻
    • 采集音視頻 CCVideoCapture類中導入 CCH264EncoderCCAACEncoder
#import "CCVideoCapture.h"
#import <AVFoundation/AVFoundation.h>
#import "CCH264Encoder.h"
#import "CCAACEncoder.h"

@interface CCVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>

@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;

@property (nonatomic, strong) CCH264Encoder *videoEncoder;
@property (nonatomic , strong) CCAACEncoder *audioEncoder;

@end

@implementation CCVideoCapture

#pragma mark - lazyload

- (CCH264Encoder *)videoEncoder {
    if (!_videoEncoder) {
        _videoEncoder = [[CCH264Encoder alloc] init];
    }
    
    return _videoEncoder;
}

- (CCAACEncoder *)audioEncoder {
    if (!_audioEncoder) {
        _audioEncoder = [[CCAACEncoder alloc] init];
    }
    
    return _audioEncoder;
}

- (void)startCapturing:(UIView *)preView
{
    // ===============  準備編碼  ===========================
    [self.videoEncoder prepareEncodeWithWidth:720 height:1280];
    
    
    // ===============  采集視頻  ===========================
    // 1. 創建 session 會話
    AVCaptureSession *session = [[AVCaptureSession alloc] init];
    session.sessionPreset = AVCaptureSessionPreset1280x720;
    self.session = session;
    .
    .
    .

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    if (self.videoConnection == connection) {
        dispatch_sync(queue, ^{
            [self.videoEncoder encoderFram:sampleBuffer];
        });
        
        NSLog(@"采集到視頻數據");
    } else {
        dispatch_sync(queue, ^{
            [self.audioEncoder encodeAAC:sampleBuffer];
        });

        NSLog(@"采集到音頻數據");
    }

    NSLog(@"采集到視頻畫面");
}

- (void)stopCapturing
{
    [self.videoEncoder endEncode];
    [self.audioEncoder endEncodeAAC];
    [self.session stopRunning];
    [self.layer removeFromSuperlayer];
}

未完待續

參考資料

編碼和封裝、推流和傳輸
AAC以 ADTS 格式封裝的分析
[音頻介紹](https://wenku.baidu.com/view/278f415e804d2b160b4ec0ac.html%20%20)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容