美文网首页
iOS实时硬编码H.264

iOS实时硬编码H.264

作者: Goning | 来源:发表于2018-05-18 11:52 被阅读199次

本文介绍iOS下使用VTCompressionSessionRef将AVCaptureSession实时采集到的视频数据进行硬编码为H.264数据

AVCaptureSession采集视频:iOS实时采集视频

EncodeH264.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h>

@interface EncodeH264 : NSObject
- (BOOL)createEncodeSession:(int)width height:(int)height fps:(int)fps bite:(int)bt;
- (void)encodeSmapleBuffer:(CMSampleBufferRef)sampleBuffer;
@end
EncodeH264.m
#import "EncodeH264.h"

@interface EncodeH264() {
    dispatch_queue_t encodeQueue;
    long timeStamp;
    VTCompressionSessionRef encodeSession;//压缩会话
}
@property (nonatomic,assign)BOOL isObtainspspps;//是否获取到pps、sps
@end

/**
 编码回调
 @param userData 回调参考值
 @param sourceFrameRefCon 帧参考值
 @param status noErr代表压缩成功,不成功为错误代码。
 @param infoFlags 编码操作信息
 @param sampleBuffer 包含压缩帧,如果压缩成功且帧未被丢弃,否则NULL。
 */
void outputCallback(void *userData,
                          void *sourceFrameRefCon,
                          OSStatus status,
                          VTEncodeInfoFlags infoFlags,
                          CMSampleBufferRef sampleBuffer)
{
    if (status!=noErr) {
        NSLog(@"压缩失败,status=%d,infoFlags=%d",(int)status,(int)infoFlags);
        return;
    }
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"sampleBuffer未准备好");
        return;
    }
    EncodeH264 *h264 = (__bridge EncodeH264*)userData;
    //判断当前帧是否为关键帧(i帧)
    bool keyframe = !CFDictionaryContainsKey((CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);//同步
    //获取sps、pps数据,sps、pps只需获取一次,保存在h264开头
    if (keyframe && !h264.isObtainspspps) {
        size_t spsSize, spsCount;
        size_t ppsSize, ppsCount;
        const uint8_t *spsData,*ppsData;
        CMFormatDescriptionRef description = CMSampleBufferGetFormatDescription(sampleBuffer);
        OSStatus err0 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, 0, &spsData, &spsSize, &spsCount, 0);
        OSStatus err1 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, 1, &ppsData, &ppsSize, &ppsCount, 0);
        if (err0==noErr&&err1==noErr) {
            h264.isObtainspspps = YES;
            NSLog(@"获取到sps、pps数据,Length:sps=%zu,pps=%zu",spsSize,ppsSize);
        }
    }
    //获取dataBuffer
    size_t lengthAtOffset,totalLength;
    char *data;
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    OSStatus error = CMBlockBufferGetDataPointer(dataBuffer, 0, &lengthAtOffset, &totalLength, &data);
    if (error==noErr) {
        size_t offset = 0;
        //返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        const int lengthInfoSize = 4;
        //循环获取nalu数据
        while (offset < totalLength-lengthInfoSize) {
            uint32_t naluLength = 0;
            //获取nalu的长度
            memcpy(&naluLength, data+offset, lengthInfoSize);
            //大端模式转化为系统端模式
            naluLength = CFSwapInt32BigToHost(naluLength);
            NSLog(@"获取到nulu数据,length=%d,totalLength=%zu",naluLength,totalLength);
            //读取下一个nalu,一次回调可能包含多个nalu
            offset += lengthInfoSize+naluLength;
        }
    }
}

@implementation EncodeH264

- (instancetype)init {
    if ([super init]) {
        encodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        timeStamp = 0;
    }
    return self;
}

/**
 创建视频编码session
 @param width 宽度(以像素为单位)
 @param height 高度
 @param fps 帧率
 @param bite 比特率
 @return 是否创建成功
 */
- (BOOL)createEncodeSession:(int)width height:(int)height fps:(int)fps bite:(int)bite {
    OSStatus status;
    //设置回调
    VTCompressionOutputCallback callback = outputCallback;
    //创建session
    status = VTCompressionSessionCreate(kCFAllocatorDefault, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, callback, (__bridge void *)(self), &encodeSession);
    if (status != noErr) {
        NSLog(@"创建session失败");
        return NO;
    }
    /*设置属性:VTSessionSetProperty()*/
    //提示视频编码器,压缩是否实时执行
    status = VTSessionSetProperty(encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    NSLog(@"set realtime  return: %d",(int)status);
    //指定编码比特流的配置文件和级别:直播一般使用baseline,可减少由于b帧带来的延时
    status = VTSessionSetProperty(encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    NSLog(@"set profile   return: %d",(int)status);
    //设置比特率上限:bps
    status  = VTSessionSetProperty(encodeSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(bite));
    NSLog(@"set bitrate   return: %d",(int)status);
    //设置比特率均值:byte
    NSArray *limit = @[@(bite*2/8),@(1)];
    status = VTSessionSetProperty(encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    NSLog(@"set limit     return: %d",(int)status);
    // 设置关键帧速率(i帧间隔)
    status = VTSessionSetProperty(encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(fps*2));
    NSLog(@"set KeyFrame  return: %d",(int)status);
    //设置期望帧率
    status = VTSessionSetProperty(encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(fps));
    NSLog(@"set framerate return: %d",(int)status);
    //开始编码
    status = VTCompressionSessionPrepareToEncodeFrames(encodeSession);
    NSLog(@"start encode  return: %d",(int)status);
    return YES;
}

/**
 编码AVCaptureSession采集的sampleBuffer
 */
- (void)encodeSmapleBuffer:(CMSampleBufferRef)sampleBuffer {
    dispatch_sync(encodeQueue, ^{
        //从sampleBuffer中获取imageBuffer
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        //时间戳
        self->timeStamp ++;
        CMTime pts = CMTimeMake(self->timeStamp, 1000);
        //duration
        CMTime duration = kCMTimeInvalid;
        VTEncodeInfoFlags flags;
        //硬编码
        OSStatus status = VTCompressionSessionEncodeFrame(self->encodeSession,
                                                          imageBuffer,
                                                          pts,
                                                          duration,
                                                          NULL,
                                                          NULL,
                                                          &flags);
        if (status!=noErr) {
            NSLog(@"编码失败,status=%d",(int)status);
            [self stopEncodeSession];
            return;
        }
    });
}

/**
 编码中断
 */
- (void)stopEncodeSession {
    VTCompressionSessionCompleteFrames(encodeSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(encodeSession);
    CFRelease(encodeSession);
    encodeSession = NULL;
}

@end
ViewController.m
#import "ViewController.h"
#import "CaptureSession.h"
#import "EncodeH264.h"

@interface ViewController ()<CaptureSessionDelegate>
@property (nonatomic,strong) CaptureSession *captureSession;
@property (nonatomic,strong) EncodeH264 *h264;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建编码对象
    _h264 = [[EncodeH264 alloc] init];
    //创建视频编码会话
    [_h264 createEncodeSession:480 height:640 fps:25 bite:640*1000];
    //创建视频采集会话
    _captureSession = [[CaptureSession alloc] initWithCaptureSessionPreset:CaptureSessionPreset640x480];
    //设置代理
    _captureSession.delegate = self;
    //创建预览层
    AVCaptureVideoPreviewLayer *preViewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession.session];
    //设置frame
    preViewLayer.frame = CGRectMake(0.f, 0.f, self.view.bounds.size.width, self.view.bounds.size.height);
    //设置方向
    preViewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;//填充且不拉伸
    [self.view.layer addSublayer:preViewLayer];
    [self.view bringSubviewToFront:self.btn];
}

- (IBAction)start:(UIButton *)sender {
    sender.selected = !sender.selected;
    if (sender.selected) {
        [_captureSession start];
    }else {
        [_captureSession stop];
    }
}

- (void)videoWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
//    NSLog(@"实时采集回调得到sampleBuffer");
    [_h264 encodeSmapleBuffer:sampleBuffer];
}

@end

可将编码后的sps/pps及每个nalu数据保存在本地文件中,使用vlc播放验证。

相关文章

网友评论

      本文标题:iOS实时硬编码H.264

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