iOS直播視頻音頻採集

今天介紹一下在iOS下進行視頻採集。要了解iOS是怎樣進行視頻採集的,首先我們要了解 AVCaptureSession,AVCaptureDevice等幾個基本概念及iOS上視頻採集的工作原理。

基本概念

iPhone包括了攝像頭,麥克風等設備,我們用 AVCaptureDevice 代表它們。同時,攝像頭又是一個輸入設備,我們還可以用AVCaptureDeviceInput 表式它;同樣,麥克風則是另一個輸入設備(AVCaptureDeviceInput)。

爲了方便,iOS定義了AVCaptureSession類來管理這些輸入設備,可以通過 AVCaptureSession 打開某個輸入設備進行數據採集,或關閉某個輸入設備。

當數據被採集回來後,需要把這些數據進行保存,處理,於是iOS又定義了AVCatpureOutput來做這件事。

下面我們分別介紹每個類。

AVCaptureSession

AVCaptureSession對象用於管理採集活動,協調數據的流入流出。

AVCaptureSession對象的 startRunning() 方法是一個阻塞調用,可能需要一些時間,因此您應該在串行隊列上執行會話設置,以使主隊列不被阻止(這將保持UI響應)

AVCaptureDevice

AVCaptureDevice對象代表了一個物理設備及與設備相關的屬性。你可以使用它設置底層硬件的屬性。一個採集設備還可以爲 AVCaptureSession 對象提供數據。

可以使有 AVCaptureDevice 的類方法枚舉所有有效的設備,並查詢它們的能力。當設備有效或無效時,AVCaptureDevice會得到系統的通知。

設置設備屬性時,必須首先使用lockForConfiguration()方法將設備鎖住。爲設備設置完屬性後,你應該查詢是否已經設置成功,並在設置完成後調用 unlockForConfiguration() 釋放鎖。

對於大部分屬性配置都可以通過 AVCaptureSession 對象來設置,但一些特殊的選項如高幀率,則需要直接在 AVCaptureDevice 上進行設置。

AVCaptureDeviceInput

AVCaptureDeviceInput 是採集設備中的輸入端,它繼承自 AVCaptureInput,AVCaptureInput是一個抽象類。

AVCaptureConnection

AVCaptureConnection 代表的是 AVCaptureSession 裏 AVCaptureInput 與 AVCaptureOutput 對象之間建立的連接。

AVCaptureOutput

AVCaptureOutput 是一個抽象類,有很多具體的實現類,如AVCaptureVideoDataOutput、AVCaptureMovieFileOutput等。如下圖所示。但今天我們主要介紹的是 AVCaptureVideoDataOutput。

AVCaptureVideoDataOutput

AVCaptureVideoDataOutput是錄製視頻和訪問視頻幀的輸出。它繼承自 AVCaptureOutput。

下圖是AVCaptureDeviceInput、AVCaptureConnection及AVCaptureOutput關係圖:

採集視頻的步驟

  1. 創建並初始化 AVCaptureSession。
  2. 創建並初始化 AVCaptureVideoDataOutput。
  3. 設置 AVCaptureVideoDataOutput的videoSettings,videoSettings 中的 Key and value 包含了輸出圖像與視頻格式定義。
  4. 調用 AVCaptureVideoDataOutput 對象的 setSampleBufferDelegate 方法,設置採樣數據緩衝區的代理。這樣當從輸入設備採集到數據後,系統就會自動調用AVCaptureVideoDataOutputSampleBufferDelegate 協議中的 captureOutput 方法,從而獲取到視頻數據。
  5. 將 AVCaptureVideoDataOutput 對象添加到 AVCaptureSession對象中。
  6. 根據視頻類型 AVMediaTypeVideo,創建 AVCaptureDevice 對象。(可以創建視頻設備也可以創建音頻設備)。
  7. 以 AVCaptureDevice 爲參數,創建 AVCaptureDeviceInput 對象。
  8. 將 AVCaptureDeviceInput 對像添加到 AVCaptureSession 對象中。
  9. 調用 AVCaptureSession 對象的 setSessionPreset 方法進行屬性設置。如 設置 quality level, bitrate, 或其它 output 的 settings。
  10. 調用 Output 對象的 connectionWithMediaType 方法,建立 Input與Output之前的連接。
  11. 調用 AVCaptureSession 對象的 startRunning() 方法,開始視頻採集。
  12. 調用 AVCaptureSession 對像的 stopRunning() 方法,停止視頻採集。

簡單示例部分代碼如下(以AVCaptureSession採集爲例)

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate、AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    if (captureOutput == _audioOutput) {
        //向SDK填充Audio數據
        [_txLivePush sendAudioSampleBuffer:sampleBuffer withType:RPSampleBufferTypeAudioMic];
    } else {
        //向SDK填充Video數據
        [_txLivePush sendVideoSampleBuffer:sampleBuffer];
    }
}

//自定義採集參數設置以及啓動推流
- (void)startRtmp {

    if (_txLivePublisher != nil) {

        TXLivePushConfig* config = [[TXLivePushConfig alloc] init];
        //【示例代碼1】設置自定義視頻採集邏輯(自定義視頻採集邏輯不要調用startPreview)
        _config.customModeType |= CUSTOM_MODE_VIDEO_CAPTURE;
        _config.autoSampleBufferSize = YES;


        //【示例代碼2】設置自定義音頻採集邏輯(音頻採樣位寬必須是16)
        //_config.customModeType |= CUSTOM_MODE_AUDIO_CAPTURE;
        //_config.audioSampleRate = AUDIO_SAMPLE_RATE_48000;


        //開始推流
        [_txLivePublisher setConfig:_config];
        _txLivePublisher.delegate = self;
        [_txLivePublisher startPush:rtmpUrl];
    }
}

//YUV數據轉CVPixelBuffer(不是必須)
- (void)didReceivedYUV420pPacket:(APYUV420pPacket)packet {
    int sYLineSize = packet.yLineSize;
    int sULineSize = packet.uLineSize;
    int sVLineSize = packet.vLineSize;
    int sYSize = sYLineSize * packet.height;
    int sUSize = sULineSize * packet.height/2;
    int sVSize = sVLineSize * packet.height/2;

    int dWidth = packet.width;
    int dHeight = packet.height;

    CVPixelBufferRef pxbuffer;
    CVReturn rc;

    rc = CVPixelBufferCreate(NULL, dWidth, dHeight, kCVPixelFormatType_420YpCbCr8PlanarFullRange, NULL, &pxbuffer);
    if (rc != 0) {
        NSLog(@"CVPixelBufferCreate failed %d", rc);
        if (pxbuffer) { CFRelease(pxbuffer); }
        return;
    }

    rc = CVPixelBufferLockBaseAddress(pxbuffer, 0);

    if (rc != 0) {
        NSLog(@"CVPixelBufferLockBaseAddress falied %d", rc);
        if (pxbuffer) { CFRelease(pxbuffer); }
        return;
    } else {
        uint8_t *y_copyBaseAddress = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pxbuffer, 0);
        uint8_t *u_copyBaseAddress= (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pxbuffer, 1);
        uint8_t *v_copyBaseAddress= (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pxbuffer, 2);

        int dYLineSize = (int)CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 0);
        int dULineSize = (int)CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 1);
        int dVLineSize = (int)CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 2);

        memcpy(y_copyBaseAddress, packet.dataBuffer,                    sYSize);
        memcpy(u_copyBaseAddress, packet.dataBuffer + sYSize,           sUSize);
        memcpy(v_copyBaseAddress, packet.dataBuffer + sYSize + sUSize,  sVSize);


        rc = CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
        if (rc != 0) {
            NSLog(@"CVPixelBufferUnlockBaseAddress falied %d", rc);
        }
    }

    CMVideoFormatDescriptionRef videoInfo = NULL;
    CMVideoFormatDescriptionCreateForImageBuffer(NULL, pxbuffer, &videoInfo);

    CMSampleTimingInfo timing = {kCMTimeInvalid, kCMTimeInvalid, kCMTimeInvalid};
    CMSampleBufferRef dstSampleBuffer = NULL;
    rc = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pxbuffer, YES, NULL, NULL, videoInfo, &timing, &dstSampleBuffer);

    if (rc) {
        NSLog(@"CMSampleBufferCreateForImageBuffer error: %d", rc);
    } else {
        [self.txLivePublisher sendVideoSampleBuffer:dstSampleBuffer];
    }

    if (pxbuffer) { CFRelease(pxbuffer); }
    if (videoInfo) { CFRelease(videoInfo); }
    if (dstSampleBuffer) { CFRelease(dstSampleBuffer); }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章