前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS摄像头采集和编码

iOS摄像头采集和编码

作者头像
gongluck
发布2022-05-09 14:59:44
8640
发布2022-05-09 14:59:44
举报
文章被收录于专栏:C++C++

设计思路

使用AVCaptureSession创建采集会话,获取图像数据后通过VideoToolBox进行编码。

采集参数设置

AVCaptureSession需要AVCaptureDeviceInput作为输入和AVCaptureVideoDataOutput接收输出数据(就是采集图像数据)。 参数设置之间需要分别调用beginConfigurationcommitConfiguration方法。

采集参数设置

代码语言:javascript
复制
//采集参数设置
-(int)doCapturePrepare{
    NSError* error;
    //获取摄像头设备对象
    AVCaptureDevice * device;
    NSArray<AVCaptureDevice *> *devices;
    AVCaptureDevicePosition position = _facing ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
    if (@available(iOS 10.0, *)) {
        AVCaptureDeviceDiscoverySession *deviceDiscoverySession =  [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:position];
        devices = deviceDiscoverySession.devices;
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
#pragma clang diagnostic pop
    }
    for(AVCaptureDevice * dev in devices)
    {
        NSLog(@"device : %@", dev);
        if([dev position] == position)
        {
            device = dev;
            break;
        }
    }
    //设置摄像头帧率,作用不大
    CMTime frameDuration = CMTimeMake(1, 30);
    for (AVFrameRateRange *range in [device.activeFormat videoSupportedFrameRateRanges]) {
        NSLog(@"support framerate:%@", range);
        if (CMTIME_COMPARE_INLINE(frameDuration, >=, range.minFrameDuration) &&
            CMTIME_COMPARE_INLINE(frameDuration, <=, range.maxFrameDuration)) {
            if ([device lockForConfiguration:&error]) {
                [device setActiveVideoMaxFrameDuration:range.minFrameDuration];
                [device setActiveVideoMinFrameDuration:range.maxFrameDuration];
                [device unlockForConfiguration];
                NSLog(@"select framerate:%@", range);
            }
        }
    }
    //创建输入
    _input = [[AVCaptureDeviceInput alloc] initWithDevice: device error:&error];
    if (error) {
        NSLog(@"create input failed,%@",error);
        return -1;
    }else{
        NSLog(@"create input succeed.");
    }
    //创建输出队列
    //DISPATCH_QUEUE_SERIAL串行队列
    //_dataCallbackQueue = dispatch_queue_create("dataCallbackQueue", DISPATCH_QUEUE_SERIAL);
    //创建数据获取线程
    _dataCallbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建输出
    _output = [[AVCaptureVideoDataOutput alloc] init];
    //绑定输出队列和代理到输出对象
    [_output setSampleBufferDelegate:self queue:_dataCallbackQueue];
    //抛弃过期帧,保证实时性
    [_output setAlwaysDiscardsLateVideoFrames:YES];
    //获取输出对象所支持的像素格式
    NSArray *supportedPixelFormats = _output.availableVideoCVPixelFormatTypes;
    for (NSNumber *currentPixelFormat in supportedPixelFormats)  {
        NSLog(@"support format : %@", currentPixelFormat);
    }
    //设置输出格式
    [_output setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
    //创建采集功能会话对象
    _captureSession = [[AVCaptureSession alloc] init];
    // 改变会话的配置前一定要先开启配置,配置完成后提交配置改变
    [_captureSession beginConfiguration];
    //设置采集参数
    if([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480])
    {
        [_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
    }
    //绑定input和output到session
    NSLog(@"input : %@", _input);
    if([_captureSession canAddInput:_input])
    {
        [_captureSession addInput:_input];
    }
    NSLog(@"output : %@", _output);
    if([_captureSession canAddOutput:_output])
    {
        [_captureSession addOutput: _output];
    }
    //显示输出画面
    AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_captureSession];
    previewLayer.frame = CGRectMake(0, 50, self.view.frame.size.width, self.view.frame.size.height - 50);
    [self.view.layer  addSublayer:previewLayer];
    //提交配置变更
    [_captureSession commitConfiguration];
    return 0;
}

- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
      fromConnection:(AVCaptureConnection *)connection
{
    int64_t cur = CFAbsoluteTimeGetCurrent() * 1000;//ms
    if(_lastime == -1)
    {
        _lastime = cur;
    }
    int64_t went = cur - _lastime;
    int64_t duration = 1000/_fps;
    //NSLog(@"duration:%ld", duration);
    //NSLog(@"last:%ld,cur:%ld,went:%ld", last, cur, went);
    if(went < duration)
    {
        NSLog(@"drop");
        return;
    }else{
        _lastime = cur - went % duration;
    }
    //NSLog(@"captureOutput:%@,%@,%@", output, sampleBuffer, connection);
    CMVideoFormatDescriptionRef description = CMSampleBufferGetFormatDescription(sampleBuffer);
    //NSLog(@"captureOutput:%@", description);
    if(CMFormatDescriptionGetMediaType(description) != kCMMediaType_Video)
    {
        return;
    }
    //FourCharCode codectype = CMVideoFormatDescriptionGetCodecType(description);
    //NSString *scodectype = FOURCC2STR(codectype);
    //NSLog(@"codec type:%@", scodectype);
    
    //CVPixelBufferRef是CVImageBufferRef的别名,两者操作几乎一致。
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    imageBuffer = RotatePixelBuffer(imageBuffer, kCGImagePropertyOrientationRight);
    //需先用CVPixelBufferLockBaseAddress()锁定地址才能从主存访问,否则调用CVPixelBufferGetBaseAddressOfPlane等函数则返回NULL或无效值。
    CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
    //NSLog(@"imageBuffer:%@", imageBuffer);
    if(CVPixelBufferIsPlanar(imageBuffer))
    {
        size_t planars = CVPixelBufferGetPlaneCount(imageBuffer);
        if(planars == 2)
        {
            size_t stride = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
            size_t width = CVPixelBufferGetWidthOfPlane(imageBuffer, 0);
            size_t height = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);
            //NSLog(@"buffer stride : %ld, w : %ld, h : %ld", stride, width, height);
            void* Y = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
            void* UV = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
            if(Y != nil && UV != nil && _hyuv != NULL)
            {
                //NSLog(@"frame size %lu",stride * height*3/2);
                fwrite(Y, 1, stride * height, _hyuv);
                fwrite(UV, 1, stride * height / 2, _hyuv);
            }
        }
    }else{
        void* YUV = CVPixelBufferGetBaseAddress(imageBuffer);
        size_t size = CVPixelBufferGetDataSize(imageBuffer);
        if(YUV != nil && _hyuv != NULL)
        {
            //NSLog(@"frame size %lu",size);
            fwrite(YUV, 1, size, _hyuv);
        }
    }
    fflush(_hyuv);

    //编码264
    if(_firstime == -1)
    {
        _firstime = cur;
    }
    //创建CMTime的pts和duration
    CMTime pts = CMTimeMake(cur - _firstime, 1000);//ms
    CMTime dur = CMTimeMake(1, _fps);//ms
    VTEncodeInfoFlags flags;
    //NSLog(@"input pts : %lf", CMTimeGetSeconds(pts));
    //开始编码该帧数据
    OSStatus statusCode = VTCompressionSessionEncodeFrame(
                                                          _compressionSession,
                                                          imageBuffer,
                                                          pts,
                                                          dur,
                                                          NULL,
                                                          NULL,
                                                          &flags
                                                          );
    if (statusCode != noErr) {
        NSLog(@"VTCompressionSessionEncodeFrame failed %d", statusCode);
        [self doEncodeDestroy];
    }

    //unlock
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    CVPixelBufferRelease(imageBuffer);
}

开始/停止采集

isRunningstartRunningstopRunning简单明了的接口。

开始/停止采集

代码语言:javascript
复制
//开始采集
-(int)doStartCapture{
    if(_captureSession != NULL && ![_captureSession isRunning])
    {
        [_captureSession startRunning];
        if([_captureSession isRunning])
        {
            NSLog(@"start capture succeed.");
            return 0;
        }
        else
        {
            return -1;
        }
    }
    return 0;
}
//停止采集
-(int)doStopCapture{
    if(_captureSession != NULL && [_captureSession isRunning])
    {
        [_captureSession stopRunning];
        if(![_captureSession isRunning])
        {
            _captureSession = NULL;
            NSLog(@"stop capture succeed.");
            return 0;
        }
        else
        {
            return -1;
        }
    }
    return 0;
}

编码参数设置和销毁

调用VTSessionSetProperty设置需要的编码参数后,调用VTCompressionSessionPrepareToEncodeFrames准备进行编码。 使用VTCompressionSessionEncodeFrame推送数据到编码器。

编码参数设置和销毁

代码语言:javascript
复制
//编码参数设置
-(int)doEncodePrepare{
    if([self doEncodeDestroy]!=0)
    {
        NSLog(@"doEncodeDestroy failed.");
        return -1;
    }
    //创建CompressionSession对象,该对象用于对画面进行编码
    OSStatus status = VTCompressionSessionCreate(NULL,      // 会话的分配器。传递NULL以使用默认分配器。
                                                _width,    // 帧的宽度,以像素为单位。
                                                _height,   // 帧的高度,以像素为单位。
                                                kCMVideoCodecType_H264,   // 编解码器的类型,表示使用h.264进行编码
                                                NULL,      // 指定必须使用的特定视频编码器。传递NULL让视频工具箱选择编码器。
                                                NULL,      // 源像素缓冲区所需的属性,用于创建像素缓冲池。如果不希望视频工具箱为您创建一个,请传递NULL
                                                NULL,      // 编码数据的分配器。传递NULL以使用默认分配器。
                                                VTCompressionOutputCallbackH264,   // 当一次编码结束会在该函数进行回调,可以在该函数中将数据,写入文件中
                                                (__bridge  void*)self,  // outputCallbackRefCon
                                                &_compressionSession);    // 指向一个变量以接收的编码会话。
    if (status != noErr){
        NSLog(@"VTCompressionSessionCreate failed : %d", status);
        return -1;
    }
    //设置实时编码输出(直播必然是实时输出,否则会有延迟)
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_RealTime failed : %d", status);
        return -1;
    }
    //设置profile
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_ProfileLevel failed : %d", status);
        return -1;
    }
    //关闭重排,可以关闭B帧。
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_AllowFrameReordering failed : %d", status);
        return -1;
    }
    //设置gop
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)(@(_fps*10)));
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_MaxKeyFrameInterval failed : %d", status);
        return -1;
    }
    //设置帧率
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)(@(_fps)));
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_ExpectedFrameRate failed : %d", status);
        return -1;
    }
    //设置码率kVTCompressionPropertyKey_AverageBitRate/kVTCompressionPropertyKey_DataRateLimits
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)(@(_bitrate)));
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_AverageBitRate failed : %d", status);
        return -1;
    }
    status = VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@1.0]);
    if (status != noErr){
        NSLog(@"kVTCompressionPropertyKey_DataRateLimits failed : %d", status);
        return -1;
    }
    //基本设置结束, 准备进行编码
    status = VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
    if (status != noErr){
        NSLog(@"VTCompressionSessionPrepareToEncodeFrames failed : %d", status);
        return -1;
    }
    return 0;
}
//销毁编码设置
-(int)doEncodeDestroy{
    if(_compressionSession != NULL)
    {
        VTCompressionSessionInvalidate(_compressionSession);
        CFRelease(_compressionSession);
        _compressionSession = NULL;
    }
    return 0;
}

void VTCompressionOutputCallbackH264(void * CM_NULLABLE outputCallbackRefCon,       //自定义回调参数
                                    void * CM_NULLABLE sourceFrameRefCon,
                                    OSStatus status,
                                    VTEncodeInfoFlags infoFlags,
                                    CM_NULLABLE CMSampleBufferRef sampleBuffer)
{
    if (status != noErr) {
        NSLog(@"encode error : %d", status);
        return;
    }
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"sampleBuffer data is not ready ");
        return;
    }
    ViewController* _self = (__bridge ViewController*)outputCallbackRefCon;
    //判断是否是关键帧
    bool isKeyframe = !CFDictionaryContainsKey((CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    if (isKeyframe)
    {
        // 获取编码后的信息(存储于CMFormatDescriptionRef中)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        // 获取SPS信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
        // 获取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
        //NSLog(@"sps size : %d", sparameterSetSize);
        //NSLog(@"pps size : %d", pparameterSetSize);
        if(_self.h264 != NULL)
        {
            //NSLog(@"write file");
            char naluhead[4] = {0x00, 0x00, 0x00, 0x01};
            fwrite(naluhead, 1, 4, _self.h264);
            fwrite(sparameterSet, 1, sparameterSetSize, _self.h264);
            fwrite(naluhead, 1, 4, _self.h264);
            fwrite(pparameterSet, 1, pparameterSetSize, _self.h264);
            fflush(_self.h264);
        }
    }
    //Float64 pts = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer));
    //NSLog(@"output pts : %lf", pts);
    // 获取数据块
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, total;
    char *data;
    OSStatus ret = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &total, &data);
    if (ret == noErr) {
        size_t offset = 0;
        static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        // 循环获取nalu数据
        while (offset < total - AVCCHeaderLength) {
            uint32_t nalulen = 0;
            // Read the NAL unit length
            memcpy(&nalulen, data + offset, AVCCHeaderLength);
            // 从大端转系统端
            nalulen = CFSwapInt32BigToHost(nalulen);
            //NSLog(@"nalu size : %d", nalulen);
            if(_self.h264 != NULL)
            {
                char naluhead[4] = {0x00, 0x00, 0x00, 0x01};
                fwrite(naluhead, 1, 4, _self.h264);
                fwrite(data + offset + AVCCHeaderLength, 1, nalulen, _self.h264);
                fflush(_self.h264);
            }
            // 移动到写一个块,转成NALU单元
            // Move to the next NAL unit in the block buffer
            offset += AVCCHeaderLength + nalulen;
        }
    }
}

图像处理

有时候可能要对采集数据做旋转镜像等操作,可以参考以下方法。

图像处理

代码语言:javascript
复制
//旋转和镜像操作
static CVPixelBufferRef RotatePixelBuffer(CVPixelBufferRef pixelBuffer, CGImagePropertyOrientation orientation) {
    CIImage *image = [CIImage imageWithCVImageBuffer:pixelBuffer];
    image = [image imageByApplyingTransform : CGAffineTransformMakeTranslation(-image.extent.origin.x, -image.extent.origin.y)];
    image = [image imageByApplyingOrientation : orientation];
    CVPixelBufferRef output = NULL;
    CVReturn ret = CVPixelBufferCreate(nil,
                                      CGRectGetWidth(image.extent),
                                      CGRectGetHeight(image.extent),
                                      CVPixelBufferGetPixelFormatType(pixelBuffer),
                                      nil,
                                      &output);
    if (ret != kCVReturnSuccess) {
        NSLog(@"CVPixelBufferCreate failed : %d", ret);
    }
    else{
        // 复用 CIContext
        static CIContext *context = nil;
        if(context == nil)
        {
            //方式0
            //context = [[CIContext alloc] init];
            //方式1
            context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:kCIContextUseSoftwareRenderer]];
            //方式2
            //EAGLContext* eaglctx = [[EAGLContext alloc] initWithAPI : kEAGLRenderingAPIOpenGLES3];
            //context = [CIContext contextWithEAGLContext : eaglctx];
        }
        [context render : image toCVPixelBuffer : output];//ios9.3
    }
    return output;
}

完整例子代码

https://github.com/gongluck/AnalysisAVP/tree/master/example/ios/iosCamera

参考

https://www.cnblogs.com/lijinfu-software/articles/11451340.html https://www.jianshu.com/p/e75d7b573ae5?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation https://www.jianshu.com/p/a0e2d7b3b8a7 https://www.jianshu.com/p/f5f3f94f36c5 https://dikeyking.github.io/2020/01/02/CVPixelBuffer%E8%A3%81%E5%89%AA%E6%97%8B%E8%BD%AC%E7%BC%A9%E6%94% 【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-12-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设计思路
  • 采集参数设置
  • 开始/停止采集
  • 编码参数设置和销毁
  • 图像处理
  • 完整例子代码
  • 参考
相关产品与服务
云直播
云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档