iOS - 完整搭建直播步骤及具体实现

作者: zerocc2014 | 来源:发表于2017-04-16 16:59 被阅读6265次

    前言

    好记性不如烂笔头,最近有点空把一些知识也整理了一遍,后面陆续写一些总结吧!先从这个不太熟悉的音视频这块开始吧,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)

    相关文章

      网友评论

      • 纠结的哈士奇:很棒的文章,解释很详细:heart:
      • xiaoyouPrince:很好。我最近也在做直播APP,不过用的swift,有些地方借鉴了。等我弄完也写一篇总结,swift版本的:smile:
        纠结的哈士奇:坐等,不知大佬有没swift的项目分享下:kissing_heart:
      • 风之谷等风来:应该不是企业级项目吧,可以把代码放出来吗
        zerocc2014:@风之谷等风来 最近有点忙,git 上有点乱没空整理,不方便哦要再等等
      • DDDDeveloper:写的挺好,挺专业的。
        zerocc2014:@一个有前途的男人 谢谢!

      本文标题:iOS - 完整搭建直播步骤及具体实现

      本文链接:https://www.haomeiwen.com/subject/vculattx.html