iOS音视频开发-视频硬编码(H264)

  • 视频编码
    视频编码分为软编码和硬编码:

    • 软编码:
      1.利用CPU进行大批量的编码计算处理。
      2.兼容性好。
      3.耗电量大,手机发烫(很烫,感觉要爆炸了O(∩_∩)O~)

    • 硬编码
      1.利用GPU进行编码处理。
      2.兼容性略差。
      3.手机不会很烫。(硬编码需要iOS8及以上版本可以使用,之前并未开发,之前版本只能软编码。)

这里记录硬编码的实现,软编码后续会记录。

  • H264
    视频编码需要了解的编码格式,H264/AVC为视频编码格式,需要将采集到的视频帧编码为H264格式的数据。
    H264的特点:

    • 1.更高的编码效率:同H.263等标准的特率效率相比,能够平均节省大于50%的码率。
    • 2.高质量的视频画面:H.264能够在低码率情况下提供高质量的视频图像,在较低带宽上提供高质量的图像传输是H.264的应用亮点。
    • 3.提高网络适应能力:H.264可以工作在实时通信应用(如视频会议)低延时模式下,也可以工作在没有延时的视频存储或视频流服务器中。

    H264的优势:
    H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。低码率(Low Bit Rate)对H.264的高的压缩比起到了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。
    PS:以上摘自百度百科。需要了解的可自行百度。

我们将采集到的视频数据编码为H264数据流,那采集到的原始视频数据是什么呢?实际上是YUV420格式的数据,视频采集那篇文章记录了设置输出设备的输出格式为:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 表示原始数据的格式为YUV420。那我们为什么要设置输出为YUV420数据呢,YUV420数据是什么呢?这篇文章介绍的很详细YUV和RGB。
简单来说有以下几点:
1.YUV420采样数据大小为RGB格式的一半(采样数据后续涉及到推流,所以数据越小越好)。
2.YUV格式所有编码器都支持,RGB格式却存在不兼容的情况。
3.YUV420格式适用于便携式设备。

代码如下:

#import 
#import 

@interface BBH264Encoder : NSObject
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;
@end

#import "BBH264Encoder.h"

@interface BBH264Encoder()
/** 记录当前的帧数 */
@property (nonatomic, assign) NSInteger frameID;

/** 编码会话 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSessionRef;

/** 文件写入对象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end

@implementation BBH264Encoder

- (instancetype)init{
    if (self = [super init]) {
        // 1.初始化写入文件的对象(NSFileHandle用于写入二进制文件)
        [self setupFileHandle];
        
        // 2.初始化压缩编码的会话
        [self setupCompressionSession];
        
    }
    return self;
}

- (void)setupFileHandle {
    // 1.获取沙盒路径
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.h264"];
    
    // 2.如果原来有文件,则删除
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    
    // 3.创建对象
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}

- (void)setupCompressionSession{
    
    //0.用于记录当前是第几帧数据(画面帧数非常多)
    _frameID = 0;
    
    //1.清空压缩上下文
    if (_compressionSessionRef) {
        VTCompressionSessionCompleteFrames(_compressionSessionRef, kCMTimeInvalid);
        VTCompressionSessionInvalidate(_compressionSessionRef);
        CFRelease(_compressionSessionRef);
        _compressionSessionRef = NULL;
    }
    
    //2.录制视频的宽度&高度
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;
    
    //3.创建压缩会话
    OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, bbCompressionSessionCallback, (__bridge void * _Nullable)(self), &_compressionSessionRef);
    
    //4.判断状态
    if (status != noErr) return;
    
    //5.设置参数
    //Profile_level,h264的协议等级,不同的清晰度使用不同的ProfileLevel
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    
    // 关键帧最大间隔
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(30)));
    
    // 设置平均码率 单位是byte
    int bitRate = [self getResolution];
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    
    // 码率上限 接收数组类型CFArray[CFNumber] [bytes,seconds,bytes,seconds...] 单位是bps
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef _Nullable)@[@(bitRate*1.5/8), @1]);
    
    // 设置期望帧率
    int fps = 30;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    // 设置实时编码
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    
    // 关闭重排Frame
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
    
    // 设置比例16:9(分辨率宽高比)
    VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AspectRatio16x9, kCFBooleanTrue);
    
    //6.准备编码
    VTCompressionSessionPrepareToEncodeFrames(_compressionSessionRef);

}

/**
 编码回调
 */
static void bbCompressionSessionCallback(
                                            void * CM_NULLABLE outputCallbackRefCon,
                                            void * CM_NULLABLE sourceFrameRefCon,
                                            OSStatus status,
                                            VTEncodeInfoFlags infoFlags,
                                            CM_NULLABLE CMSampleBufferRef sampleBuffer ){
    
    BBH264Encoder *encoder = (__bridge BBH264Encoder *)(outputCallbackRefCon);
    
    //1.判断状态是否为没有错误
    if (status != noErr) {
        return;
    }
    
    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
    BOOL isKeyframe = NO;
    if (attachments != NULL) {
        CFDictionaryRef attachment;
        CFBooleanRef dependsOnOthers;
        attachment = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
        dependsOnOthers = CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
        dependsOnOthers == kCFBooleanFalse ? (isKeyframe = YES) : (isKeyframe = NO);
    }
    
    //2.是否为关键帧
    if (isKeyframe) {
        //SPS and PPS.
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        size_t spsSize, ppsSize;
        size_t parmCount;
        const uint8_t* sps, *pps;
        
        OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &parmCount, NULL );
        //获取SPS无错误则继续获取PPS
        if (status == noErr) {
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &parmCount, NULL );
            
            NSData *spsData = [NSData dataWithBytes:sps length:spsSize];
            NSData *ppsData = [NSData dataWithBytes:pps length:ppsSize];
            
            //写入文件
            [encoder gotSpsPps:spsData pps:ppsData];
            
        }else{
            return;
        }
    }
    
    
    //3.前4个字节表示长度,后面的数据的长度
    // 除了关键帧,其它帧只有一个数据
    char  *buffer;
    size_t total;
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, NULL, &total, &buffer);
    
    if (statusCodeRet == noErr) {
        size_t offset = 0;
        //返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        int const headerLenght = 4;
        
        //循环获取NAL unit数据
        while (offset < total - headerLenght) {
            int NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, buffer + offset, headerLenght);
            
            //从大端转系统端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            NSData *data = [NSData dataWithBytes:buffer + headerLenght + offset length:NALUnitLength];
            
            // Move to the next NAL unit in the block buffer
            offset += headerLenght + NALUnitLength;
            
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
        }
    }
}

/**
 获取屏幕分辨率
 */
- (int)getResolution{
    CGRect screenRect = [[UIScreen mainScreen] bounds];
    CGSize screenSize = screenRect.size;
    CGFloat scale = [UIScreen mainScreen].scale;
    CGFloat screenX = screenSize.width * scale;
    CGFloat screenY = screenSize.height * scale;
    return screenX * screenY;
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    // 1.拼接NALU的header
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    // 2.将NALU的头&NALU的体写入文件
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:sps];
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:pps];
    
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData %d", (int)[data length]);
    if (self.fileHandle != NULL)
    {
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:data];
    }
}

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 1.将sampleBuffer转成imageBuffer
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.根据当前的帧数,创建CMTime的时间
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
    VTEncodeInfoFlags flags;
    
    // 3.开始编码该帧数据
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSessionRef,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, (__bridge void * _Nullable)(self), &flags);
    if (statusCode == noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    }
}

- (void)endEncode {
    VTCompressionSessionCompleteFrames(self.compressionSessionRef, kCMTimeInvalid);
    VTCompressionSessionInvalidate(self.compressionSessionRef);
    CFRelease(self.compressionSessionRef);
    self.compressionSessionRef = NULL;
    [self.fileHandle closeFile];
    self.fileHandle = NULL;
}
@end

上述H264码流的NALU和SPS、PPS是什么呢?关于H264码流结构NALU、SPS\PPS。
此代码是将采集到的原始数据编码为H.264码流写入本地文件,此文件可以利用VLC播放器直接播放,测试结果,注意需要真机测试。
真机获取沙盒文件的方法请见:真机获取沙盒文件。
感谢coderWhy和iOSSinger两位的分享。

你可能感兴趣的:(iOS音视频开发-视频硬编码(H264))