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 <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>

@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码流结构NALUSPS\PPS
此代码是将采集到的原始数据编码为H.264码流写入本地文件,此文件可以利用VLC播放器直接播放,测试结果,注意需要真机测试。
真机获取沙盒文件的方法请见:真机获取沙盒文件
感谢coderWhy和iOSSinger两位的分享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 视频编码与封装方式详解 1.编码方式和封装格式 2.视频编码标准两大系统 MPEG-1 MPEG-2 MPEG-3...
    latthias阅读 6,214评论 0 22
  • 视频编码格式全面解析 我们在详解各种主流的视频格式之前,先抛开各种视频格式的定义,来讨论这样一件事情:你觉得目前的...
    latthias阅读 20,721评论 2 30
  • 视频 视频实质:纯粹的视频(不包括音频)实质上就是一组帧图片,经过视频编码成为视频(video)文件再把音频(au...
    勇敢的_心_阅读 2,784评论 1 30
  • [TOC] 音视频&流媒体 是什么促使我要写这一篇音视频入门文章?那是因为和一妹子打赌码率的概念,结果输了;对一个...
    AllenWu阅读 4,622评论 1 25
  • 小小机器人阅读 347评论 0 0