iOS音视频

740 阅读11分钟

AVFoundation

  • 捕捉会话: AVCaptureSession.

  • 捕捉设备: AVCaptureDevice.

  • 捕捉设备输入: AVCaptureDevicelnput

  • 捕捉设备输出: AVCaptureOutput抽象类.

    • AVCaptureStillImageOutput
    • AVCaputureMovieFileOutput
    • AVCaputureAudioDataOutput
    • AVCaputureVideoDataOutput
  • 捕捉连接: AVCaptureConnection

  • 捕捉预览: AVCaptureVideoPreviewLayer

设置Session

  • 初始化
  • 设置分辨率
  • 配置输入设备(注意转换为AVCaptureDevicelnput)
  • 配置输入设备包括音频输入,视频输入
  • 配置输出(静态图像输出,视频文件输出)
  • 在为session添加输入输出时.注意一定判断能否添加.原因是:摄像头并不隶属任何一个APP,它公共设备.

在i0S中,会经常使用到 session的方式.比如我们使用任何硬件设备都要使用对应的session,麦克风就要使用 AudioSession,使用 Camera就要使用 AVCaptureSession,使用编码则需要使用 VTCompressionSession.解码时,要使用 VTDecompressionSessionRef.

捕捉设备输入

采集视频,音频

  • 使用iOS原生框架AVFoundation.framework

视频滤镜处理

  • 使用iOS原生框架 CoreImage.framework
  • 使用第三方框架 GPUImage.framework

CoreImage与GPUImage 框架比较:

在实际项目开发中,开发者更加倾向使用于 GPUImage 框架.

首先它在使用性能上与iOS提供的原生框架,并没有差别;其次它的使用便利性高于iOS原生框架,最后也是最重要的 GPUImage框架是开源的.而大家如果想要学习GPUImage框架,建议学习OpenGL ES,其实 GPUImage的封装和思维都是基于 OpenGL ES.

音频输入

/********************音频相关**********/
//音频设备
@property (nonatomic, strong) AVCaptureDeviceInput *audioInputDevice;
//输出数据接收
@property (nonatomic, strong) AVCaptureAudioDataOutput *audioDataOutput;
//捕捉音频连接
@property (nonatomic, strong) AVCaptureConnection *audioConnection;

#pragma mark-init Audio/video
- (void)setupAudio{
    //麦克风设备
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    //将audioDevice ->AVCaptureDeviceInput 对象
    self.audioInputDevice = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];
    //音频输出
    self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
    [self.audioDataOutput setSampleBufferDelegate:self queue:self.captureQueue];
    //配置
    [self.captureSession beginConfiguration];
    if ([self.captureSession canAddInput:self.audioInputDevice]) {
        [self.captureSession addInput:self.audioInputDevice];
    }
    if([self.captureSession canAddOutput:self.audioDataOutput]){
        [self.captureSession addOutput:self.audioDataOutput];
    }
    [self.captureSession commitConfiguration];
    
    self.audioConnection = [self.audioDataOutput connectionWithMediaType:AVMediaTypeAudio];
}

视频输入

/********************视频相关**********/
//当前使用的视频设备
@property (nonatomic, weak) AVCaptureDeviceInput *videoInputDevice;
//前后摄像头
@property (nonatomic, strong) AVCaptureDeviceInput *frontCamera;
@property (nonatomic, strong) AVCaptureDeviceInput *backCamera;
//输出数据接收
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
//捕捉视频连接
@property (nonatomic, strong) AVCaptureConnection *videoConnection;

- (void)setupVideo{
    //所有video设备
    NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    //前置摄像头
    self.frontCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.lastObject error:nil];
    self.backCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.firstObject error:nil];
    //设置当前设备为前置
    self.videoInputDevice = self.backCamera;
    //视频输出
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    [self.videoDataOutput setSampleBufferDelegate:self queue:self.captureQueue];
    [self.videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];
    //kCVPixelBufferPixelFormatTypeKey它指定像素的输出格式,这个参数直接影响到生成图像的成功与否
   // kCVPixelFormatType_420YpCbCr8BiPlanarFullRange  YUV420格式.
    
    [self.videoDataOutput setVideoSettings:@{
                                             (__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
                                             }];
    //配置
    [self.captureSession beginConfiguration];
    if ([self.captureSession canAddInput:self.videoInputDevice]) {
        [self.captureSession addInput:self.videoInputDevice];
    }
    if([self.captureSession canAddOutput:self.videoDataOutput]){
        [self.captureSession addOutput:self.videoDataOutput];
    }
    //分辨率
    [self setVideoPreset];
    [self.captureSession commitConfiguration];
    //commit后下面的代码才会有效
    self.videoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
    //设置视频输出方向
    self.videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    
    //fps
    /*
     FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。
     FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的动作就会越流畅。通常,要避免动作不流畅的最低是30。某些计算机视频格式,每秒只能提供15帧。
     
     */
    [self updateFps:25];
    //设置预览
    [self setupPreviewLayer];
}

捕捉设备输出

视频内容的捕捉。当设置捕捉会话时,添加一个名为 AVCaptureMovieFileOutput 的输出。这个了定义了方法将 QuickTime 影片捕捉到磁盘。这个类大多数核心功能继承于超类 AVCaptureFileOutput. 这个超类定义了许多实用功能。比如录制到最长时限或录制到特定文件大小时为止。还可以配置成保留最小可用的磁盘空间。这一点在存储空间有限的移动设备上录制视频时非常重要。

通常当 QuickTime 影片准备发布时,影片头的元数据处于文件的开始位置。这样可以让视频播放器快速读取头包含信息,来确定文件的内容、结构和其包含的多个样本的位置。

截屏2021-04-07 上午8.59.36.png

不过,当录制一个 QuickTime 影片时,直到所有的样片都完成捕捉后才能创建信息头。当录制结束时,创建头数据并将它附在文件结尾处。

截屏2021-04-07 上午9.00.40.png

将创建头的过程放在所有影片样本完成捕捉之后存在一个问题,尤其是在移动设备的情况下。如果遇到崩溃或其他中断,比如有电话拨入,则影片头就不会被正确写入,会在磁盘生成一个不可读的影片文件。AVCaptureMovieFileOutput 提供一个核心功能就是分段捕捉 QuickTime 影片。

截屏2021-04-07 上午9.02.19.png

当录制开始时,在文件最前面写入一个最小化的头信息,随着录制的进行,片段按照一定的周期写入,创建完整的头信息。默认状态下,每 10 秒写入一个片段,不过这个时间的间隔可以通过修改捕捉设备输出的 movieFragentlnterval 属性来改变。写入片段的方式可以逐步创建完整的 QuickTime 影片头。这样确保了当遇到应用程序崩溃或中断时,影片仍然会以最好的一个写入片段为终点进行保存。我们用默认的间隔来做这 demo,但是如果你可以在你的的 APP 修改这个值。

编码解码

何为编码?

编码就是按照一定的格式记录采样和量化后的数据。

编码中软编码和硬编码的区别?

  • 硬编码:使用非CPU进行编码,例如使用GPU芯片处理。 性能高,低码率下通常质量低于软编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
  • 软编码:使用CPU来进行编码计算。 实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。

硬编码

  • 视频:VideoToolBox框架
  • 音频:AudioToolBox框架

软编码

  • 视频:使用 FFmpeg,X264 算法把视频原数据YUV/RGB编码成H264
  • 音频:使用fdk_aac将音频数据PCM转换成AAC

视频

VideoToolBox 硬编码

什么是视频的编码解码?

视频数据为什么可以压缩呢,因为视频数据存在冗余。通俗地理解,例如一个视频中,前一秒画面跟当前的画面内容相似度很高,那么这两秒的数据我们是不是可以不用全部保存,只保留一个完整的画面,下一个画面看有哪些地方有变化了记录下来,拿视频去播放的时候我们就按这个完整的画面和其他有变化的地方把其他画面也恢复出来。记录画面不同然后保存下来这个过程就是数据编码,根据不同的地方恢复画面的过程就是数据解码。

H264是一种视频编码标准 在H264协议里定义了三种帧:

  • I帧:完整编码的帧,也叫关键帧
  • P帧:参考之前的I帧生成的只包含差异部分编码的帧
  • B帧:参考前后的帧编码的帧叫B帧

视频到底是什么?

截屏2021-04-07 上午9.09.05.png

内容元素:
  • 图像(Image)
  • 音频(Audio)
  • 元信息(Metadata)
编码格式:
  • Video: H264
  • Audio: AAC
容器(视频封装格式)
  • 封装格式:就是将已经编码压缩好的视频数据和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器.当然可以理解为这只是一个外壳.
  • 通常我们不仅仅只存放音频数据和视频数据,还会存放一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的. 常见的视频容器格式:
  • AVI:是当时为对抗quicktime格式 (mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
  • MOV:是Quicktime封装
  • WMV:微软推出的,作为市场竞争
  • mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
  • flv:这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
  • MP4:主要应用于mpeg4的封装,主要在手机上使用。

VideoToolbox工作流程

VideoToolbox 基于 Core Foundation 库函数,C语言

  1. 创建session -> 设置编码相关参数 -> 开始编码 -> 循环输入源数据(YUV类型的数据,直接从摄像头获取)-> 获取编码后的 H264 数据 -> 结束编码
  2. H264文件

编码的输入和输出

在AVFoundation 回调方法中,它有提供我们的数据其实就是CVPixelBuffer.只不过当时使用的是引用类型CVImageBufferRef,其实就是CVPixelBuffer的另外一个定义,Camera返回的CVImageBuffer 中存储的数据是一个CVPixelBuffer,而经过VideoToolBox编码输出的CMSampleBuffer 中存储的数据是一个CMBlockBuffer的引用. 截屏2021-04-07 上午9.14.05.png

解码

解码思路

既然NALU,一个接一个实时解码!首先,你要对数据解析!分析NALU数据.前面4个字节是起始位!标识一个NALU的开始!从第5位才开始来获取!从第五位才是NALU数据类型.要获取到第5位数据,转化十进制,然后根据表格判断它数据类型(第5个字节是表示数据类型,转为10进制后,7是sps, 8是pps, 5是IDR(I帧)信息)!判断好数据类型,才能将NALU送入解码器.SPS/PPS获取就可以,是不需求解码的!

  1. 解析数据(NALU Unit) I/P/B...
    • 既然NALU,一个接一个实时解码!首先,你要对数据解析!分析NALU数据.前面4个字节是起始位!标识一个NALU的开始!从第5位才开始来获取!从第五位才是NALU数据类型.
    • 要获取到第5位数据,转化十进制,然后根据表格判断它数据类型!
    • 判断好数据类型,才能将NALU送入解码器.SPS/PPS获取就可以,是不需求解码的!
    • CVPixelBufferRef 保存是解码后的数据或者未编码前的数据
  2. 初始化解码器
  3. 将解析后的H264 NALU Unit输入解码器
  4. 解码完成回调,输出解码数据
  5. 解码数据显示(OpenGL ES)

解码三个核心函数:

  1. 创建session, VTDecompressionsessionCreate
  2. 解码一个frame, VTDecompressionSessionDecodeFrame
  3. 销毁解码session, VTDecompressionSessionInvalidate

原理分析:

H264原始码流 -> NALU.

  • I帧:保留了一张完整视频帧.解码关键!
  • P帧:先前参考帧.差异数据.解码需要依赖于I帧
  • B帧:双向参考帧.解码时既需要I帧,也需要P帧! 如果H264码流中I帧错误/丢失,就会导致错误传递,P/B帧单独是完成不了解码工作!花屏的现象产生.

音频

PCM(脉冲编码调制)

模拟 -> 数字信号得到的数据PCM数据

截屏2021-04-07 下午6.53.34.png

  1. 采样
  2. 量化
  3. 编码

音频压缩原理

数字音频信号包含的对人们感受信息影响可以忽略的成分成为冗余

  1. 时域冗余
  2. 频域冗余
  3. 听觉冗余(人所能听到的声音型号频率为20Hz-20KHz,在这个频率以外的声音可以压缩去掉,这种冗余称为听觉冗余)
  • 消除冗余数据(有损编码)

    因为采集过程,采集各种频率声音!我们可以丢弃人耳无法听到那一部分声音数据.大大减少数据的存储!可以直接从源数据干掉!

  • 哈夫曼无损编码:

    除了人耳部分听不到声音压缩之外,其他的声音数据都原样保留!压缩后数据能够完全复原!(短码高频,长码低频)

音频冗余信息

  • 压缩主要办法:去除采集的音频冗余信息!人耳听觉范围以外的数据,被遮蔽的音频信号.
  • 遮蔽效应:一个较弱的声音的听觉会被另一个较强的声音影响!
  • 信号:频域遮蔽和时域遮蔽.

AudioToolbox能做什么?

在AAC编码的场景下,源格式就是采集的PCM数据,目的格式就是AAC.

  1. 设置编码器(codec),并开始录制
  2. 收集PCM数据,传给编码器
  3. 编码完成后回调callback,写入文件