H264硬编码

一、前言

前一段时间一直在做新的项目,已经很久没更新文章了,今天终于闲下来了,笔者很高兴呐,趁这时间,分享一下(直播)拉流端视频编码。iOS8.0以后,可以直接用系统的VideoToolbox框架进行编解码,CPU占用率相比较ffmpeg来说会低很多,在做拉流时,我就是用的这个框架。下面废话也不多说了,直接讲解步骤上代码。

二、视频硬编码

1.编码器类DDHardwareVideoEncoder.h中,该类继承自DDVideoEncoding(编码器抽象接口类)其中,DDLiveVideoConfiguration是视频配置文件,里面是视频帧率、输入方向、分辨率等视频相关属性,具体文件实现如下:

#import "DDVideoEncoding.h"

@interface DDHardwareVideoEncoder : NSObject<DDVideoEncoding>

#pragma mark - Initializer
///=============================================================================
/// @name Initializer
///=============================================================================
- (nullable instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (nullable instancetype)new UNAVAILABLE_ATTRIBUTE;

@property (nonatomic, strong, nonnull) DDLiveVideoConfiguration *configuration;

// 是否插入关键帧
@property (assign, nonatomic) BOOL isInsertKeyFrame;

@end

2.编码器抽象接口类DDVideoEncoding.h文件实现如下:
其中DDVideoFrame类是编码成功后数据处理类,里面有每帧编码成功后的data(h264裸流)、videoFrameRate(帧率)、frameCount(帧数)、timestamp(时间戳)等属性。

#import <Foundation/Foundation.h>
#import "DDVideoFrame.h"
#import "DDLiveVideoConfiguration.h"

@protocol DDVideoEncoding;
/// 编码器编码后回调
@protocol DDVideoEncodingDelegate <NSObject>
@required
- (void)videoEncoder:(nullable id<DDVideoEncoding>)encoder videoFrame:(nullable DDVideoFrame*)frame;
@end

/// 编码器抽象的接口
@protocol DDVideoEncoding <NSObject>
@required
- (void)encodeVideoData:(nullable CVImageBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp;

- (void)stopEncoder;
@optional
@property (nonatomic, assign) NSInteger videoBitRate;
- (nullable instancetype)initWithVideoStreamConfiguration:(nullable DDLiveVideoConfiguration*)configuration;
- (void)setDelegate:(nullable id<DDVideoEncodingDelegate>)delegate;

3.下面是具体实现的DDHardwareVideoEncoder.m类文件

#import "DDHardwareVideoEncoder.h"
#import <VideoToolbox/VideoToolbox.h>

@interface DDHardwareVideoEncoder (){
    VTCompressionSessionRef compressionSession;
    NSInteger frameCount;
    NSData *sps;
    NSData *pps;
}


@property (nonatomic,weak) id<DDVideoEncodingDelegate> h264Delegate;
@property (nonatomic) BOOL isBackGround;
@property (nonatomic) NSInteger currentVideoBitRate;
@property (assign, nonatomic) uint64_t lastTimestamp;

@end

@implementation DDHardwareVideoEncoder

#pragma mark -- LifeCycle
- (instancetype)initWithVideoStreamConfiguration:(DDLiveVideoConfiguration *)configuration{
    if(self = [super init]){
        _configuration = configuration;
        [self initCompressionSession];
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterBackground:) name:UIApplicationWillResignActiveNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationDidBecomeActiveNotification object:nil];
    }
    return self;
}

- (void)updateConfiguration {
    [self initCompressionSession];
    self->sps = NULL;
}

- (void)clearCompressionSession {
    if(compressionSession){
        VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
        
        VTCompressionSessionInvalidate(compressionSession);
        CFRelease(compressionSession);
        compressionSession = NULL;
    }
}

- (void)initCompressionSession{
    [self clearCompressionSession];

    [self configCompressionSession];
}

- (void)configCompressionSession {
    // VideoCompressonOutputCallback回调函数:视频图像编码成功后调用
    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;
    
    // 设置关键帧间隔
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval,(__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration,(__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
    
    // 设置期望帧率
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(_configuration.videoFrameRate));
    
    //设置码率,均值,单位是byte
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(_configuration.videoBitRate)); // bps
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@(_configuration.videoMaxBitRate), @1]); // Bps
    
    // 设置实时编码输出(避免延迟)
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    
//    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
//    status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CAVLC);
    VTCompressionSessionPrepareToEncodeFrames(compressionSession);
}

- (void)setVideoBitRate:(NSInteger)videoBitRate{
    if(_isBackGround) return;
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@(_configuration.videoMaxBitRate), @1]);
    _currentVideoBitRate = videoBitRate;
}

-(NSInteger)videoBitRate{
    return _currentVideoBitRate;
}

- (void)dealloc{
    if(compressionSession != NULL)
    {
        VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(compressionSession);
        CFRelease(compressionSession);
        compressionSession = NULL;
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark -- DDVideoEncoder
- (void)encodeVideoData:(CVImageBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
    if(_isBackGround) return;
    
    NSInteger timeCha = timeStamp - self.lastTimestamp;
    if (timeCha - 1000/(int32_t)_configuration.videoFrameRate < 0) {
        return;
    }
    self.lastTimestamp = timeStamp;
    
    frameCount ++;
    CMTime presentationTimeStamp = CMTimeMake(frameCount, 1000);
    VTEncodeInfoFlags flags;
    CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);
    
    NSDictionary *properties = nil;
    if(frameCount % (int32_t)_configuration.videoMaxKeyframeInterval == 0 || self.isInsertKeyFrame == YES){
        properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
    }
    NSNumber *timeNumber = @(timeStamp);
    
    // 编码,编码成功后调用回调函数
   OSStatus statusCode =  VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);

    if (frameCount > 262143) { // 该数值根据后台要求定义
        frameCount = 0;
    }

    self.isInsertKeyFrame = NO; 
    
    if (statusCode != noErr) {
        VTCompressionSessionInvalidate(compressionSession);
        compressionSession = NULL;
        return;
    }
}

- (void)stopEncoder{
    VTCompressionSessionCompleteFrames(compressionSession, kCMTimeIndefinite);
}

- (void)setDelegate:(id<DDVideoEncodingDelegate>)delegate{
    _h264Delegate = delegate;
}

#pragma mark -- NSNotification
- (void)willEnterBackground:(NSNotification*)notification{
    _isBackGround = YES;
}

- (void)willEnterForeground:(NSNotification*)notification{
    [self initCompressionSession];
    _isBackGround = NO;
}

#pragma mark -- VideoCallBack
// 视频编码成功后回调,将编码成功的CMSampleBuffer转换成H264码流
static void VideoCompressonOutputCallback(void *VTref, void *VTFrameRef, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    if(!sampleBuffer) return;
    CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    if(!array) return;
    CFDictionaryRef dic = (CFDictionaryRef)CFArrayGetValueAtIndex(array, 0);
    if(!dic) return;
    
    BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);
    uint64_t timeStamp = [((__bridge_transfer NSNumber*)VTFrameRef) longLongValue];
    
    DDHardwareVideoEncoder *videoEncoder = (__bridge DDHardwareVideoEncoder *)VTref;
    if(status != noErr){
        return;
    }
    
    if (keyframe && !videoEncoder->sps)
    {
        // 描述信息
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        if (statusCode == noErr)
        {
            size_t pparameterSetSize, pparameterSetCount;
            const uint8_t *pparameterSet;
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
            if (statusCode == noErr)
            {
                videoEncoder->sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                videoEncoder->pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
            }
        }
    }
    
    // 编码后的数据结构
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4;
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            // Read the NAL unit length
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);

            DDVideoFrame *videoFrame = [[DDVideoFrame alloc] init];
            videoFrame.timestamp = timeStamp;
            videoFrame.isKeyFrame = keyframe;
            videoFrame.frameCount = videoEncoder->frameCount;
            videoFrame.videoFrameRate = videoEncoder.configuration.videoFrameRate;
            videoFrame.videoWidth = videoEncoder.configuration.videoSize.width;
            videoFrame.videoHeight = videoEncoder.configuration.videoSize.height;
            NSData *h264Data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];

            // 以后数据处理,根据各自后台要求数据格式处理
            NSMutableData *mData = [NSMutableData data];
            const char bytes[] = "\x00\x00\x00\x01";
            size_t length = (sizeof bytes) - 1;
            NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
            if (keyframe) {
                [mData appendData:ByteHeader];
                [mData appendData:videoEncoder->sps];
                [mData appendData:ByteHeader];
                [mData appendData:videoEncoder->pps];
            }
                [mData appendData:ByteHeader];
                [mData appendData:h264Data];
                videoFrame.data = mData;
            if(videoEncoder.h264Delegate && [videoEncoder.h264Delegate respondsToSelector:@selector(videoEncoder:videoFrame:)]){
                [videoEncoder.h264Delegate videoEncoder:videoEncoder videoFrame:videoFrame]; // 数据传出去之后,实现该代理方法,根据后台数据格式进行数据封装,然后发送
            }
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

@end

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

推荐阅读更多精彩内容