AVFoundation框架(五) 媒體捕捉上- 簡單的錄制

1. 媒體捕捉的類

AVFoundation的照片和視頻捕捉功能是它的強項.先看一下其中捕捉相關(guān)的類.

找到唯一比較清楚的圖.png
  • 捕捉會話 AVCaptureSession:
    AVCaptureSession相當于一個虛擬的插座,連接了輸入和輸出資源.它管理著從物理設(shè)備(攝像頭和麥克風)得到的數(shù)據(jù)流,然后輸出到其他地方.
    可以額外配置一個會話預(yù)設(shè)值,用來控制捕捉數(shù)據(jù)的格式和質(zhì)量,默認是AVCaptureSessionPresetHigh
  • 捕捉設(shè)備 AVCaptureDevice:
    AVCaptureDevice為物理設(shè)備定義一個接口和大量控制方法,例如對焦幽歼、曝光漱受、白平衡和閃光等屹篓。
  • 捕捉設(shè)備輸入 AVCaptureDeviceInput:
    在使用AVCaptureDevice進行處理前,需要將它封裝到AVCaptureDeviceInput實例中.因為一個捕捉設(shè)備不能直接添加到AVCaptureSession中; 如下:
NSError *error;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
  • 捕捉的輸出 AVCaptureOutput:
    AVCaptureOutput有許多擴展類,它本身只是一個抽象基類,用于為從Session得到的數(shù)據(jù)尋找輸出目的地. 看上面結(jié)構(gòu)圖就可以看出各擴展類功能方向, 這里單獨說一下, AVCaptureAudioDataOutputAVCaptureVideoDataOutput可以直接訪問硬件撲捉到的數(shù)字樣本,可以用于音視頻流進行實施處理.
  • 捕捉連接 AVCaptureConnection
    這個類其實就是上圖中連接不同組件的連接箭頭所表示. 對這些連接的訪問可以讓開發(fā)者對信號流就行底層控制,比如禁用某些特定的連接,或者音頻連接中限制單獨的音頻軌道.
  • 捕捉預(yù)覽 AVCaptureVideoPreviewLayer
    這個類不在上圖中, 它是對捕捉視頻數(shù)據(jù)進行實時預(yù)覽.在視頻角色中類似于AVPlayerLayer

捕捉會話應(yīng)用流程:

  • 第一步:配置會話
- (BOOL)setupSession:(NSError **)error {
    // 1. 創(chuàng)建捕捉會話
    self.captureSession = [[AVCaptureSession alloc] init];                  
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;

     //2. 根據(jù)要生成的媒體類型獲取捕捉設(shè)備,并添加到Session上.
    AVCaptureDevice *videoDevice =                                          
        [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
           // 在把此捕捉設(shè)備添加到AVCaptureSession之前,先封裝成一個input對象.
    AVCaptureDeviceInput *videoInput =                                      
        [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
    if (videoInput) {
        if ([self.captureSession canAddInput:videoInput]) {  // 測試是否可以add
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        }
    } else {
        return NO;
    }
/*// 如果要獲取音頻捕捉部分
    AVCaptureDevice *audioDevice =                                          
        [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    AVCaptureDeviceInput *audioInput =                                      
        [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
    if (audioInput) {
        if ([self.captureSession canAddInput:audioInput]) {                
            [self.captureSession addInput:audioInput];
        }
    } else {
        return NO;
    }
*/

     //3. 設(shè)置捕捉會話的輸出部分
        // image輸出
    self.imageOutput = [[AVCaptureStillImageOutput alloc] init];            
    self.imageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};

    if ([self.captureSession canAddOutput:self.imageOutput]) {
        [self.captureSession addOutput:self.imageOutput];
    }

      // movie輸出
    self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];             

    if ([self.captureSession canAddOutput:self.movieOutput]) {
        [self.captureSession addOutput:self.movieOutput];
    }

    return YES;
}
  • 第二: 啟動和停止會話
- (void)startSession {
    if (![self.captureSession isRunning]) {                                 
        dispatch_async([self globalQueue], ^{
            [self.captureSession startRunning];
        });
    }
}

- (void)stopSession {
    if ([self.captureSession isRunning]) {                                  
        dispatch_async([self globalQueue], ^{
            [self.captureSession stopRunning];
        });
    }
}
// 開始和停止會話都是同步耗時操作,所以采用異步執(zhí)行.
- (dispatch_queue_t)globalQueue {
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}

iOS 8之后使用硬件設(shè)備都需要用戶給予權(quán)限,使用前都要判斷下. 例如:

AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) {
  NSLog(@"相機權(quán)限受限!");
}

2. 細節(jié)部分.

2.1 捕捉設(shè)備的修改與配置

  • 獲取攝像頭,切換攝像頭.
// 當前捕捉會話對應(yīng)的攝像頭.
- (AVCaptureDevice *)activeCamera {                                         
    return self.activeVideoInput.device;
}
// 當前未激活的攝像頭 
- (AVCaptureDevice *)inactiveCamera {                                      
    AVCaptureDevice *device = nil;
    if (self.cameraCount > 1) {
        if ([self activeCamera].position == AVCaptureDevicePositionBack) {  
            device = [self cameraWithPosition:AVCaptureDevicePositionFront];
        } else {
            device = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }
    }
    return device;
}
// 切換攝像頭
- (BOOL)switchCameras {

    if (![self canSwitchCameras]) {                                         
        return NO;
    }

    // 1. 獲取當前未使用的攝像頭,并為他創(chuàng)建一個新的Input.
    NSError *error;
    AVCaptureDevice *videoDevice = [self inactiveCamera];                   
    AVCaptureDeviceInput *videoInput =
    [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    // 2. 用新的Input替換掉正在激活的Input
    if (videoInput) {
        [self.captureSession beginConfiguration];    // 原子性配置開始.保證線程安全

        [self.captureSession removeInput:self.activeVideoInput];            

        if ([self.captureSession canAddInput:videoInput]) {                 
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        } else { // 確保安全,如果新的Input不能添加,繼續(xù)使用舊的
            [self.captureSession addInput:self.activeVideoInput];
        }

        [self.captureSession commitConfiguration];    // 原子性配置完成

    } else {    // 錯誤處理
        [self.delegate deviceConfigurationFailedWithError:error];           
        return NO;
    }

    return YES;
}

- (BOOL)canSwitchCameras {                                                  
    return self.cameraCount > 1;
}

- (NSUInteger)cameraCount {                                                 
    return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}

// 獲取指定位置的AVCaptureDevice
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position { 
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device in devices) {  
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}
  • 調(diào)整焦距和曝光
    修改AVCaptureDevice配置設(shè)備時,一定要先測試修改動作是否被硬件設(shè)備支持. 比如前置攝像頭不支持對焦操作.嘗試一個不被支持的修改動作就會導(dǎo)致程序崩潰. 所以任何修改都要先通過判斷一下.
/* 調(diào)整對焦 */
- (void)focusAtPoint:(CGPoint)point {                                      
    AVCaptureDevice *device = self.activeVideoInput.device;

    if (device.isFocusPointOfInterestSupported &&                          
        [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {     // 判斷設(shè)備是否支持興趣點對焦和自動對焦模式.

        NSError *error;
        if ([device lockForConfiguration:&error]) {    // 鎖定設(shè)備,準備配置修改. 之后修改完釋放鎖定.
            device.focusPointOfInterest = point;
            device.focusMode = AVCaptureFocusModeAutoFocus;

            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}


/* 
調(diào)整曝光 
曝光模式定義了四種方式: 
    AVCaptureExposureModeLocked                            鎖定
    AVCaptureExposureModeAutoExpose                    調(diào)整一次并鎖定
    AVCaptureExposureModeContinuousAutoExposure            一直自動調(diào)整
    AVCaptureExposureModeCustom                 
大部分設(shè)備是根據(jù)環(huán)境自動調(diào)整, 現(xiàn)在我們做調(diào)整曝光度并鎖定.  但在iOS中并不支持AVCaptureExposureModeAutoExpose, 所以我們要用其他定義來實現(xiàn).這個功能
*/
static const NSString *THCameraAdjustingExposureContext;
- (void)exposeAtPoint:(CGPoint)point {

    AVCaptureDevice *device = self.activeVideoInput.device;

    AVCaptureExposureMode exposureMode =
    AVCaptureExposureModeContinuousAutoExposure;

    if (device.isExposurePointOfInterestSupported &&
        [device isExposureModeSupported:exposureMode]) {    // 判斷設(shè)備是否支持AutoExposure模式

        NSError *error;
        if ([device lockForConfiguration:&error]) {    // 鎖定設(shè)備,準備配置修改. 之后修改完釋放鎖定.

            device.exposurePointOfInterest = point;
            device.exposureMode = exposureMode;

            // 判斷設(shè)備是否支持鎖定曝光設(shè)置, 如果支持,使用KVO來觀察設(shè)備"adjustingExposure"屬性狀態(tài). 然后在曝光調(diào)整完成時在該點上鎖定曝光.
            if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                [device addObserver:self                                    
                         forKeyPath:@"adjustingExposure"
                            options:NSKeyValueObservingOptionNew
                            context:&THCameraAdjustingExposureContext];
            }

            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    if (context == &THCameraAdjustingExposureContext) {    
    // 這里通過測試Context是否為&THCameraAdjustingExposureContext指針,來判斷監(jiān)聽到的回調(diào)是否對應(yīng)我們期望的變更操作.

        AVCaptureDevice *device = (AVCaptureDevice *)object;
        if (!device.isAdjustingExposure &&                                  
            [device isExposureModeSupported:AVCaptureExposureModeLocked]) {

            [object removeObserver:self                                     
                        forKeyPath:@"adjustingExposure"
                           context:&THCameraAdjustingExposureContext];
            // 回到主隊列設(shè)置exposureMode 
            dispatch_async(dispatch_get_main_queue(), ^{                    
                NSError *error;
                if ([device lockForConfiguration:&error]) {
                    device.exposureMode = AVCaptureExposureModeLocked;
                    [device unlockForConfiguration];
                } else {
                    [self.delegate deviceConfigurationFailedWithError:error];
                }
            });
        }

    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

/* 恢復(fù)對焦和曝光設(shè)置 */
- (void)resetFocusAndExposureModes {

    AVCaptureDevice *device = self.activeVideoInput.device;

    AVCaptureExposureMode exposureMode =
    AVCaptureExposureModeContinuousAutoExposure;

    AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;

    BOOL canResetFocus = [device isFocusPointOfInterestSupported] &&        
    [device isFocusModeSupported:focusMode];

    BOOL canResetExposure = [device isExposurePointOfInterestSupported] &&  
    [device isExposureModeSupported:exposureMode];

    CGPoint centerPoint = CGPointMake(0.5f, 0.5f);    // 創(chuàng)建一個中心掃描點.

    NSError *error;
    if ([device lockForConfiguration:&error]) {

        if (canResetFocus) {    // 焦點可以重置
            device.focusMode = focusMode;
            device.focusPointOfInterest = centerPoint;
        }

        if (canResetExposure) {    // 曝光可以充值
            device.exposureMode = exposureMode;
            device.exposurePointOfInterest = centerPoint;
        }
        
        [device unlockForConfiguration];
        
    } else {
        [self.delegate deviceConfigurationFailedWithError:error];
    }
}
  • 調(diào)整閃光燈和手電筒模式(Flash和 Torch 模式)
    AVCaptureDevice可以控制攝像頭的LED燈,當拍照時是做閃光燈,當拍視頻時做手電筒.
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {

    AVCaptureDevice *device =  self.activeVideoInput.device;

    if (device.flashMode != flashMode &&
        [device isFlashModeSupported:flashMode]) {

        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.flashMode = flashMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}


- (void)setTorchMode:(AVCaptureTorchMode)torchMode {

    AVCaptureDevice *device = self.activeVideoInput.device;

    if (device.torchMode != torchMode &&
        [device isTorchModeSupported:torchMode]) {

        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.torchMode = torchMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

3. 基本功能實現(xiàn)-拍照和攝像

  • 拍照
    上面介紹了基本流程,如果要拍照就是希望捕捉會話的輸出Output實例是AVCaptureStillImageOutput類型,這需要在第一步配置會話時添加上去. 而關(guān)于接下來具體拍攝方法就定義在AVCaptureStillImageOutput中. 代碼如下:
- (void)captureStillImage {
    // 獲取Output對象當前使用的AVCaptureConnection連接.
    // 原始相機只支持垂直方向,所以當設(shè)備旋轉(zhuǎn)時,用戶界面保持不變.我們希望橫向時相應(yīng)調(diào)整圖片方向.
    AVCaptureConnection *connection =                                   
        [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];
        // 這里要先確定Connection 是否支持設(shè)置視頻方向. 然后設(shè)置圖片方向.
    if (connection.isVideoOrientationSupported) {    
        connection.videoOrientation = [self currentVideoOrientation];
    }
    

    id handler = ^(CMSampleBufferRef sampleBuffer, NSError *error) {
        if (sampleBuffer != NULL) {    
            NSData *imageData =
                [AVCaptureStillImageOutput
                    jpegStillImageNSDataRepresentation:sampleBuffer];
           // 得到的照片
            UIImage *image = [[UIImage alloc] initWithData:imageData];
        } else {
            NSLog(@"NULL sampleBuffer: %@", [error localizedDescription]);
        }
    };
    // 拍攝靜態(tài)圖片
    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection
                                                  completionHandler:handler];
}
  • 視頻
    同拍照流程一樣,設(shè)置捕捉會話的輸出Output實例是 AVCaptureMovieFileOutput類型. 具體攝像代碼如下:
- (void)startRecording {

    if (!self.movieOutput.isRecording) { // 是否在錄制中
        // 設(shè)置錄制視頻方向為當前方向
        AVCaptureConnection *videoConnection =                              
            [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        if ([videoConnection isVideoOrientationSupported]) {                
            videoConnection.videoOrientation = self.currentVideoOrientation;
        }
        // 設(shè)置VideoStabilization.可以提高視頻質(zhì)量.
        if ([videoConnection isVideoStabilizationSupported]) {              
            if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.0) {
                videoConnection.enablesVideoStabilizationWhenAvailable = YES;
            } else {
                videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
            }
        }

        AVCaptureDevice *device = self.activeVideoInput.device;
        // 設(shè)置平滑對焦模式,提升移動錄制質(zhì)量
        if (device.isSmoothAutoFocusSupported) {                            
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.smoothAutoFocusEnabled = NO;
                [device unlockForConfiguration];
            } else {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
        }

        NSURL *outputURL = [self uniqueURL];   // 定義視頻文件唯一輸出路徑.        
        // 最后,開始錄制. 通過AVCaptureFileOutputRecordingDelegate控制錄制過程.
        [self.movieOutput startRecordingToOutputFileURL:outputURL      
                                      recordingDelegate:self];

    }
}

AVCaptureMovieFileOutput支持分段捕捉.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末登下,一起剝皮案震驚了整個濱河市匠童,隨后出現(xiàn)的幾起案子巩趁,更是在濱河造成了極大的恐慌栈幸,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嵌牺,死亡現(xiàn)場離奇詭異打洼,居然都是意外死亡,警方通過查閱死者的電腦和手機逆粹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進店門募疮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人僻弹,你說我怎么就攤上這事阿浓。” “怎么了蹋绽?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵芭毙,是天一觀的道長筋蓖。 經(jīng)常有香客問我,道長稿蹲,這世上最難降的妖魔是什么扭勉? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任鹊奖,我火速辦了婚禮苛聘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘忠聚。我一直安慰自己设哗,他們只是感情好,可當我...
    茶點故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布两蟀。 她就那樣靜靜地躺著网梢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赂毯。 梳的紋絲不亂的頭發(fā)上战虏,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天,我揣著相機與錄音党涕,去河邊找鬼烦感。 笑死,一個胖子當著我的面吹牛膛堤,可吹牛的內(nèi)容都是我干的手趣。 我是一名探鬼主播,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼肥荔,長吁一口氣:“原來是場噩夢啊……” “哼绿渣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起燕耿,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤中符,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后誉帅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舟茶,經(jīng)...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年堵第,在試婚紗的時候發(fā)現(xiàn)自己被綠了吧凉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,687評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡踏志,死狀恐怖阀捅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情针余,我是刑警寧澤饲鄙,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布凄诞,位于F島的核電站,受9級特大地震影響忍级,放射性物質(zhì)發(fā)生泄漏帆谍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一轴咱、第九天 我趴在偏房一處隱蔽的房頂上張望汛蝙。 院中可真熱鬧,春花似錦朴肺、人聲如沸窖剑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽西土。三九已至,卻和暖如春鞍盗,著一層夾襖步出監(jiān)牢的瞬間需了,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工般甲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肋乍,地道東北人。 一個月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓欣除,卻偏偏與公主長得像住拭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子历帚,可洞房花燭夜當晚...
    茶點故事閱讀 43,576評論 2 349

推薦閱讀更多精彩內(nèi)容