美文网首页音视频开发音视频及流媒体
iOS音视频开发-视频硬编码(H264)

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

作者: ibabyblue | 来源:发表于2018-01-31 17:56 被阅读325次
    • 视频编码
      视频编码分为软编码和硬编码:

      • 软编码:
        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两位的分享。

    相关文章

      网友评论

        本文标题:iOS音视频开发-视频硬编码(H264)

        本文链接:https://www.haomeiwen.com/subject/ciqkzxtx.html