美文网首页iCoderios developers音视频从入门到放弃
iOS音视频采集与格式转换(yuv转rgb)

iOS音视频采集与格式转换(yuv转rgb)

作者: zhq1992 | 来源:发表于2019-06-04 15:33 被阅读0次

    最近在项目里遇到了iOS音视频相关的东西,并且使用libyuv这个库将NV12转为BGRA, 这方面知识工作中用到不多,为了避免遗忘,趁热打铁写下这篇文章。

    1.音视频采集(使用AVFoundation)

    基本流程
    1.初始化输入设备
    2.初始化输出设备
    3.创建AVCaptureSession,用来管理视频与数据的捕获
    4.创建预览视图

    // 初始化输入设备
    - (void)initInputDevice{
        //获得输入设备
        AVCaptureDevice *backCaptureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得后置摄像头
        AVCaptureDevice *frontCaptureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionFront];//取得前置摄像头
        
        //根据输入设备初始化设备输入对象,用于获得输入数据
        _backCamera = [[AVCaptureDeviceInput alloc]initWithDevice:backCaptureDevice error:nil];
        _frontCamera = [[AVCaptureDeviceInput alloc]initWithDevice:frontCaptureDevice error:nil];
        
        AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
        self.audioInputDevice = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];
    }
    
    // 初始化输出设备
    - (void)initOutputDevice{
        //创建数据获取线程
        dispatch_queue_t captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //视频数据输出
        self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
        //设置代理,需要当前类实现protocol:AVCaptureVideoDataOutputSampleBufferDelegate
        [self.videoDataOutput setSampleBufferDelegate:self queue:captureQueue];
        //抛弃过期帧,保证实时性
        [self.videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];
        //设置输出格式为 yuv420
        [self.videoDataOutput setVideoSettings:@{
                                                 (__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
                                                 }];
        
        //音频数据输出
        self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
        //设置代理,需要当前类实现protocol:AVCaptureAudioDataOutputSampleBufferDelegate
        [self.audioDataOutput setSampleBufferDelegate:self queue:captureQueue];
    }
    
    
    // 创建AVCaptureSession
    - (void)createAVCaptureSession{
        
        self.captureSession = [[AVCaptureSession alloc] init];
        
        // 改变会话的配置前一定要先开启配置,配置完成后提交配置改变
        [self.captureSession beginConfiguration];
        // 设置分辨率
        [self setVideoPreset];
    
        //将设备输入添加到会话中
        if ([self.captureSession canAddInput:self.backCamera]) {
            [self.captureSession addInput:self.backCamera];
        }
        
        if ([self.captureSession canAddInput:self.audioInputDevice]) {
            [self.captureSession addInput:self.audioInputDevice];
        }
        
        //将设备输出添加到会话中
        if ([self.captureSession canAddOutput:self.videoDataOutput]) {
            [self.captureSession addOutput:self.videoDataOutput];
        }
        
        if ([self.captureSession canAddOutput:self.audioDataOutput]) {
            [self.captureSession addOutput:self.audioDataOutput];
        }
        
        [self createPreviewLayer];
        
        //提交配置变更
        [self.captureSession commitConfiguration];
        
        [self startRunning];
        
    }
    
    // 创建预览视图
    - (void)createPreviewLayer{
        
        [self.view addSubview:self.preView];
        
        //创建视频预览层,用于实时展示摄像头状态
        _captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];
        
        _captureVideoPreviewLayer.frame = self.view.bounds;
        _captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
        //将视频预览层添加到界面中
        [self.view.layer addSublayer:_captureVideoPreviewLayer];
    }
    
    
    #pragma mark - Control start/stop capture or change camera
    - (void)startRunning{
        if (!self.captureSession.isRunning) {
            [self.captureSession startRunning];
        }
    }
    - (void)stop{
        if (self.captureSession.isRunning) {
            [self.captureSession stopRunning];
        }
        
    }
    
    /**设置分辨率**/
    - (void)setVideoPreset{
        if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080])  {
            self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
        }else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
            self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
        }else{
            self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
        }
        
    }
    
    /**
     *  取得指定位置的摄像头
     *
     *  @param position 摄像头位置
     *
     *  @return 摄像头设备
     */
    -(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{
        NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
        for (AVCaptureDevice *camera in cameras) {
            if ([camera position]==position) {
                return camera;
            }
        }
        return nil;
    }
    
    

    2. CMSampleBufferRef

    前面介绍了如何通过相机实时获取音视频数据,我们接下来就需要了解获取到的数据到底是什么样的,使用系统提供的接口获取到的音视频数据都保存在CMSampleBufferRef中,这个结构在iOS中表示一帧音频/视频数据,它里面包含了这一帧数据的内容和格式,我们可以把它的内容取出来,提取出/转换成我们想要的数据。
    代表视频的CMSampleBufferRef中保存的数据是yuv420格式的视频帧(因为我们在视频输出设置中将输出格式设为:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)。
    在下面的回调中,可以拿到最终的CMSampleBufferRef数据

    -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
                
    }
    

    3.yuv,NV12

    视频是由一帧一帧的数据连接而成,而一帧视频数据其实就是一张图片。
    yuv是一种图片储存格式,跟RGB格式类似。
    RGB格式的图片很好理解,计算机中的大多数图片,都是以RGB格式存储的。
    yuv中,y表示亮度,单独只有y数据就可以形成一张图片,只不过这张图片是灰色的。u和v表示色差(u和v也被称为:Cb-蓝色差,Cr-红色差),
    为什么要yuv?
    有一定历史原因,最早的电视信号,为了兼容黑白电视,采用的就是yuv格式。
    一张yuv的图像,去掉uv,只保留y,这张图片就是黑白的。
    而且yuv可以通过抛弃色差来进行带宽优化。
    比如yuv420格式图像相比RGB来说,要节省一半的字节大小,抛弃相邻的色差对于人眼来说,差别不大。
    一张yuv格式的图像,占用字节数为 (width * height + (width * height) / 4 + (width * height) / 4) = (width * height) * 3 / 2
    一张RGB格式的图像,占用字节数为(width * height) * 3
    在传输上,yuv格式的视频也更灵活(yuv3种数据可分别传输)。
    很多视频编码器最初是不支持rgb格式的。但是所有的视频编码器都支持yuv格式。
    我们这里使用的就是yuv420格式的视频。
    yuv420也包含不同的数据排列格式:I420,NV12,NV21.
    其格式分别如下,
    I420格式:y,u,v 3个部分分别存储:Y0,Y1...Yn,U0,U1...Un/2,V0,V1...Vn/2
    NV12格式:y和uv 2个部分分别存储:Y0,Y1...Yn,U0,V0,U1,V1...Un/2,Vn/2
    NV21格式:同NV12,只是U和V的顺序相反。
    综合来说,除了存储顺序不同之外,上述格式对于显示来说没有任何区别。
    使用哪种视频的格式,取决于初始化相机时设置的视频输出格式。
    设置为kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange时,表示输出的视频格式为NV12;
    设置为kCVPixelFormatType_420YpCbCr8Planar时,表示使用I420。
    设置为kCVPixelFormatType_32RGBA时,表示使用BGRA。

    GPUImage设置相机输出数据时,使用的就是NV12.
    为了一致,我们这里也选择NV12格式输出视频。

    4.libyuv

    libyuv是Google开源的实现各种YUV与RGB之间相互转换、旋转、缩放的库。它是跨平台的,可在Windows、Linux、Mac、Android等操作系统,x86、x64、arm架构上进行编译运行,支持SSE、AVX、NEON等SIMD指令加速.

    5.使用libyuv将nv12转为rgba

    导入libyuv库,并设置头文件搜索路径,不然会报错


    Header Search Paths
    -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
        CVPixelBufferRef initialPixelBuffer= CMSampleBufferGetImageBuffer(sampleBuffer);
        if (initialPixelBuffer == NULL) {
            return;
        }
        // 获取最终的音视频数据
        CVPixelBufferRef newPixelBuffer = [self convertVideoSmapleBufferToBGRAData:sampleBuffer];
        
        // 将CVPixelBufferRef转换成CMSampleBufferRef
        [self pixelBufferToSampleBuffer:newPixelBuffer];
        NSLog(@"initialPixelBuffer%@,newPixelBuffer%@", initialPixelBuffer, newPixelBuffer);
    
        // 使用完newPixelBuffer记得释放,否则内存会会溢出
        CFRelease(newPixelBuffer);
    }
    
    
    //转化
    -(CVPixelBufferRef)convertVideoSmapleBufferToBGRAData:(CMSampleBufferRef)videoSample{
        
        //CVPixelBufferRef是CVImageBufferRef的别名,两者操作几乎一致。
        //获取CMSampleBuffer的图像地址
        CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(videoSample);
      //VideoToolbox解码后的图像数据并不能直接给CPU访问,需先用CVPixelBufferLockBaseAddress()锁定地址才能从主存访问,否则调用CVPixelBufferGetBaseAddressOfPlane等函数则返回NULL或无效值。值得注意的是,CVPixelBufferLockBaseAddress自身的调用并不消耗多少性能,一般情况,锁定之后,往CVPixelBuffer拷贝内存才是相对耗时的操作,比如计算内存偏移。
        CVPixelBufferLockBaseAddress(pixelBuffer, 0);
        //图像宽度(像素)
        size_t pixelWidth = CVPixelBufferGetWidth(pixelBuffer);
        //图像高度(像素)
        size_t pixelHeight = CVPixelBufferGetHeight(pixelBuffer);
        //获取CVImageBufferRef中的y数据
        uint8_t *y_frame = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
        //获取CMVImageBufferRef中的uv数据
        uint8_t *uv_frame =(unsigned char *) CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
        
        
        // 创建一个空的32BGRA格式的CVPixelBufferRef
        NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}};
        CVPixelBufferRef pixelBuffer1 = NULL;
        CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault,
                                              pixelWidth,pixelHeight,kCVPixelFormatType_32BGRA,
                                              (__bridge CFDictionaryRef)pixelAttributes,&pixelBuffer1);
        if (result != kCVReturnSuccess) {
            NSLog(@"Unable to create cvpixelbuffer %d", result);
            return NULL;
        }
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        
        result = CVPixelBufferLockBaseAddress(pixelBuffer1, 0);
        if (result != kCVReturnSuccess) {
            CFRelease(pixelBuffer1);
            NSLog(@"Failed to lock base address: %d", result);
            return NULL;
        }
        
        // 得到新创建的CVPixelBufferRef中 rgb数据的首地址
        uint8_t *rgb_data = (uint8*)CVPixelBufferGetBaseAddress(pixelBuffer1);
        
        // 使用libyuv为rgb_data写入数据,将NV12转换为BGRA
        int ret = NV12ToARGB(y_frame, pixelWidth, uv_frame, pixelWidth, rgb_data, pixelWidth * 4, pixelWidth, pixelHeight);
        if (ret) {
            NSLog(@"Error converting NV12 VideoFrame to BGRA: %d", result);
            CFRelease(pixelBuffer1);
            return NULL;
        }
        CVPixelBufferUnlockBaseAddress(pixelBuffer1, 0);
        
        return pixelBuffer1;
    }
    
    // 将CVPixelBufferRef转换成CMSampleBufferRef
    -(CMSampleBufferRef)pixelBufferToSampleBuffer:(CVPixelBufferRef)pixelBuffer
    {
        
        CMSampleBufferRef sampleBuffer;
        CMTime frameTime = CMTimeMakeWithSeconds([[NSDate date] timeIntervalSince1970], 1000000000);
        CMSampleTimingInfo timing = {frameTime, frameTime, kCMTimeInvalid};
        CMVideoFormatDescriptionRef videoInfo = NULL;
        CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);
        
        OSStatus status = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);
        if (status != noErr) {
            NSLog(@"Failed to create sample buffer with error %zd.", status);
        }
        CVPixelBufferRelease(pixelBuffer);
        if(videoInfo)
            CFRelease(videoInfo);
        
        return sampleBuffer;
    }
    

    6.总结

    文章只介绍了使用AVFoundation进行视频采集和使用libyuv进行格式转换,音视频相关的知识还有很多,这里不再做详细介绍了。

    demo下载地址

    相关文章

      网友评论

        本文标题:iOS音视频采集与格式转换(yuv转rgb)

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