1. 程式人生 > >AVFoundation開發祕籍筆記-06捕捉媒體

AVFoundation開發祕籍筆記-06捕捉媒體

一、捕捉功能

1、捕捉會話 AVCaptureSession

AVFoundation捕捉棧的核心類是AVCaptureSession。一個捕捉會話相當於一個虛擬的“插線板”,用於連線輸入和輸出的資源。

捕捉會話管理從屋裡裝置得到的資料流,比如攝像頭和麥克風裝置,輸出到一個或多個目的地。可以動態配置輸入和輸出的線路,可以再會話進行中按需配置捕捉環境。

捕捉會話還可以額外配置一個會話預設值(session preset),用來控制捕捉資料的格式和質量。會話預設值預設為AVCaptureSessionPresetHigh,適用於大多數情況。還有很多預設值,可以根據需求設定。

2、捕捉裝置 AVCaptureDevice

AVCaptureDevice為攝像頭或麥克風等物理裝置定義了一個介面。對硬體裝置定義了大量的控制方法,如對焦、曝光、白平衡和閃光燈等。

AVCaptureDevice定義大量類方法用用訪問系統的捕捉裝置,最常用的是defaultDeviceWithMediaType:,根據給定的媒體型別返回一個系統指定的預設裝置

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];請求的是一個預設的視訊裝置,在包含前置和後置攝像頭的iOS系統,返回後置攝像頭。

3、捕捉裝置的輸入 AVCaptureInput

AVCaptureInput是一個抽象類,提供一個連線介面將捕獲到的輸入源連線到AVCaptureSession

抽象類無法直接使用,只能通過其子類滿足需求:AVCaptureDeviceInput-使用該物件從AVCaptureDevice獲取裝置資料(攝像頭、麥克風等)、AVCaptureScreenInput-通過螢幕獲取資料(如錄屏)、AVCaptureMetaDataInput-獲取元資料

  • 以 AVCaptureDeviceInput 為例

使用捕捉裝置進行處理前,需要將它新增為捕捉會話的輸入。通過將裝置(AVCaptureDevice)封裝到AVCaptureDeviceInput

例項中,實現將裝置插入到AVCaptureSession中。

AVCaptureDeviceInput在裝置輸出資料和捕捉會話間,扮演接線板的作用。

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];

4、捕捉的輸出 AVCaptureOutput

AVCaptureOutput是一個抽象基類,用於從捕捉會話得到的資料尋找輸出目的地。

框架定義一些這個基類的高階擴充套件類,比如
AVCaptureStillImageOutput用來捕捉靜態圖片,AVCaptureMovieFileOutput捕捉視訊

還有一些底層擴充套件,如AVCaptureAudioDataOutputAVCaptureVideoDataOutput使用它們可以直接訪問硬體捕捉到的數字樣本。使用底層輸出類需要對捕捉裝置的資料渲染有更好的理解,不過這些類可以提供更強大的功能,比如對音訊和視訊流進行實時處理。

5、捕捉連線 AVCaptureConnection

AVCaptureConnection 連線

捕捉會話首先確定有給定捕捉裝置輸入渲染的媒體型別,並自動建立其到能夠接收該媒體型別的捕捉輸出端的連線。

對連線的訪問可以對訊號流進行底層的空值,比如禁用某些特定的連線,或者再音訊連線中訪問單獨的音訊軌道(一些高階用法,不糾結)。

  • 附加AVCaptureConnection解決一個影象旋轉90°的問題:(setVideoOrientation:方法)
AVCaptureConnection *stillImageConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
AVCaptureVideoOrientation  avcaptureOrientation = [self avOrientationForDeviceOrientation:UIDeviceOrientationPortrait];

[stillImageConnection setVideoOrientation:avcaptureOrientation];

6、捕捉預覽 AVCaptureVideoPreviewLayer

AVCaptureVideoPreviewLayer是一個CoreAnimationCALayer的子類,對捕捉視訊資料進行實時預覽。

類似於AVPlayerLayer,不過針對攝像頭捕捉的需求進行了定製。他也支援視訊重力概念setVideoGravity:

  • AVLayerVideoGravityResizeAspect –在承載層範圍內縮放視訊大小來保持視訊原始寬高比,預設值,適用於大部分情況
  • AVLayerVideoGravityResizeAspectFill –保留視訊寬高比,通過縮放填滿層的範圍區域,會導致視訊圖片被部分裁剪。
  • AVLayerVideoGravityResize –拉伸視訊內容拼配承載層的範圍,會導致圖片扭曲,funhouse effect效應。

二、建立簡單捕捉會話

當如庫檔案 #import <AVFoundation/AVFoundation.h>

  • 1、建立捕捉會話 AVCaptureSession,可以設定為成員變數,開始會話以及停止會話都是用到例項物件。
    AVCaptureSession *session = [[AVCaptureSession alloc] init];

  • 2、建立獲取捕捉裝置 AVCaptureDevice
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

  • 3、建立捕捉輸入 AVCaptureDeviceInput

NSError *error;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
  • 4、將捕捉輸入加到會話中
if ([session canAddInput:input]) {
    //首先檢測是否能夠新增輸入,直接新增可能會有crash
    [session addInput:input];
}
  • 5、建立一個靜態圖片輸出 AVCaptureStillImageOutput

AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];

  • 6、將捕捉輸出新增到會話中
if ([session canAddOutput:imageOutput]) {
    //檢測是否可以新增輸出
    [session addOutput:imageOutput];
}
  • 7、建立影象預覽層AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
previewLayer.frame = self.view.frame;
[self.view.layer addSublayer:previewLayer];
  • 8、開始會話

[session startRunning];

開始之前先獲取裝置攝像頭許可權。info.plist中新增Privacy - Camera Usage Description

這裡只是實現捕捉流程,梳理核心元件的關係,沒有任何操作。典型的會話建立過程會更復雜,這是毋庸置疑的。當開始執行會話,視訊資料流就可以再系統中傳輸。

三、建立一個簡單的拍照視訊專案

整個的邏輯依舊是上面的幾步,更多的是一些新的屬性設定,因為是簡單專案,所以,只是實現了功能,並沒有作具體的優化。怎麼簡單怎麼來,主要是熟悉一下主要功能。

1、建立捕捉會話

專案裡不只是要實現靜態圖片捕捉,還會有視訊拍攝,所以還有視訊和音訊輸入。

就是前面說的【建立簡單會話】流程的升級版,可以同時給會話新增多個輸入和多個輸出,然後分別單獨處理。

self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;

    //獲取裝置攝像頭
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    // 得到一個指向預設視訊捕捉裝置的指標。

    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
    //將裝置新增到Session之前,先封裝到AVCaptureDeviceInput物件

    if (videoInput) {
        if ([self.captureSession canAddInput:videoInput]) {
            [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]) {
            //對於有效的input,新增到會話並給它傳遞捕捉裝置的輸入資訊
            [self.captureSession addInput:audioInput];
        }
    } else {
        return NO ;
    }

    //設定 靜態圖片輸出
    self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];

    self.stillImageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
    //配置字典表示希望捕捉JPEG格式圖片

    if ([self.captureSession canAddOutput:self.stillImageOutput]) {
        // 測試輸出是否可以新增到捕捉對話,然後再新增
        [self.captureSession addOutput:self.stillImageOutput];
    }


    //設定視訊檔案輸出

    self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];

    if ([self.captureSession canAddOutput:self.movieOutput]) {
        [self.captureSession addOutput:self.movieOutput];
        NSLog(@"add movie output success");
    }

2、開始和結束會話

- (dispatch_queue_t)globalQueue {
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}

//開始捕捉會話
- (void)startSession {
    if (![self.captureSession isRunning]) {
        dispatch_async([self globalQueue], ^{
            //開始會話 同步呼叫會消耗一定時間,所以用非同步方式在videoQueue排隊呼叫該方法,不會阻塞主執行緒。
            [self.captureSession startRunning];
        });
    }
}

//停止捕捉會話
- (void)stopSession {
    if ([self.captureSession isRunning]) {
        dispatch_async([self globalQueue], ^{
            [self.captureSession stopRunning];
        });
    }
}

3、切換攝像頭

切換前置和後置攝像頭需要重新配置捕捉回話,可以動態重新配置AVCaptureSession,不必擔心停止會話和重新啟動會話帶來的開銷。
對會話進行的任何改變,都要通beginConfigurationcommitConfiguration,進行單獨的、原子性的變化。

- (BOOL)switchCameras { //驗證是否有可切換的攝像頭
    if (![self canSwitchCameras]) {
        return NO;
    }
    NSError *error;
    AVCaptureDevice *videoDevice = [self inactiveCamera];

    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    if (videoInput) {
        [self.captureSession beginConfiguration];
        // 標註源自配置變化的開始

        [self.captureSession removeInput:self.activeVideoInput];
        if ([self.captureSession canAddInput:videoInput]) {
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        } else if (self.activeVideoInput) {
            [self.captureSession addInput:self.activeVideoInput];
        }
        [self.captureSession commitConfiguration];
    } else {
        [self.delegate deviceConfigurationFailedWithError:error];       
        return NO;
    }
    return YES;
}
// 返回指定位置的AVCaptureDevice 有效位置為 AVCaptureDevicePositionFront 和AVCaptureDevicePositionBack,遍歷可用視訊裝置,並返回position引數對應的值
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device  in devices) {
        if (device.position  == position) {
            return device;
        }
    }
    return nil;
}

// 當前捕捉會話對應的攝像頭,返回啟用的捕捉裝置輸入的device屬性
- (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)canSwitchCameras {
    return self.cameraCount > 1;
}

// 返回可用視訊捕捉裝置的數量
- (NSUInteger)cameraCount {
    return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}

4、捕獲靜態圖片

AVCaptureConnection,當建立一個會話並新增捕捉裝置輸入和捕捉輸出時,會話自動建立輸入和輸出的連結,按需選擇訊號流線路。訪問這些連線,可以更好地對傳送到輸出端的資料進行控制。

CMSampleBuffer是有CoreMedia框架定義的CoreFoundation物件。可以用來儲存捕捉到的圖片資料。圖片格式根據輸出物件設定的格式決定。

- (void)captureStillImage {
    NSLog(@"still Image");
    AVCaptureConnection *connection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
    if (connection.isVideoOrientationSupported) {
        connection.videoOrientation = [self currentVideoOrientation];
    }
    id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error) {
        if (sampleBuffer != NULL) {
            NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
            UIImage *image = [UIImage imageWithData:imageData];
            //這就得到了拍攝到的圖片,可以做響應處理。


        } else {
            NSLog(@"NULL sampleBuffer :%@",[error localizedDescription]);
        }
    };
    [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];
}

處理圖片方向問題。

- (AVCaptureVideoOrientation)currentVideoOrientation {
    AVCaptureVideoOrientation orientation;

    switch ([[UIDevice currentDevice] orientation]) {
        case UIDeviceOrientationPortrait:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationLandscapeRight:
            orientation = AVCaptureVideoOrientationLandscapeLeft;
            break;
        case UIDeviceOrientationPortraitUpsideDown:
            orientation = AVCaptureVideoOrientationPortraitUpsideDown;
            break;

        default:
            orientation = AVCaptureVideoOrientationLandscapeRight;
            break;
    }

    return orientation;
}

5、錄製視訊

視訊內容捕捉,設定捕捉會話,新增名為AVCaptureMovieFileOutput的輸出。將QuickTime影片捕捉大磁碟,這個類的大多數核心功能繼承與超類AVCaptureFileOutput

通常當QuickTime應聘準備釋出時,影片頭的元資料處於檔案的開始位置,有利於視訊播放器快速讀取頭包含的資訊。錄製的過程中,知道所有的樣本都完成捕捉後才能建立資訊頭。

- (void)startRecording {
    if (![self isRecording]) {

        AVCaptureConnection *videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        if ([videoConnection isVideoOrientationSupported]) {
            videoConnection.videoOrientation = [self currentVideoOrientation];
        }
        if ([videoConnection isVideoStabilizationSupported]) {
            videoConnection.preferredVideoStabilizationMode = YES;

        }

        //如果支援preferredVideoStabilizationMode,設定為YES。支援視訊穩定可以顯著提升捕捉到的視訊質量。
        // 只在錄製視訊檔案時才會涉及。

        AVCaptureDevice *device = [self activeCamera];
        if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            } else {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
            //攝像頭可以進行平滑對焦模式的操作,減慢攝像頭鏡頭對焦的速度。
            //通常情況下,使用者移動拍攝時攝像頭會嘗試快速自動對焦,這會在捕捉視訊中出現脈衝式效果。
            //當平滑對焦時,會較低對焦操作的速率,從而提供更加自然的視訊錄製效果。
        }

        self.outputURL = [self uniqueURL];
        NSLog(@"url %@",self.outputURL);
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
        // 查詢寫入捕捉視訊的唯一檔案系統URL。保持對地址的強引用,這個地址在後面處理視訊時會用到
        // 新增代理,處理回撥結果。

    }
}

// 獲取錄製時間
- (CMTime)recordedDuration {
    return self.movieOutput.recordedDuration;
}

// 設定儲存路徑
- (NSURL *)uniqueURL {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *directionPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"camera_movie"];

    NSLog(@"unique url :%@",directionPath);
    if (![fileManager fileExistsAtPath:directionPath]) {
        [fileManager createDirectoryAtPath:directionPath withIntermediateDirectories:YES attributes:nil error:nil];
    }

    NSString *filePath = [directionPath stringByAppendingPathComponent:@"camera_movie.mov"];
    if ([fileManager fileExistsAtPath:filePath]) {
        [fileManager removeItemAtPath:filePath error:nil];
    }
    return [NSURL fileURLWithPath:filePath];

    return nil;
}

// 停止錄製
- (void)stopRecording {
    if ([self isRecording]) {
        [self.movieOutput stopRecording];
    }
}

// 驗證錄製狀態
- (BOOL)isRecording {
    return self.movieOutput.isRecording;
}

代理回撥,拿到錄製視訊的地址。

#pragma mark -- AVCaptureFileOutputRecordingDelegate

// 錄製完成
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
{
    NSLog(@"capture output");
    if (error) {
        NSLog(@"record error :%@",error);
        [self.delegate mediaCaptureFailedWithError:error];
    } else {
        // 沒有錯誤的話在儲存響應的路徑下已經完成視訊錄製,可以通過url訪問該檔案。

    }
    self.outputURL = nil;
}

6、將圖片和視訊儲存到相簿

將拍攝到的圖片和視訊可以通過這個系統庫儲存到相簿。

不過AssetsLibrary在iOS9.0之後就被棄用了,可以使用從iOS8.0支援的Photos/Photos.h庫來實現圖片和視訊的儲存。

- (void)writeImageToAssetsLibrary:(UIImage *)image {
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    [library writeImageToSavedPhotosAlbum:image.CGImage orientation:(NSInteger)image.imageOrientation completionBlock:^(NSURL *assetURL, NSError *error) {
        if (!error) {

        } else {
            NSLog(@"Error :%@",[error localizedDescription]);
        }

    }];

}
- (void)writeVideoToAssetsLibrary:(NSURL *)videoUrl {
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoUrl]) {
        //檢驗是否可以寫入

        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
        completionBlock = ^(NSURL *assetURL, NSError *error) {
            if (error) {
                [self.delegate asssetLibraryWriteFailedWithError:error];
            } else {

            }
        };
        [library writeVideoAtPathToSavedPhotosAlbum:videoUrl completionBlock:completionBlock];

    }
}

Photos/Photos.h實現圖片和視訊儲存

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"success :%d ,error :%@",success,error);
        if (success) {
                // DO: 

        }
    }];
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoUrl];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
    if (success) {
        // DO:
        [self generateThumbnailForVideoAtURL:videoUrl];
    } else {
        [self.delegate asssetLibraryWriteFailedWithError:error];
        NSLog(@"video save error :%@",error);
    }
}];

7、關於閃光燈和手電筒的設定

裝置後面的LED燈,當拍攝靜態圖片時作為閃光燈,當拍攝視訊時用作連續燈光(手電筒).捕捉裝置的flashMode和torchMode。

  • AVCapture(Flash|Torch)ModeAuto:基於周圍環境光照情況自動關閉或開啟
  • AVCapture(Flash|Torch)ModeOff:總是關閉
  • AVCapture(Flash|Torch)ModeOn:總是開啟

修改閃光燈或手電筒設定的時候,一定要先鎖定裝置再修改,否則會掛掉。

- (BOOL)cameraHasFlash {
    return [[self activeCamera] hasFlash];
}

- (AVCaptureFlashMode)flashMode {
    return [[self activeCamera] flashMode];
}

- (void)setFlashMode:(AVCaptureFlashMode)flashMode {
    AVCaptureDevice *device = [self activeCamera];
    if ([device isFlashModeSupported:flashMode]) {
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.flashMode = flashMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

- (BOOL)cameraHasTorch {
    return [[self activeCamera] hasTorch];
}

- (AVCaptureTorchMode)torchMode {
    return [[self activeCamera] torchMode];
}

- (void)setTorchMode:(AVCaptureTorchMode)torchMode {
    AVCaptureDevice *device = [self activeCamera];
    if ([device isTorchModeSupported:torchMode]) {
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.torchMode = torchMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

8、其他一些設定

還有許多可以設定的屬性,比如聚焦、曝光等等,設定起來差不多,首先要檢測裝置(攝像頭)是否支援相應功能,鎖定裝置,而後設定相關屬性。

再以對焦為例

// 詢問啟用中的攝像頭是否支援興趣點對焦
- (BOOL)cameraSupportsTapToFocus {
    return [[self activeCamera] isFocusPointOfInterestSupported];
}

// 點的座標已經從螢幕座標轉換為捕捉裝置座標。
- (void)focusAtPoint:(CGPoint)point {
    AVCaptureDevice *device = [self activeCamera];
    if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
        // 確認是否支援興趣點對焦並確認是否支援自動對焦模式。
        // 這一模式會使用單獨掃描的自動對焦,並將focusMode設定為AVCaptureFocusModeLocked

        NSError *error;
        if ([device lockForConfiguration:&error]) {
            //鎖定裝置準備配置
            device.focusPointOfInterest = point;
            device.focusMode = AVCaptureFocusModeAutoFocus;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

關於螢幕座標與裝置座標的轉換

captureDevicePointOfInterestForPoint:–獲取螢幕座標系的CGPoint資料,返回轉換得到的裝置座標系CGPoint資料

pointForCaptureDevicePointOfInterest:–獲取社小偷座標系的CGPoint資料,返回轉換得到的螢幕座標系CGPoint資料