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阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容