今天介紹一下在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關係圖:
採集視頻的步驟
- 創建並初始化 AVCaptureSession。
- 創建並初始化 AVCaptureVideoDataOutput。
- 設置 AVCaptureVideoDataOutput的videoSettings,videoSettings 中的 Key and value 包含了輸出圖像與視頻格式定義。
- 調用 AVCaptureVideoDataOutput 對象的 setSampleBufferDelegate 方法,設置採樣數據緩衝區的代理。這樣當從輸入設備採集到數據後,系統就會自動調用AVCaptureVideoDataOutputSampleBufferDelegate 協議中的 captureOutput 方法,從而獲取到視頻數據。
- 將 AVCaptureVideoDataOutput 對象添加到 AVCaptureSession對象中。
- 根據視頻類型 AVMediaTypeVideo,創建 AVCaptureDevice 對象。(可以創建視頻設備也可以創建音頻設備)。
- 以 AVCaptureDevice 爲參數,創建 AVCaptureDeviceInput 對象。
- 將 AVCaptureDeviceInput 對像添加到 AVCaptureSession 對象中。
- 調用 AVCaptureSession 對象的 setSessionPreset 方法進行屬性設置。如 設置 quality level, bitrate, 或其它 output 的 settings。
- 調用 Output 對象的 connectionWithMediaType 方法,建立 Input與Output之前的連接。
- 調用 AVCaptureSession 對象的 startRunning() 方法,開始視頻採集。
- 調用 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); } }