iOS 直播专题4-音视频编码

现在的高清手机拍个照都有2M以上,按照人眼帧率24帧/秒的速度传输,网络数度需要达到2 * 24M/秒,一般日常中的网络显然不可能有这样的网速。这时就需要对音视频进行编码压缩了。

常用的编码类型有:

  • 视频编码:H.264、H.265、VP8、VP9
  • 音频编码:aac、Opus、mp3

生活中常说的mp4、avi、flv等指的是封装格式,就是个容器,把音视频、字幕、媒体信息等装进容器里,编码在这里充当的是压缩音视频的角色,这样才能减少体积。

名词介绍

YUV

视频裸数据的一种格式,大部分设备的视频帧数据都是YUV,“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。跟我们熟悉的RGB类似,YUV也是一种颜色编码方法。

  • 主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。

  • NV12和NV21属于YUV420格式,是一种two-plane模式,苹果iOS的视频是NV12格式,Android的视频是NV21格式

H.264

拍摄一段视频,如果全身基本不动,只有嘴唇在动,如果记录每一帧的画面那么这段视频会非常大,如果只记录每个帧之间的变化,观看视频每帧的原始数据都可以在上一帧的基础上加上这一帧的变化数据还原出来,不仅大幅度缩小体积,还能保持比较好的画质,整个过程就是H.264的编码、解码
在H264协议里定义了三种帧:
I帧:完整编码的帧,也叫关键帧
P帧:参考之前的I帧生成的只包含差异部分编码的帧
B帧:参考前后的帧编码的帧叫B帧

H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。
H264码流可以分为两层:VCL层和NAL层
NAL:Network abstraction layer,叫网络抽象层,它保存了H264相关的参数信息和图像信息,NAL层由多个单元NALU组成,NALU由NALU头(00 00 00 01或者00 00 01)、sps(序列参数集)、pps(图像参数集合)、slice、sei、IDR帧、I帧(在图像运动变化较少时,I帧后面是7个P帧,如果图像运动变化大时,一个序列就短了,I帧后面可能是3个或者4个P帧)、P帧、B帧等数据。

H264原始码流是由一个接一个的NALU(Nal Unit)组成的,NALU = 开始码 + NAL类型 + 视频数据
开始码用于标示这是一个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01"
NALU类型如下:

类型 说明
0 未规定
1 非IDR图像中不采用数据划分的片段
2 非IDR图像中A类数据划分片段
3 非IDR图像中B类数据划分片段
4 非IDR图像中C类数据划分片段
5 IDR图像的片段
6 补充增强信息(SEI)
7 序列参数集(SPS)
8 图像参数集(PPS)
9 分割符
10 序列结束符
11 流结束符
12 填充数据
13 序列参数集扩展
14 带前缀的NAL单元
15 子序列参数集
16-18 保留
19 不采用数据划分的辅助编码图像片段
20 编码片段扩展
21-23 保留
24-31 未规定

一般我们只用到了1、5、7、8这4个类型就够了。类型为5表示这是一个I帧,I帧前面必须有SPS和PPS数据,也就是类型为7和8,类型为1表示这是一个P帧或B帧。

GOP

Group of picture图像组,也就是两个I帧之间的距离,GOP值越大,那么I帧率之间P帧和B帧数量越多,图像画质越精细,如果GOP是120,如果分辨率是720P,帧率是60,那么两I帧的时间就是120/60=2s.

SPS

sequence parameter set,序列参数集(解码相关信息,档次级别、分别率、某档次中编码工具开关标识和涉及的参数、时域可分级信息等)

PPS

picture parameter set, 图像参数集(一幅图像所用的公共参数,一幅图像中所有SS应用同一个PPS,初始图像控制信息,初始化参数、分块信息)

IDR

Instantaneous Decoding Refresh, 即时解码刷新,跟I帧是同一个东西,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。

H.265

比H.264更加高效的编码方式是H.265,是H.264的下一个版本,它属于下一代MPEG-H标准而非MPEG-4,比H264有着更强的压缩效率,被称为HEVC(High Efficiency Video Coding/高效视频编码),同码率下理论占用空间节省了50%足足一半,动态画面表现会更加清晰。

  • 市面上,虽然已经有一些新手机、电视、CPU都支持硬解H265了,但更多的现存老设备都不能向上支持硬解,软解更吃不消。
  • H265的商业授权费太贵,市场都没成熟呢,网络视频站点更不乐意花大钱推。但国内IDC带宽成本比较高,能降低一半的流量,无论对我们用户,还是对运营商,都是很香的,相信再过几年,H265就会很快普及了。

PCM

原始的未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。
描述PCM数据的6个参数:

  • Sample Rate : 采样频率。8kHz(电话)、44.1kHz(CD)、48kHz(DVD)。
  • Sample Size : 量化位数。通常该值为16-bit。
  • Number of Channels : 通道个数。常见的音频有立体声(stereo)和单声道(mono)两种类型,立体声包含左声道和右声道。另外还有环绕立体声等其它不太常用的类型。
  • Sign : 表示样本数据是否是有符号位,比如用一字节表示的样本数据,有符号的话表示范围为-128 ~ 127,无符号是0 ~ 255。
  • Byte Ordering : 字节序。字节序是little-endian还是big-endian。通常均为little-endian。字节序说明见第4节。
  • Integer Or Floating Point : 整形或浮点型。大多数格式的PCM样本数据使用整形表示,而在一些对精度要求高的应用方面,使用浮点类型表示PCM样本数据。

AAC

全称Advanced Audio Coding,高级音频编码,是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。

  • 优点:相较于mp3,AAC格式的音质更佳,文件更小。

  • 不足:AAC属于有损压缩的格式,与时下流行的APE、FLAC等无损格式相比音质存在“本质上”的差距。加之,传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上“小巧”的光环不复存在。

帧率

单位为fps(frame pre second),视频画面每秒有多少帧画面,数值越大画面越流畅

码率

单位为bps(bit pre second),视频每秒输出的数据量,数值越大画面越清晰

分辨率

视频画面像素密度,例如常见的720P、1080P等

关键帧间隔

每隔多久编码一个关键帧

流程:


image.png

视频编码

分为软编码和音编码方式

  • 软编码
    使用CPU进行编码。
    H.264 初始化软编码:
- (void)initCompressionSession{
    _sendQueue = dispatch_queue_create("com.youku.laifeng.h264.sendframe", DISPATCH_QUEUE_SERIAL);
    [self initializeNALUnitStartCode];
    _lastPTS = kCMTimeInvalid;
    _timescale = 1000;
    frameCount = 0;
#ifdef DEBUG
    enabledWriteVideoFile = NO;
    [self initForFilePath];
#endif
    
    _encoder = [WSAVEncoder encoderForHeight:(int)_configuration.videoSize.height andWidth:(int)_configuration.videoSize.width bitrate:(int)_configuration.videoBitRate];
    [_encoder encodeWithBlock:^int(NSArray* dataArray, CMTimeValue ptsValue) {
        [self incomingVideoFrames:dataArray ptsValue:ptsValue];
        return 0;
    } onParams:^int(NSData *data) {
        [self generateSPSandPPS];
        return 0;
    }];
}

H.264开始软编码:

- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
  
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    CMVideoFormatDescriptionRef videoInfo = NULL;
    CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);
    
    CMTime frameTime = CMTimeMake(timeStamp, 1000);
    CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);
    CMSampleTimingInfo timing = {duration, frameTime, kCMTimeInvalid};
    
    CMSampleBufferRef sampleBuffer = NULL;
    CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, YES, NULL, NULL, videoInfo, &timing, &sampleBuffer);
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    [_encoder encodeFrame:sampleBuffer];
    CFRelease(videoInfo);
    CFRelease(sampleBuffer);

    frameCount++;
}
  • 硬编码
    不使用CPU进行编码,使用显卡(GPU)进行硬件加速,专用的DSP、FPGA、ASIC芯片等硬件进行编码。
    iOS8.0以上才支持硬编码
    H.264初始化硬编码:
    - (void)resetCompressionSession {
    if (compressionSession) {
        VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);

        VTCompressionSessionInvalidate(compressionSession);
        CFRelease(compressionSession);
        compressionSession = NULL;
    }

    // allocator  分配器,设置为默认分配
    // width 宽
    // height 高
    // encoderSpecification 编码规范,设置nil由videoToolbox自己选择
    // imageBufferAttributes 源像素缓冲区属性.设置nil不让videToolbox创建,而自己创建
    // compressedDataAllocator 压缩数据分配器.设置nil,默认的分配
    // outputCallback 编码回调
    // refcon 回调客户定义的参考值,此处把self传过去,因为我们需要在C函数中调用self的方法,而C函数无法直接调self
    // compressionSessionOut 编码会话
    OSStatus status = VTCompressionSessionCreate(NULL, _configuration.videoSize.width, _configuration.videoSize.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoCompressonOutputCallback, (__bridge void *)self, &compressionSession);
    if (status != noErr) {
        return;
    }

    _currentVideoBitRate = _configuration.videoBitRate;
    /// 设置关键帧(GOPsize)间隔,GOP太小的话图像会模糊
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
    /// 最大帧率间隔
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval/_configuration.videoFrameRate));
    /// 设置期望帧率,不是实际帧率
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(_configuration.videoFrameRate));
    /// 码率,码率大了话就会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊,但也勉强能看
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(_configuration.videoBitRate));
    /// 数据率限制
    NSArray *limit = @[@(_configuration.videoBitRate * 1.5/8), @(1)];
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    /// 设置实时编码输出(避免延迟)
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
    /// 是否产生B帧(因为B帧在解码时并不是必要的,是可以抛弃B帧的)
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CABAC);
    VTCompressionSessionPrepareToEncodeFrames(compressionSession);

}

H.264开始编码:

- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
    if(_isBackGround) return;
    frameCount++;
    CMTime presentationTimeStamp = CMTimeMake(frameCount, (int32_t)_configuration.videoFrameRate);
    VTEncodeInfoFlags flags;
    CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);

    NSDictionary *properties = nil;
    if (frameCount % (int32_t)_configuration.videoMaxKeyframeInterval == 0) {
        properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
    }
    NSNumber *timeNumber = @(timeStamp);

    OSStatus status = VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);
    if(status != noErr){
        [self resetCompressionSession];
    }
}

H.265敬请期待。。。。。。。。!

音频编码

AAC编码:

- (void)encodeAudioData:(nullable NSData*)audioData timeStamp:(uint64_t)timeStamp {
    if (![self createAudioConvert]) {
        return;
    }
    
    if(leftLength + audioData.length >= self.configuration.bufferLength){
        /// 发送
        NSInteger totalSize = leftLength + audioData.length;
        NSInteger encodeCount = totalSize/self.configuration.bufferLength;
        char *totalBuf = malloc(totalSize);
        char *p = totalBuf;
        
        memset(totalBuf, (int)totalSize, 0);
        memcpy(totalBuf, leftBuf, leftLength);
        memcpy(totalBuf + leftLength, audioData.bytes, audioData.length);
        
        for(NSInteger index = 0;index < encodeCount;index++){
            [self encodeBuffer:p  timeStamp:timeStamp];
            p += self.configuration.bufferLength;
        }
        
        leftLength = totalSize%self.configuration.bufferLength;
        memset(leftBuf, 0, self.configuration.bufferLength);
        memcpy(leftBuf, totalBuf + (totalSize -leftLength), leftLength);
        
        free(totalBuf);
        
    }else{
        /// 积累
        memcpy(leftBuf+leftLength, audioData.bytes, audioData.length);
        leftLength = leftLength + audioData.length;
    }
}

- (void)encodeBuffer:(char*)buf timeStamp:(uint64_t)timeStamp{
    
    AudioBuffer inBuffer;
    inBuffer.mNumberChannels = 1;
    inBuffer.mData = buf;
    inBuffer.mDataByteSize = (UInt32)self.configuration.bufferLength;
    
    AudioBufferList buffers;
    buffers.mNumberBuffers = 1;
    buffers.mBuffers[0] = inBuffer;
    
    
    // 初始化编码后输出缓冲列表
    AudioBufferList outBufferList;
    outBufferList.mNumberBuffers = 1;
    outBufferList.mBuffers[0].mNumberChannels = inBuffer.mNumberChannels;
    outBufferList.mBuffers[0].mDataByteSize = inBuffer.mDataByteSize;   // 设置缓冲区大小
    outBufferList.mBuffers[0].mData = aacBuf;           // 设置AAC缓冲区
    UInt32 outputDataPacketSize = 1;
    if (AudioConverterFillComplexBuffer(m_converter, inputDataProc, &buffers, &outputDataPacketSize, &outBufferList, NULL) != noErr) {
        return;
    }
    
    WSAudioFrame *audioFrame = [WSAudioFrame new];
    audioFrame.timestamp = timeStamp;
    audioFrame.data = [NSData dataWithBytes:aacBuf length:outBufferList.mBuffers[0].mDataByteSize];
    
    char exeData[2];
    exeData[0] = _configuration.asc[0];
    exeData[1] = _configuration.asc[1];
    audioFrame.audioInfo = [NSData dataWithBytes:exeData length:2];
    if (self.aacDeleage && [self.aacDeleage respondsToSelector:@selector(audioEncoder:audioFrame:)]) {
        [self.aacDeleage audioEncoder:self audioFrame:audioFrame];
    }
    
    if (self->enabledWriteVideoFile) {
        NSData *adts = [self adtsData:_configuration.numberOfChannels rawDataLength:audioFrame.data.length];
        fwrite(adts.bytes, 1, adts.length, self->fp);
        fwrite(audioFrame.data.bytes, 1, audioFrame.data.length, self->fp);
    }
    
}

项目源码下载

你可能感兴趣的:(iOS 直播专题4-音视频编码)