在 iOS 上捕獲視頻

隨着每一代 iPhone 處理能力和相機硬件配置的提高,使用它來捕獲視頻也變得更加有意思。它們小巧,輕便,低調,而且與專業攝像機之間的差距已經變得非常小,小到在某些情況下,iPhone 可以真正替代它們。

這篇文章討論了關於如何配置視頻捕獲管線 (pipeline) 和最大限度地利用硬件性能的一些不同選擇。 這裏有個使用了不同管線的樣例 app,可以在 GitHub 查看。

UIImagePickerController

目前,將視頻捕獲集成到你的應用中的最簡單的方法是使用 UIImagePickerController。這是一個封裝了完整視頻捕獲管線和相機 UI 的 view controller。

在實例化相機之前,首先要檢查設備是否支持相機錄製:

if ([UIImagePickerController  
       isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
    NSArray *availableMediaTypes = [UIImagePickerController
      availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
    if ([availableMediaTypes containsObject:(NSString *)kUTTypeMovie]) {
        // 支持視頻錄製
    }
}

然後創建一個 UIImagePickerController 對象,設置好代理便於進一步處理錄製好的視頻 (比如存到相冊) 以及對於用戶關閉相機作出響應:

UIImagePickerController *camera = [UIImagePickerController new];  
camera.sourceType = UIImagePickerControllerSourceTypeCamera;  
camera.mediaTypes = @[(NSString *)kUTTypeMovie];  
camera.delegate = self;  

這是你實現一個功能完善的攝像機所需要寫的所有代碼。

相機配置

UIImagePickerController 提供了額外的配置選項。

通過設置 cameraDevice 屬性可以選擇一個特定的相機。這是一個 UIImagePickerControllerCameraDevice 枚舉,默認情況下是 UIImagePickerControllerCameraDeviceRear,你也可以把它設置爲 UIImagePickerControllerCameraDeviceFront。每次都應事先確認你想要設置的相機是可用的:

UIImagePickerController *camera = …  
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) {  
    [camera setCameraDevice:UIImagePickerControllerCameraDeviceFront];
}

videoQuality 屬性用於控制錄製視頻的質量。它允許你設置一個特定的編碼預設,從而改變視頻的比特率和分辨率。以下是六種預設:

enum {  
   UIImagePickerControllerQualityTypeHigh             = 0,
   UIImagePickerControllerQualityTypeMedium           = 1,  // default  value
   UIImagePickerControllerQualityTypeLow              = 2,
   UIImagePickerControllerQualityType640x480          = 3,
   UIImagePickerControllerQualityTypeIFrame1280x720   = 4,
   UIImagePickerControllerQualityTypeIFrame960x540    = 5
};
typedef NSUInteger  UIImagePickerControllerQualityType;  

前三種爲相對預設 (low, medium, high)。這些預設的編碼配置會因設備不同而不同。如果選擇 high,那麼你選定的相機會提供給你該設備所能支持的最高畫質。後面三種是特定分辨率的預設 (640x480 VGA, 960x540 iFrame, 和 1280x720 iFrame)。

自定義 UI

就像上面提到的,UIImagePickerController 自帶一套相機 UI,可以直接使用。然而,你也可以自定義相機的控件,通過隱藏默認控件,然後創建帶有控件的自定義視圖,並覆蓋在相機預覽圖層上面:

UIView *cameraOverlay = …  
picker.showsCameraControls = NO;  
picker.cameraOverlayView = cameraOverlay;  

然後你需要將你覆蓋層上的控件關聯上 UIImagePickerController 的控制方法 (比如,startVideoCapture 和 stopVideoCapture)。

AVFoundation

如果你想要更多關於處理捕獲視頻的方法,而這些方法是 UIImagePickerController 所不能提供的,那麼你需要使用 AVFoundation。

AVFoundation 中關於視頻捕獲的主要的類是 AVCaptureSession。它負責調配影音輸入與輸出之間的數據流:

AVCaptureSession setup

使用一個 capture session,你需要先實例化,添加輸入與輸出,接着啓動從輸入到輸出之間的數據流:

AVCaptureSession *captureSession = [AVCaptureSession new];  
AVCaptureDeviceInput *cameraDeviceInput = …  
AVCaptureDeviceInput *micDeviceInput = …  
AVCaptureMovieFileOutput *movieFileOutput = …  
if ([captureSession canAddInput:cameraDeviceInput]) {  
    [captureSession addInput:cameraDeviceInput];
}
if ([captureSession canAddInput:micDeviceInput]) {  
    [captureSession addInput:micDeviceInput];
}
if ([captureSession canAddOutput:movieFileOutput]) {  
    [captureSession addOutput:movieFileOutput];
}

[captureSession startRunning];

(爲了簡單起見,調度隊列 (dispatch queue) 的相關代碼已經從上面那段代碼中省略了。所有對 capture session 的調用都是阻塞的,因此建議將它們分配到後臺串行隊列中。)

capture session 可以通過一個 sessionPreset 來進一步配置,這可以用來指定輸出質量的等級。有 11 種不同的預設模式:

NSString *const  AVCaptureSessionPresetPhoto;  
NSString *const  AVCaptureSessionPresetHigh;  
NSString *const  AVCaptureSessionPresetMedium;  
NSString *const  AVCaptureSessionPresetLow;  
NSString *const  AVCaptureSessionPreset352x288;  
NSString *const  AVCaptureSessionPreset640x480;  
NSString *const  AVCaptureSessionPreset1280x720;  
NSString *const  AVCaptureSessionPreset1920x1080;  
NSString *const  AVCaptureSessionPresetiFrame960x540;  
NSString *const  AVCaptureSessionPresetiFrame1280x720;  
NSString *const  AVCaptureSessionPresetInputPriority;  

第一個代表高像素圖片輸出。 接下來的九個和之前我們在設置 UIImagePickerController 的  videoQuality 時看到過的 UIImagePickerControllerQualityType 選項非常相似,不同的是,這裏有一些額外可用於 capture session 的預設。 最後一個 (AVCaptureSessionPresetInputPriority) 代表 capture session 不去控制音頻與視頻輸出設置。而是通過已連接的捕獲設備的 activeFormat 來反過來控制 capture session 的輸出質量等級。在下一節,我們將會看到更多關於設備和設備格式的細節。

輸入

AVCaptureSession 的輸入其實就是一個或多個的 AVCaptureDevice 對象,這些對象通過 AVCaptureDeviceInput 連接上 capture session。

我們可以使用 [AVCaptureDevice devices] 來尋找可用的捕獲設備。以 iPhone 6 爲例:

(
    “<AVCaptureFigVideoDevice: 0x136514db0 [Back Camera][com.apple.avfoundation.avcapturedevice.built-in_video:0]>”,
    “<AVCaptureFigVideoDevice: 0x13660be80 [Front Camera][com.apple.avfoundation.avcapturedevice.built-in_video:1]>”,
    “<AVCaptureFigAudioDevice: 0x174265e80 [iPhone Microphone][com.apple.avfoundation.avcapturedevice.built-in_audio:0]>”
)

視頻輸入

配置相機輸入,需要實例化一個 AVCaptureDeviceInput 對象,參數是你期望的相機設備,然後把它添加到 capture session:

AVCaptureSession *captureSession = …  
AVCaptureDevice *cameraDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];  
NSError *error;  
AVCaptureDeviceInput *cameraDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:cameraDevice error:&error];  
if ([captureSession canAddInput:input]) {  
    [captureSession addInput:cameraDeviceInput];
}

如果上面提到的 capture session 預設列表裏能滿足你的需求,那你就不需要做更多的事情了。如果不夠,比如你想要高的幀率,你將需要配置具體的設備格式。一個視頻捕獲設備有許多設備格式,每個都帶有特定的屬性和功能。下面是對於 iPhone6 的後置攝像頭的一些例子 (一共有 22 種可用格式):

格式 分辨率 FPS HRSI FOV VIS 最大放大比例 Upscales AF ISO SS HDR
420v 1280x720 5 - 240 1280x720 54.626 YES 49.12 1.09 1 29.0 - 928 0.000003-0.200000 NO
420f 1280x720 5 - 240 1280x720 54.626 YES 49.12 1.09 1 29.0 - 928 0.000003-0.200000 NO
420v 1920x1080 2 - 30 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000013-0.500000 YES
420f 1920x1080 2 - 30 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000013-0.500000 YES
420v 1920x1080 2 - 60 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000008-0.500000 YES
420f 1920x1080 2 - 60 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000008-0.500000 YES
  • 格式 = 像素格式
  • FPS = 支持幀數範圍
  • HRSI = 高像素靜態圖片尺寸
  • FOV = 視角
  • VIS = 該格式支持視頻防抖
  • Upscales = 加入數字 upscaling 時的放大比例
  • AF = 自動對焦系統(1 是反差對焦,2 是相位對焦)
  • ISO = 支持感光度範圍
  • SS = 支持曝光時間範圍
  • HDR = 支持高動態範圍圖像

通過上面的那些格式,你會發現如果要錄製 240 幀每秒的視頻的話,可以根據想要的像素格式選用第一個或第二個格式。另外若是要捕獲 1920x1080 的分辨率的視頻的話,是不支持 240 幀每秒的。

配置一個具體設備格式,你首先需要調用 lockForConfiguration: 來獲取設備的配置屬性的獨佔訪問權限。接着你簡單地使用 setActiveFormat: 來設置設備的捕獲格式。這將會自動把 capture session 的預設設置爲 AVCaptureSessionPresetInputPriority

一旦你設置了預想的設備格式,你就可以在這種設備格式的約束參數範圍內進行進一步的配置了。

對於視頻捕獲的對焦,曝光和白平衡的設置,與圖像捕獲時一樣,具體可參考第 21 期“iOS 上的相機捕捉”。除了那些,這裏還有一些視頻特有的配置選項。

你可以用捕獲設備的 activeVideoMinFrameDuration 和 activeVideoMaxFrameDuration 屬性設置幀速率,一幀的時長是幀速率的倒數。設置幀速率之前,要先確認它是否在設備格式所支持的範圍內,然後鎖住捕獲設備來進行配置。爲了確保幀速率恆定,可以將最小與最大的幀時長設置成一樣的值:

NSError *error;  
CMTime frameDuration = CMTimeMake(1, 60);  
NSArray *supportedFrameRateRanges = [device.activeFormat videoSupportedFrameRateRanges];  
BOOL frameRateSupported = NO;  
for (AVFrameRateRange *range in supportedFrameRateRanges) {  
    if (CMTIME_COMPARE_INLINE(frameDuration, >=, range.minFrameDuration) &&
        CMTIME_COMPARE_INLINE(frameDuration, <=, range.maxFrameDuration)) {
        frameRateSupported = YES;
    }
}

if (frameRateSupported && [device lockForConfiguration:&error]) {  
    [device setActiveVideoMaxFrameDuration:frameDuration];
    [device setActiveVideoMinFrameDuration:frameDuration];
    [device unlockForConfiguration];
}

視頻防抖 是在 iOS 6 和 iPhone 4S 發佈時引入的功能。到了 iPhone 6,增加了更強勁和流暢的防抖模式,被稱爲影院級的視頻防抖動。相關的 API 也有所改動 (目前爲止並沒有在文檔中反映出來,不過可以查看頭文件)。防抖並不是在捕獲設備上配置的,而是在 AVCaptureConnection 上設置。由於不是所有的設備格式都支持全部的防抖模式,所以在實際應用中應事先確認具體的防抖模式是否支持:

AVCaptureDevice *device = ...;  
AVCaptureConnection *connection = ...;

AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;  
if ([device.activeFormat isVideoStabilizationModeSupported:stabilizationMode]) {  
    [connection setPreferredVideoStabilizationMode:stabilizationMode];
}

iPhone 6 的另一個新特性就是視頻 HDR (高動態範圍圖像),它是“高動態範圍的視頻流,與傳統的將不同曝光度的靜態圖像合成成一張高動態範圍圖像的方法完全不同”,它是內建在傳感器中的。有兩種方法可以配置視頻 HDR:直接將 capture device 的 videoHDREnabled 設置爲啓用或禁用,或者使用 automaticallyAdjustsVideoHDREnabled 屬性來留給系統處理。

技術參考:iPhone 6 和 iPhone Plus 的新 AV Foundation 相機特性

音頻輸入

之前展示的捕獲設備列表裏面只有一個音頻設備,你可能覺得奇怪,畢竟 iPhone 6 有 3 個麥克風。然而因爲有時會放在一起使用,便於優化性能,因此可能被當做一個設備來使用。例如在 iPhone 5 及以上的手機錄製視頻時,會同時使用前置和後置麥克風,用於定向降噪。

Technical Q&A: AVAudioSession - Microphone Selection

大多數情況下,設置成默認的麥克風配置即可。後置麥克風會自動搭配後置攝像頭使用 (前置麥克風則用於降噪),前置麥克風和前置攝像頭也是一樣。

然而想要訪問和配置單獨的麥克風也是可行的。例如,當用戶正在使用後置攝像頭捕獲場景的時候,使用前置麥克風來錄製解說也應是可能的。這就要依賴於 AVAudioSession。 爲了變更要訪問的音頻,audio session 首先需要設置爲支持這樣做的類別。然後我們需要遍歷 audio session 的輸入端口和端口數據來源,來找到我們想要的麥克風:

// 配置 audio session
AVAudioSession *audioSession = [AVAudioSession sharedInstance];  
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[audioSession setActive:YES error:nil];

// 尋找期望的輸入端口
NSArray* inputs = [audioSession availableInputs];  
AVAudioSessionPortDescription *builtInMic = nil;  
for (AVAudioSessionPortDescription* port in inputs) {  
    if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
        builtInMic = port;
        break;
    }
}

// 尋找期望的麥克風
for (AVAudioSessionDataSourceDescription* source in builtInMic.dataSources) {  
    if ([source.orientation isEqual:AVAudioSessionOrientationFront]) {
        [builtInMic setPreferredDataSource:source error:nil];
        [audioSession setPreferredInput:builtInMic error:&error];
        break;
    }
}

除了設置非默認的麥克風配置,你也可以使用 AVAudioSession 來配置其他音頻設置,比如音頻增益和採樣率等。

訪問權限

有件事你需要記住,訪問相機和麥克風需要先獲得用戶授權。當你給視頻或音頻創建第一個 AVCaptureDeviceInput 對象時,iOS 會自動彈出一次對話框,請求用戶授權,但你最好還是自己實現下。之後你就可以在還沒有被授權的時候,使用相同的代碼來提示用戶進行授權。當用戶未授權時,對於錄製視頻或音頻的嘗試,得到的將是黑色畫面和無聲。

輸出

輸入配置完了,現在把我們的注意力轉向 capture session 的輸出。

AVCaptureMovieFileOutput

將視頻寫入文件,最簡單的選擇就是使用 AVCaptureMovieFileOutput 對象。把它作爲輸出添加到 capture session 中,就可以將視頻和音頻寫入 QuickTime 文件,這隻需很少的配置。

AVCaptureMovieFileOutput *movieFileOutput = [AVCaptureMovieFileOutput new];  
if([captureSession canAddOutput:movieFileOutput]){  
    [captureSession addOutput:movieFileOutput];
}

// 開始錄製
NSURL *outputURL = …  
[movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];

當實際的錄製開始或停止時,想要接收回調的話就必須要一個錄製代理。當錄製停止時,輸出通常還在寫入數據,等它完成之後會調用代理方法。

AVCaptureMovieFileOutput 有一些其他的配置選項,比如在某段時間後,在達到某個指定的文件尺寸時,或者當設備的最小磁盤剩餘空間達到某個閾值時停止錄製。如果你還需要更多設置,比如自定義視頻音頻的壓縮率,或者你想要在寫入文件之前,處理視頻音頻的樣本,那麼你需要一些更復雜的操作。

AVCaptureDataOutput 和 AVAssetWriter

如果你想要對影音輸出有更多的操作,你可以使用 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 而不是我們上節討論的 AVCaptureMovieFileOutput

這些輸出將會各自捕獲視頻和音頻的樣本緩存,接着發送到它們的代理。代理要麼對採樣緩衝進行處理 (比如給視頻加濾鏡),要麼保持原樣傳送。使用 AVAssetWriter 對象可以將樣本緩存寫入文件:

Using an AVAssetWriter

配置一個 asset writer 需要定義一個輸出 URL 和文件格式,並添加一個或多個輸入來接收採樣的緩衝。我們還需要將輸入的 expectsMediaInRealTime 屬性設置爲 YES,因爲它們需要從 capture session 實時獲得數據。

NSURL *url = …;  
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil];  
AVAssetWriterInput *videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:nil];  
videoInput.expectsMediaDataInRealTime = YES;  
AVAssetWriterInput *audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:nil];  
audioInput.expectsMediaDataInRealTime = YES;  
if ([assetWriter canAddInput:videoInput]) {  
    [assetWriter addInput:videoInput];
}
if ([assetWriter canAddInput:audioInput]) {  
    [assetWriter addInput:audioInput];
}

(這裏推薦將 asset writer 派送到後臺串行隊列中調用。)

在上面的示例代碼中,我們將 asset writer 的 outputSettings 設置爲 nil。這就意味着附加上來的樣本不會再被重新編碼。如果你確實想要重新編碼這些樣本,那麼需要提供一個包含具體輸出參數的字典。關於音頻輸出設置的鍵值被定義在這裏, 關於視頻輸出設置的鍵值定義在這裏

爲了更簡單點,AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 分別帶有 recommendedVideoSettingsForAssetWriterWithOutputFileType: 和 recommendedAudioSettingsForAssetWriterWithOutputFileType: 方法,可以生成與 asset writer 兼容的帶有全部鍵值對的字典。所以你可以通過在這個字典裏調整你想要重寫的屬性,來簡單地定義你自己的輸出設置。比如,增加視頻比特率來提高視頻質量等。

或者,你也可以使用 AVOutputSettingsAssistant 來配置輸出設置的字典,但是從我的經驗來看,使用上面的方法會更好,它們會提供更實用的輸出設置,比如視頻比特率。另外,AVOutputSettingsAssistant 似乎存在一些缺點,例如,當你改變希望的視頻的幀速率時,視頻的比特率並不會改變。

實時預覽

當使用 AVFoundation 來做圖像捕獲時,我們必須提供一套自定義的用戶界面。其中一個關鍵的相機交互組件是實時預覽圖。最簡單的實現方式是通過把 AVCaptureVideoPreviewLayer 對象作爲一個 sublayer 加到相機圖層上去:

AVCaptureSession *captureSession = ...;  
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:captureSession];  
UIView *cameraView = ...;  
previewLayer.frame = cameraView.bounds;  
[cameraView.layer addSublayer:previewLayer];

如果你想要更進一步操作,比如,在實時預覽圖加濾鏡,你需要將 AVCaptureVideoDataOutput 對象加到 capture session,並且使用 OpenGL 展示畫面,具體可查看該文“iOS 上的相機捕捉”

總結

有許多不同的方法可以給 iOS 上的視頻捕獲配置管線,從最直接的 UIImagePickerController,到精密配合的 AVCaptureSession 與 AVAssetWriter 。如何抉擇取決於你的項目要求,比如期望的視頻質量和壓縮率,或者是你想要展示給用戶的相機控件。

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