隨著每一代 iPhone 處理能力和相機硬件配置的提高,使用它來捕獲視頻也變得更加有意思。它們小巧,輕便,低調(diào),而且與專業(yè)攝像機之間的差距已經(jīng)變得非常小,小到在某些情況下,iPhone 可以真正替代它們。
這篇文章討論了關(guān)于如何配置視頻捕獲管線 (pipeline) 和最大限度地利用硬件性能的一些不同選擇。 這里有個使用了不同管線的樣例 app,可以在 GitHub 查看。
UIImagePickerController
目前,將視頻捕獲集成到你的應(yīng)用中的最簡單的方法是使用 UIImagePickerController
。這是一個封裝了完整視頻捕獲管線和相機 UI 的 view controller。
在實例化相機之前,首先要檢查設(shè)備是否支持相機錄制:
if ([UIImagePickerController
isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
NSArray *availableMediaTypes = [UIImagePickerController
availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
if ([availableMediaTypes containsObject:(NSString *)kUTTypeMovie]) {
// 支持視頻錄制
}
}
然后創(chuàng)建一個 UIImagePickerController
對象,設(shè)置好代理便于進一步處理錄制好的視頻 (比如存到相冊) 以及對于用戶關(guān)閉相機作出響應(yīng):
UIImagePickerController *camera = [UIImagePickerController new];
camera.sourceType = UIImagePickerControllerSourceTypeCamera;
camera.mediaTypes = @[(NSString *)kUTTypeMovie];
camera.delegate = self;
這是你實現(xiàn)一個功能完善的攝像機所需要寫的所有代碼。
UIImagePickerController
提供了額外的配置選項。
通過設(shè)置 cameraDevice
屬性可以選擇一個特定的相機。這是一個 UIImagePickerControllerCameraDevice
枚舉,默認情況下是 UIImagePickerControllerCameraDeviceRear
,你也可以把它設(shè)置為 UIImagePickerControllerCameraDeviceFront
。每次都應(yīng)事先確認你想要設(shè)置的相機是可用的:
UIImagePickerController *camera = …
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) {
[camera setCameraDevice:UIImagePickerControllerCameraDeviceFront];
}
videoQuality
屬性用于控制錄制視頻的質(zhì)量。它允許你設(shè)置一個特定的編碼預(yù)設(shè),從而改變視頻的比特率和分辨率。以下是六種預(yù)設(shè):
enum {
UIImagePickerControllerQualityTypeHigh = 0,
UIImagePickerControllerQualityTypeMedium = 1, // default value
UIImagePickerControllerQualityTypeLow = 2,
UIImagePickerControllerQualityType640x480 = 3,
UIImagePickerControllerQualityTypeIFrame1280x720 = 4,
UIImagePickerControllerQualityTypeIFrame960x540 = 5
};
typedef NSUInteger UIImagePickerControllerQualityType;
前三種為相對預(yù)設(shè) (low, medium, high)。這些預(yù)設(shè)的編碼配置會因設(shè)備不同而不同。如果選擇 high,那么你選定的相機會提供給你該設(shè)備所能支持的最高畫質(zhì)。后面三種是特定分辨率的預(yù)設(shè) (640x480 VGA, 960x540 iFrame, 和 1280x720 iFrame)。
就像上面提到的,UIImagePickerController
自帶一套相機 UI,可以直接使用。然而,你也可以自定義相機的控件,通過隱藏默認控件,然后創(chuàng)建帶有控件的自定義視圖,并覆蓋在相機預(yù)覽圖層上面:
UIView *cameraOverlay = …
picker.showsCameraControls = NO;
picker.cameraOverlayView = cameraOverlay;
然后你需要將你覆蓋層上的控件關(guān)聯(lián)上 UIImagePickerController
的控制方法 (比如,startVideoCapture
和 stopVideoCapture
)。
如果你想要更多關(guān)于處理捕獲視頻的方法,而這些方法是 UIImagePickerController
所不能提供的,那么你需要使用 AVFoundation。
AVFoundation 中關(guān)于視頻捕獲的主要的類是 AVCaptureSession
。它負責調(diào)配影音輸入與輸出之間的數(shù)據(jù)流:
http://wiki.jikexueyuan.com/project/objc/images/23-1.svg" alt="" />
使用一個 capture session,你需要先實例化,添加輸入與輸出,接著啟動從輸入到輸出之間的數(shù)據(jù)流:
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];
(為了簡單起見,調(diào)度隊列 (dispatch queue) 的相關(guān)代碼已經(jīng)從上面那段代碼中省略了。所有對 capture session 的調(diào)用都是阻塞的,因此建議將它們分配到后臺串行隊列中。)
capture session 可以通過一個 sessionPreset
來進一步配置,這可以用來指定輸出質(zhì)量的等級。有 11 種不同的預(yù)設(shè)模式:
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;
第一個代表高像素圖片輸出。
接下來的九個和之前我們在設(shè)置 UIImagePickerController
的 videoQuality
時看到過的 UIImagePickerControllerQualityType
選項非常相似,不同的是,這里有一些額外可用于 capture session 的預(yù)設(shè)。
最后一個 (AVCaptureSessionPresetInputPriority
) 代表 capture session 不去控制音頻與視頻輸出設(shè)置。而是通過已連接的捕獲設(shè)備的 activeFormat
來反過來控制 capture session 的輸出質(zhì)量等級。在下一節(jié),我們將會看到更多關(guān)于設(shè)備和設(shè)備格式的細節(jié)。
AVCaptureSession
的輸入其實就是一個或多個的 AVCaptureDevice
對象,這些對象通過 AVCaptureDeviceInput
連接上 capture session。
我們可以使用 [AVCaptureDevice devices]
來尋找可用的捕獲設(shè)備。以 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
對象,參數(shù)是你期望的相機設(shè)備,然后把它添加到 capture session:
AVCaptureSession *captureSession = …
AVCaptureDevice *cameraDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *cameraDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice: error:&error];
if ([captureSession canAddInput:input]) {
[captureSession addInput:cameraDeviceInput];
}
如果上面提到的 capture session 預(yù)設(shè)列表里能滿足你的需求,那你就不需要做更多的事情了。如果不夠,比如你想要高的幀率,你將需要配置具體的設(shè)備格式。一個視頻捕獲設(shè)備有許多設(shè)備格式,每個都帶有特定的屬性和功能。下面是對于 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 |
通過上面的那些格式,你會發(fā)現(xiàn)如果要錄制 240 幀每秒的視頻的話,可以根據(jù)想要的像素格式選用第一個或第二個格式。另外若是要捕獲 1920x1080 的分辨率的視頻的話,是不支持 240 幀每秒的。
配置一個具體設(shè)備格式,你首先需要調(diào)用 lockForConfiguration:
來獲取設(shè)備的配置屬性的獨占訪問權(quán)限。接著你簡單地使用 setActiveFormat:
來設(shè)置設(shè)備的捕獲格式。這將會自動把 capture session 的預(yù)設(shè)設(shè)置為 AVCaptureSessionPresetInputPriority
。
一旦你設(shè)置了預(yù)想的設(shè)備格式,你就可以在這種設(shè)備格式的約束參數(shù)范圍內(nèi)進行進一步的配置了。
對于視頻捕獲的對焦,曝光和白平衡的設(shè)置,與圖像捕獲時一樣,具體可參考第 21 期“iOS 上的相機捕捉”。除了那些,這里還有一些視頻特有的配置選項。
你可以用捕獲設(shè)備的 activeVideoMinFrameDuration
和 activeVideoMaxFrameDuration
屬性設(shè)置幀速率,一幀的時長是幀速率的倒數(shù)。設(shè)置幀速率之前,要先確認它是否在設(shè)備格式所支持的范圍內(nèi),然后鎖住捕獲設(shè)備來進行配置。為了確保幀速率恒定,可以將最小與最大的幀時長設(shè)置成一樣的值:
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 發(fā)布時引入的功能。到了 iPhone 6,增加了更強勁和流暢的防抖模式,被稱為影院級的視頻防抖動。相關(guān)的 API 也有所改動 (目前為止并沒有在文檔中反映出來,不過可以查看頭文件)。防抖并不是在捕獲設(shè)備上配置的,而是在 AVCaptureConnection
上設(shè)置。由于不是所有的設(shè)備格式都支持全部的防抖模式,所以在實際應(yīng)用中應(yīng)事先確認具體的防抖模式是否支持:
AVCaptureDevice *device = ...;
AVCaptureConnection *connection = ...;
AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;
if ([device.activeFormat isVideoStabilizationModeSupported:stabilizationMode]) {
[connection setPreferredVideoStabilizationMode:stabilizationMode];
}
iPhone 6 的另一個新特性就是視頻 HDR (高動態(tài)范圍圖像),它是“高動態(tài)范圍的視頻流,與傳統(tǒng)的將不同曝光度的靜態(tài)圖像合成成一張高動態(tài)范圍圖像的方法完全不同”,它是內(nèi)建在傳感器中的。有兩種方法可以配置視頻 HDR:直接將 capture device 的 videoHDREnabled
設(shè)置為啟用或禁用,或者使用 automaticallyAdjustsVideoHDREnabled
屬性來留給系統(tǒng)處理。
之前展示的捕獲設(shè)備列表里面只有一個音頻設(shè)備,你可能覺得奇怪,畢竟 iPhone 6 有 3 個麥克風。然而因為有時會放在一起使用,便于優(yōu)化性能,因此可能被當做一個設(shè)備來使用。例如在 iPhone 5 及以上的手機錄制視頻時,會同時使用前置和后置麥克風,用于定向降噪。
大多數(shù)情況下,設(shè)置成默認的麥克風配置即可。后置麥克風會自動搭配后置攝像頭使用 (前置麥克風則用于降噪),前置麥克風和前置攝像頭也是一樣。
然而想要訪問和配置單獨的麥克風也是可行的。例如,當用戶正在使用后置攝像頭捕獲場景的時候,使用前置麥克風來錄制解說也應(yīng)是可能的。這就要依賴于 AVAudioSession
。
為了變更要訪問的音頻,audio session 首先需要設(shè)置為支持這樣做的類別。然后我們需要遍歷 audio session 的輸入端口和端口數(shù)據(jù)來源,來找到我們想要的麥克風:
// 配置 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;
}
}
除了設(shè)置非默認的麥克風配置,你也可以使用 AVAudioSession
來配置其他音頻設(shè)置,比如音頻增益和采樣率等。
有件事你需要記住,訪問相機和麥克風需要先獲得用戶授權(quán)。當你給視頻或音頻創(chuàng)建第一個 AVCaptureDeviceInput
對象時,iOS 會自動彈出一次對話框,請求用戶授權(quán),但你最好還是自己實現(xiàn)下。之后你就可以在還沒有被授權(quán)的時候,使用相同的代碼來提示用戶進行授權(quán)。當用戶未授權(quán)時,對于錄制視頻或音頻的嘗試,得到的將是黑色畫面和無聲。
輸入配置完了,現(xiàn)在把我們的注意力轉(zhuǎn)向 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];
當實際的錄制開始或停止時,想要接收回調(diào)的話就必須要一個錄制代理。當錄制停止時,輸出通常還在寫入數(shù)據(jù),等它完成之后會調(diào)用代理方法。
AVCaptureMovieFileOutput
有一些其他的配置選項,比如在某段時間后,在達到某個指定的文件尺寸時,或者當設(shè)備的最小磁盤剩余空間達到某個閾值時停止錄制。如果你還需要更多設(shè)置,比如自定義視頻音頻的壓縮率,或者你想要在寫入文件之前,處理視頻音頻的樣本,那么你需要一些更復(fù)雜的操作。
AVCaptureDataOutput
和 AVAssetWriter
如果你想要對影音輸出有更多的操作,你可以使用 AVCaptureVideoDataOutput
和 AVCaptureAudioDataOutput
而不是我們上節(jié)討論的 AVCaptureMovieFileOutput
。
這些輸出將會各自捕獲視頻和音頻的樣本緩存,接著發(fā)送到它們的代理。代理要么對采樣緩沖進行處理 (比如給視頻加濾鏡),要么保持原樣傳送。使用 AVAssetWriter
對象可以將樣本緩存寫入文件:
http://wiki.jikexueyuan.com/project/objc/images/23-2.svg" alt="" />
配置一個 asset writer 需要定義一個輸出 URL 和文件格式,并添加一個或多個輸入來接收采樣的緩沖。我們還需要將輸入的 expectsMediaInRealTime
屬性設(shè)置為 YES,因為它們需要從 capture session 實時獲得數(shù)據(jù)。
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 派送到后臺串行隊列中調(diào)用。)
在上面的示例代碼中,我們將 asset writer 的 outputSettings 設(shè)置為 nil
。這就意味著附加上來的樣本不會再被重新編碼。如果你確實想要重新編碼這些樣本,那么需要提供一個包含具體輸出參數(shù)的字典。關(guān)于音頻輸出設(shè)置的鍵值被定義在這里, 關(guān)于視頻輸出設(shè)置的鍵值定義在這里。
為了更簡單點,AVCaptureVideoDataOutput
和 AVCaptureAudioDataOutput
分別帶有 recommendedVideoSettingsForAssetWriterWithOutputFileType:
和 recommendedAudioSettingsForAssetWriterWithOutputFileType:
方法,可以生成與 asset writer 兼容的帶有全部鍵值對的字典。所以你可以通過在這個字典里調(diào)整你想要重寫的屬性,來簡單地定義你自己的輸出設(shè)置。比如,增加視頻比特率來提高視頻質(zhì)量等。
或者,你也可以使用 AVOutputSettingsAssistant
來配置輸出設(shè)置的字典,但是從我的經(jīng)驗來看,使用上面的方法會更好,它們會提供更實用的輸出設(shè)置,比如視頻比特率。另外,AVOutputSettingsAssistant
似乎存在一些缺點,例如,當你改變希望的視頻的幀速率時,視頻的比特率并不會改變。
當使用 AVFoundation
來做圖像捕獲時,我們必須提供一套自定義的用戶界面。其中一個關(guān)鍵的相機交互組件是實時預(yù)覽圖。最簡單的實現(xiàn)方式是通過把 AVCaptureVideoPreviewLayer
對象作為一個 sublayer 加到相機圖層上去:
AVCaptureSession *captureSession = ...;
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:captureSession];
UIView *cameraView = ...;
previewLayer.frame = cameraView.bounds;
[cameraView.layer addSublayer:previewLayer];
如果你想要更進一步操作,比如,在實時預(yù)覽圖加濾鏡,你需要將 AVCaptureVideoDataOutput
對象加到 capture session,并且使用 OpenGL 展示畫面,具體可查看該文“iOS 上的相機捕捉”
有許多不同的方法可以給 iOS 上的視頻捕獲配置管線,從最直接的 UIImagePickerController
,到精密配合的 AVCaptureSession
與 AVAssetWriter
。如何抉擇取決于你的項目要求,比如期望的視頻質(zhì)量和壓縮率,或者是你想要展示給用戶的相機控件。