ios 视频流H264硬编码---分解LFLiveKit

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

@interface LFHardwareVideoEncoder (){
    VTCompressionSessionRef compressionSession; // 编码器
    NSInteger frameCount; // 帧数(用于设置关键帧)
    NSData *sps;
    NSData *pps;
    FILE *fp;
    BOOL enabledWriteVideoFile;
}

@property (nonatomic, strong) LFLiveVideoConfiguration *configuration;
@property (nonatomic, weak) id<LFVideoEncodingDelegate> h264Delegate;
@property (nonatomic) NSInteger currentVideoBitRate;
@property (nonatomic) BOOL isBackGround;

@end

@implementation LFHardwareVideoEncoder

#pragma mark -- LifeCycle
- (instancetype)initWithVideoStreamConfiguration:(LFLiveVideoConfiguration *)configuration {
    if (self = [super init]) {
        NSLog(@"USE LFHardwareVideoEncoder");
        _configuration = configuration;
        [self resetCompressionSession];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterBackground:) name:UIApplicationWillResignActiveNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationDidBecomeActiveNotification object:nil];
#ifdef DEBUG
        enabledWriteVideoFile = NO;
        [self initForFilePath];
#endif

    }
    return self;
}

- (void)resetCompressionSession {

    if (compressionSession) {
        /*
         1.强制完成一些或全部未处理的视频帧数据
         2.编码器失效(类似于让Timer失效)
         3.释放内存(VTCompressionSession是一套C函数,需要开发者手动管理内存)
         4.编码器置空
         */
        VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(compressionSession);
        CFRelease(compressionSession);
        compressionSession = NULL;
    }

    /*
     创建编码器
     1.分配器,NULL标识使用默认的
     2.视频帧的像素宽
     3.视频帧的像素高
     4.编码类型
     5.如果使用指定的视频编码器就传值,NULL表示VideoToolBox自己创建一个
     6.源缓存,NULL表示不使用VideoToolBox创建的,而使用费VTB分配的内存,这样可以拷贝图片数据
     7.压缩数据分配器,NULL表示使用默认的
     8.回调函数(编码后的视频帧数据)
     9.回调方法所在的对象类实例,也会传递给回调函数(回调函数会被VTCompressionSessionEncodeFrame函数唤醒并且异步执行)
     10.指向编码器的内存地址
     */
    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; // 值越大效果越好,帧数据越大
    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);
    VTSessionSetProperty(compressionSession,
                         kVTCompressionPropertyKey_AllowFrameReordering,
                         kCFBooleanTrue);
    VTSessionSetProperty(compressionSession,
                         kVTCompressionPropertyKey_H264EntropyMode,
                         kVTH264EntropyMode_CABAC);
    VTCompressionSessionPrepareToEncodeFrames(compressionSession);

}

- (void)setVideoBitRate:(NSInteger)videoBitRate {
    if(_isBackGround) return;
    VTSessionSetProperty(compressionSession,
                         kVTCompressionPropertyKey_AverageBitRate,
                         (__bridge CFTypeRef)@(videoBitRate));
    NSArray *limit = @[@(videoBitRate * 1.5/8), @(1)];
    VTSessionSetProperty(compressionSession,
                         kVTCompressionPropertyKey_DataRateLimits,
                         (__bridge CFArrayRef)limit);
    _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 -- LFVideoEncoder
- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
    if(_isBackGround) return;
    frameCount++;

    /*
     CMTime CMTimeMake (
                        int64_t value,    //表示 当前视频播放到的第几桢数
                        int32_t timescale //每秒的帧数
     );
     */
    CMTime presentationTimeStamp = CMTimeMake(frameCount, (int32_t)_configuration.videoFrameRate);
    VTEncodeInfoFlags flags;

    CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);

    // 设置当前帧为 关键帧.关键帧间隔在config文件中定义了,是帧率24(fps)*2,即 frameCount % (24*2) = 0就设置为关键帧
    NSDictionary *properties = nil;
    if (frameCount % (int32_t)_configuration.videoMaxKeyframeInterval == 0) {
        properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
    }
    NSNumber *timeNumber = @(timeStamp);

    /* 开启帧编码
     1.编码器
     2.一个将要被压缩的视频帧,不能为空
     3.展示当前帧的时间戳
     4.播放24帧需要多长时间,默认kCMTimeInvalid
     5.当前帧的属性(主要判断是否为关键帧)
     6.当前帧关联的值(时间戳),会传递给回调函数
     7.编码操作的信息
     */
    OSStatus status = VTCompressionSessionEncodeFrame(compressionSession,
                                                      pixelBuffer,
                                                      presentationTimeStamp,
                                                      duration,
                                                      (__bridge CFDictionaryRef)properties,
                                                      (__bridge_retained void *)timeNumber,
                                                      &flags);
    if(status != noErr){
        [self resetCompressionSession];
    }
}

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

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

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

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

#pragma mark -- VideoCallBack
/*
 1.可以引用到帧编码函数中的参数
 2.原始帧数据的引用值(未编码之前)
 3.编码是否成功
 4.编码操作的信息
 5.编码后的帧(如果编码成功并且没有丢帧,反之参数为NULL)
 */
static void VideoCompressonOutputCallback(void *VTref,
                                          void *VTFrameRef,
                                          OSStatus status,
                                          VTEncodeInfoFlags infoFlags,
                                          CMSampleBufferRef sampleBuffer){
    // 编码失败 或 丢帧
    if (!sampleBuffer) return;

    /*
     返回一个不可变数组(元素是dictionary)
     1.访问的对象
     2.如果样本为空是否创建一个空数组
     */
    CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    if (!array) return;

    // 获取数组中第一个dictionary数据
    CFDictionaryRef dic = (CFDictionaryRef)CFArrayGetValueAtIndex(array, 0);
    if (!dic) return;

    // 是否是关键帧
    BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);

    // 获取帧编码函数中的时间戳参数
    uint64_t timeStamp = [((__bridge_transfer NSNumber *)VTFrameRef) longLongValue];

    // 获取回调函数所在类的实例对象
    LFHardwareVideoEncoder *videoEncoder = (__bridge LFHardwareVideoEncoder *)VTref;
    if (status != noErr) {
        return;
    }

    // 关键帧 且 尚未设置sps
    if (keyframe && !videoEncoder->sps) {
        // 获取样本缓存中的一个样本的格式描述(获取帧描述信息)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);

        // sps和pps一般保存在视频文件头中。sps在前pps在后

        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        /*
         获取sps信息(序列参数集) H.264码流中第一个NALU单元,保存了一组编码视频序列的全局参数.
         1. 格式信息
         2. sps信息在format中的索引
         3. 指向参数集的指针,如果不需要这些信息传NULL
         4. 指向参数字节多少的指针,
         5. 指向参数数量的指针,
         6.
         */
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
                                                                                 0,
                                                                                 &sparameterSet,
                                                                                 &sparameterSetSize,
                                                                                 &sparameterSetCount,
                                                                                 0);
        if (statusCode == noErr) {
            size_t pparameterSetSize, pparameterSetCount;
            const uint8_t *pparameterSet;
            // 获取pps信息(图像参数集)H.264码流中第二个NALU单元,
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
                                                                                     1,
                                                                                     &pparameterSet,
                                                                                     &pparameterSetSize,
                                                                                     &pparameterSetCount,
                                                                                     0);
            // 写入本地
            if (statusCode == noErr) {
                // 设置sps、pps
                videoEncoder->sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                videoEncoder->pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];

                /*
                 处理数据时,sps、pps放在H264视频流的最前端
                 编码的视频写入本地时,需要添加4个字节的头信息并且卸载H264文件最前面
                 如果推流,sps、pps、头信息放入flv数据区即可。
                 */
                if (videoEncoder->enabledWriteVideoFile) {
                    NSMutableData *data = [[NSMutableData alloc] init];
                    uint8_t header[] = {0x00, 0x00, 0x00, 0x01};
                    [data appendBytes:header length:4];
                    [data appendData:videoEncoder->sps];
                    [data appendBytes:header length:4];
                    [data appendData:videoEncoder->pps];
                    fwrite(data.bytes, 1, data.length, videoEncoder->fp);
                }
            }
        }
    }

    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;

    /*
     获取IDR信息
     1.媒体信息数据
     2.IDR信息在媒体信息数据中的索引
     3.IDR信息数据长度
     4.媒体信息数据长度
     5.媒体信息数据的字节数(前4个字节是数据长度)
     */
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer,
                                                         0,
                                                         &length,
                                                         &totalLength,
                                                         &dataPointer);

    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4;

        // 循环获取NALU数据,真正用来播放的视频帧数据
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            // Read the NAL unit length
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);

            // 大小端转换
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);

            LFVideoFrame *videoFrame = [LFVideoFrame new];
            videoFrame.timestamp = timeStamp;
            videoFrame.data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            videoFrame.isKeyFrame = keyframe;
            videoFrame.sps = videoEncoder->sps;
            videoFrame.pps = videoEncoder->pps;

            if (videoEncoder.h264Delegate && [videoEncoder.h264Delegate respondsToSelector:@selector(videoEncoder:videoFrame:)]) {
                [videoEncoder.h264Delegate videoEncoder:videoEncoder videoFrame:videoFrame];
            }

            if (videoEncoder->enabledWriteVideoFile) {
                NSMutableData *data = [[NSMutableData alloc] init];
                if (keyframe) {
                    uint8_t header[] = {0x00, 0x00, 0x00, 0x01};
                    [data appendBytes:header length:4];
                } else {
                    uint8_t header[] = {0x00, 0x00, 0x01};
                    [data appendBytes:header length:3];
                }
                [data appendData:videoFrame.data];

                fwrite(data.bytes, 1, data.length, videoEncoder->fp);
            }

            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)initForFilePath {
    NSString *path = [self GetFilePathByfileName:@"IOSCamDemo.h264"];
    NSLog(@"%@", path);
    self->fp = fopen([path cStringUsingEncoding:NSUTF8StringEncoding], "wb");
}

- (NSString *)GetFilePathByfileName:(NSString*)filename {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *writablePath = [documentsDirectory stringByAppendingPathComponent:filename];
    return writablePath;
}

原文地址:https://www.cnblogs.com/madaha/p/9816663.html

时间: 2024-11-03 22:08:20

ios 视频流H264硬编码---分解LFLiveKit的相关文章

视频直播技术(二):实时视频编码之H264硬编码

1.硬编码 & 软编码 硬编码:通过Android系统自带的Camera录制视频,实际上调用的是底层的高清编码硬件模块,即显卡,不使用CPU,速度快 软编码:使用CPU进行编码,如常见C/C++代码,编译生成二进制文件,速度相对较慢.例如使用Android NDK编译H264生成so库,编写jni接口,再使用java调用so库. 2.硬编码过程和原理 过程:通过MediaRecoder采集视频,再将视频流映射到LocalSocket实现收发. 原理:详见[流媒體]H264-MP4格式及在MP4文

iOS-VideoToolbox硬编码H264

前言 VideoToolBox是iOS8之后,苹果开发的用于硬解码编码H264/H265(iOS11以后支持)的API. 对于H264还不了解的童鞋一定要先看下这边的H264的简介. 编码流程 我们实现一个简单的Demo,从摄像头获取到视频数据,然后再编码成H264裸数据保存在沙盒中. 1. 创建初始化VideoToolBox 核心代码如下 - (void)initVideoToolBox { dispatch_sync(encodeQueue , ^{ frameNO = 0; int wid

简单高效易用Windows/Linux/ARM/Android/iOS平台实现RTMP推送组件EasyRTMP-Android MediaCodec硬编码流程介绍

音视频流媒体硬解码是指不使用CPU进行编码,使用显卡GPU,专用的DSP.FPGA.ASIC芯片等硬件进行编码.编码框架Video ToolBox和AudioToolbox. EasyRTMP是结合了多种音视频缓存及网络技术的一个rtmp直播推流端,包括:圆形缓冲区(circular buffer).智能丢帧.自动重连.rtmp协议等等多种技术,能够非常有效地适应各种平台(Windows.Linux.ARM.Android.iOS),各种网络环境(有线.wifi.4G),以及各种情况下的直播恢复

ios - iPhone开发重构:从硬编码到模型到规律

无论在iPhone开发还是学习的过程中都会看到一些不是很理想的代码,不可 否认自己也在不断“贡献”着这类代码.面对一些代码的“坏味道”,重构显然 是个有效的解决途径.<iPhone开发重构>系列就想总结和补充iPhone开发中经 历的一些重构,其间可能会引用一些开源以及实际项目的代码,本着对技术的探 求,冒昧之处还请作者多多见谅. 在iPhone开发的过程中经常会遇到根据不同的Table行或者标识符推入不同 的Controller的需要,一个最直接的实现就是硬编码,通过if…else if…e

iOS视频流开发(2) — 视频播放

iOS视频流开发(2) — 视频播放  承上篇,本篇文章主要介绍iOS视频播放需要用到的类.以及他们的使用场景和开发中遇到的问题. MPMoviePlayerViewController MP简介 iOS提供MPMoviePlayerController类进行播放,支持流媒体和文件播放.MPMoviePlayerController足够强大,几乎不用写几行代码就能完成一个播放器.视频内容会渲染到他的View上,这个View可以移动.缩放,放在任何用户想放的地方,而在缩放移动的过程中视频都可以正常

【流媒体】 Android 实时视频编码—H.264硬编码

[流媒體] Android 实时视频编码—H.264硬编码 SkySeraph Apr 4th 2012 Email:[email protected].com 1  硬编码 & 软编码 硬编码:通过调用Android系统自带的Camera录制视频,实际上是调用了底层的高清编码硬件模块,也即显卡,不使用CPU,速度快 软编码:使用CPU进行编码,如常见C/C++代码,一般编译生成的二进制都是的,速度相对较慢.例如使用Android NDK编译H264生成so库,编写jni接口,再使用java调用

android流媒体之硬编码【代码篇】

转载此处:http://www.apkbus.com/blog-86476-43829.html 上一篇文章进行了思路和16进制文件的分析.这篇该代码实现了.目前没有在真实手机上测试, android4.0之后的模拟器可以用模拟摄像头或者叫做webcam的[其实就是笔记本摄像头].之后会在程序安装包data/data/edu.ustb.videoencoder/下面会有h264.3gp,sps[存放sps数据].pps[存放pps数据].media.xml[存放找到mdat的位置],/sdcar

使用VideoToolbox硬编码H.264&lt;转&gt;

文/落影loyinglin(简书作者)原文链接:http://www.jianshu.com/p/37784e363b8a著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. =========================================== 使用VideoToolbox硬编码H.264 前言 H.264是目前很流行的编码层视频压缩格式,目前项目中的协议层有rtmp与http,但是视频的编码层都是使用的H.264.在熟悉H.264的过程中,为更好的了解H.264,尝

Nvidia NVENC 硬编码预研总结

本篇博客记录NVENC硬编码的预研过程 步骤如下: (1)环境搭建 (2)demo编译,测试,ARGB编码 (3)研究demo源码,阅读API文档 (4)封装so共享库,联调测试多路编码性能 (5)研究内存,显存拷贝方案,尝试解决CPU,GPU消耗过高等性能问题 1. 环境搭建 (1)编译环境,预研中这个环境不是我亲手搭建的,需要CUDAToolKit, NVENC SDK (2)运行环境,需要Nvidia独立显卡,另外还要注意NVENC SDK的版本对显卡驱动版本有要求,具体在SDK的文档中会