iOS音視頻采集與格式轉(zhuǎn)換(yuv轉(zhuǎn)rgb)

最近在項目里遇到了iOS音視頻相關(guān)的東西,并且使用libyuv這個庫將NV12轉(zhuǎn)為BGRA, 這方面知識工作中用到不多,為了避免遺忘,趁熱打鐵寫下這篇文章。

1.音視頻采集(使用AVFoundation)

基本流程
1.初始化輸入設(shè)備
2.初始化輸出設(shè)備
3.創(chuàng)建AVCaptureSession,用來管理視頻與數(shù)據(jù)的捕獲
4.創(chuàng)建預(yù)覽視圖

// 初始化輸入設(shè)備
- (void)initInputDevice{
    //獲得輸入設(shè)備
    AVCaptureDevice *backCaptureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得后置攝像頭
    AVCaptureDevice *frontCaptureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionFront];//取得前置攝像頭
    
    //根據(jù)輸入設(shè)備初始化設(shè)備輸入對象,用于獲得輸入數(shù)據(jù)
    _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];
}
// 初始化輸出設(shè)備
- (void)initOutputDevice{
    //創(chuàng)建數(shù)據(jù)獲取線程
    dispatch_queue_t captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //視頻數(shù)據(jù)輸出
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    //設(shè)置代理,需要當前類實現(xiàn)protocol:AVCaptureVideoDataOutputSampleBufferDelegate
    [self.videoDataOutput setSampleBufferDelegate:self queue:captureQueue];
    //拋棄過期幀,保證實時性
    [self.videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];
    //設(shè)置輸出格式為 yuv420
    [self.videoDataOutput setVideoSettings:@{
                                             (__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
                                             }];
    
    //音頻數(shù)據(jù)輸出
    self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
    //設(shè)置代理,需要當前類實現(xiàn)protocol:AVCaptureAudioDataOutputSampleBufferDelegate
    [self.audioDataOutput setSampleBufferDelegate:self queue:captureQueue];
}

// 創(chuàng)建AVCaptureSession
- (void)createAVCaptureSession{
    
    self.captureSession = [[AVCaptureSession alloc] init];
    
    // 改變會話的配置前一定要先開啟配置,配置完成后提交配置改變
    [self.captureSession beginConfiguration];
    // 設(shè)置分辨率
    [self setVideoPreset];

    //將設(shè)備輸入添加到會話中
    if ([self.captureSession canAddInput:self.backCamera]) {
        [self.captureSession addInput:self.backCamera];
    }
    
    if ([self.captureSession canAddInput:self.audioInputDevice]) {
        [self.captureSession addInput:self.audioInputDevice];
    }
    
    //將設(shè)備輸出添加到會話中
    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];
    
}

// 創(chuàng)建預(yù)覽視圖
- (void)createPreviewLayer{
    
    [self.view addSubview:self.preView];
    
    //創(chuàng)建視頻預(yù)覽層,用于實時展示攝像頭狀態(tài)
    _captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];
    
    _captureVideoPreviewLayer.frame = self.view.bounds;
    _captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
    //將視頻預(yù)覽層添加到界面中
    [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];
    }
    
}

/**設(shè)置分辨率**/
- (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 攝像頭設(shè)備
 */
-(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{
    NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *camera in cameras) {
        if ([camera position]==position) {
            return camera;
        }
    }
    return nil;
}

2. CMSampleBufferRef

前面介紹了如何通過相機實時獲取音視頻數(shù)據(jù),我們接下來就需要了解獲取到的數(shù)據(jù)到底是什么樣的,使用系統(tǒng)提供的接口獲取到的音視頻數(shù)據(jù)都保存在CMSampleBufferRef中,這個結(jié)構(gòu)在iOS中表示一幀音頻/視頻數(shù)據(jù),它里面包含了這一幀數(shù)據(jù)的內(nèi)容和格式,我們可以把它的內(nèi)容取出來,提取出/轉(zhuǎn)換成我們想要的數(shù)據(jù)。
代表視頻的CMSampleBufferRef中保存的數(shù)據(jù)是yuv420格式的視頻幀(因為我們在視頻輸出設(shè)置中將輸出格式設(shè)為:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)。
在下面的回調(diào)中,可以拿到最終的CMSampleBufferRef數(shù)據(jù)

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

3.yuv,NV12

視頻是由一幀一幀的數(shù)據(jù)連接而成,而一幀視頻數(shù)據(jù)其實就是一張圖片。
yuv是一種圖片儲存格式,跟RGB格式類似。
RGB格式的圖片很好理解,計算機中的大多數(shù)圖片,都是以RGB格式存儲的。
yuv中,y表示亮度,單獨只有y數(shù)據(jù)就可以形成一張圖片,只不過這張圖片是灰色的。u和v表示色差(u和v也被稱為:Cb-藍色差,Cr-紅色差),
為什么要yuv?
有一定歷史原因,最早的電視信號,為了兼容黑白電視,采用的就是yuv格式。
一張yuv的圖像,去掉uv,只保留y,這張圖片就是黑白的。
而且yuv可以通過拋棄色差來進行帶寬優(yōu)化。
比如yuv420格式圖像相比RGB來說,要節(jié)省一半的字節(jié)大小,拋棄相鄰的色差對于人眼來說,差別不大。
一張yuv格式的圖像,占用字節(jié)數(shù)為 (width * height + (width * height) / 4 + (width * height) / 4) = (width * height) * 3 / 2
一張RGB格式的圖像,占用字節(jié)數(shù)為(width * height) * 3
在傳輸上,yuv格式的視頻也更靈活(yuv3種數(shù)據(jù)可分別傳輸)。
很多視頻編碼器最初是不支持rgb格式的。但是所有的視頻編碼器都支持yuv格式。
我們這里使用的就是yuv420格式的視頻。
yuv420也包含不同的數(shù)據(jù)排列格式: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的順序相反。
綜合來說,除了存儲順序不同之外,上述格式對于顯示來說沒有任何區(qū)別。
使用哪種視頻的格式,取決于初始化相機時設(shè)置的視頻輸出格式。
設(shè)置為kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange時,表示輸出的視頻格式為NV12;
設(shè)置為kCVPixelFormatType_420YpCbCr8Planar時,表示使用I420。
設(shè)置為kCVPixelFormatType_32RGBA時,表示使用BGRA。

GPUImage設(shè)置相機輸出數(shù)據(jù)時,使用的就是NV12.
為了一致,我們這里也選擇NV12格式輸出視頻。

4.libyuv

libyuv是Google開源的實現(xiàn)各種YUV與RGB之間相互轉(zhuǎn)換、旋轉(zhuǎn)、縮放的庫。它是跨平臺的,可在Windows、Linux、Mac、Android等操作系統(tǒng),x86、x64、arm架構(gòu)上進行編譯運行,支持SSE、AVX、NEON等SIMD指令加速.

5.使用libyuv將nv12轉(zhuǎn)為rgba

導入libyuv庫,并設(shè)置頭文件搜索路徑,不然會報錯


Header Search Paths
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
    CVPixelBufferRef initialPixelBuffer= CMSampleBufferGetImageBuffer(sampleBuffer);
    if (initialPixelBuffer == NULL) {
        return;
    }
    // 獲取最終的音視頻數(shù)據(jù)
    CVPixelBufferRef newPixelBuffer = [self convertVideoSmapleBufferToBGRAData:sampleBuffer];
    
    // 將CVPixelBufferRef轉(zhuǎn)換成CMSampleBufferRef
    [self pixelBufferToSampleBuffer:newPixelBuffer];
    NSLog(@"initialPixelBuffer%@,newPixelBuffer%@", initialPixelBuffer, newPixelBuffer);

    // 使用完newPixelBuffer記得釋放,否則內(nèi)存會會溢出
    CFRelease(newPixelBuffer);
}


//轉(zhuǎn)化
-(CVPixelBufferRef)convertVideoSmapleBufferToBGRAData:(CMSampleBufferRef)videoSample{
    
    //CVPixelBufferRef是CVImageBufferRef的別名,兩者操作幾乎一致。
    //獲取CMSampleBuffer的圖像地址
    CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(videoSample);
  //VideoToolbox解碼后的圖像數(shù)據(jù)并不能直接給CPU訪問,需先用CVPixelBufferLockBaseAddress()鎖定地址才能從主存訪問,否則調(diào)用CVPixelBufferGetBaseAddressOfPlane等函數(shù)則返回NULL或無效值。值得注意的是,CVPixelBufferLockBaseAddress自身的調(diào)用并不消耗多少性能,一般情況,鎖定之后,往CVPixelBuffer拷貝內(nèi)存才是相對耗時的操作,比如計算內(nèi)存偏移。
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    //圖像寬度(像素)
    size_t pixelWidth = CVPixelBufferGetWidth(pixelBuffer);
    //圖像高度(像素)
    size_t pixelHeight = CVPixelBufferGetHeight(pixelBuffer);
    //獲取CVImageBufferRef中的y數(shù)據(jù)
    uint8_t *y_frame = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
    //獲取CMVImageBufferRef中的uv數(shù)據(jù)
    uint8_t *uv_frame =(unsigned char *) CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
    
    
    // 創(chuàng)建一個空的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;
    }
    
    // 得到新創(chuàng)建的CVPixelBufferRef中 rgb數(shù)據(jù)的首地址
    uint8_t *rgb_data = (uint8*)CVPixelBufferGetBaseAddress(pixelBuffer1);
    
    // 使用libyuv為rgb_data寫入數(shù)據(jù),將NV12轉(zhuǎn)換為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轉(zhuǎn)換成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.總結(jié)

文章只介紹了使用AVFoundation進行視頻采集和使用libyuv進行格式轉(zhuǎn)換,音視頻相關(guān)的知識還有很多,這里不再做詳細介紹了。

demo下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容