Objective-C
iPhone
iOS

[AVFoundation] Preview with AVCaptureMovieFileOutput

More than 3 years have passed since last update.

AVCaptureVideoPreviewLayerとAVCaptureMovieFileOutputを使ってカメラ映像を見ながら録画する方法をメモ。なんでこんなこと書くかというとAVFoundationではAVCaptureVideoOutputとAVCaptureMovieFileOutputは同時には使えないっぽいので、このPreviewLayerというものを使って同時に見るしかないみたい。

参考:StackOverflow

必要なFramework

  • Foundation
  • AVFoundation
  • CoreMedia
  • CoreVideo
  • AssetLibrary (カメラロールに保存するのなら必要)

必要なクラス

詳しいことはAVFoundationの仕様を読んでもらった方がいいけれど、今回必要なものは以下。

  • AVCaptureSession : InputとOutputの橋渡し的な役割
  • AVCaptureMovieFileOutput : 動画ファイルとして出力するOutput
  • AVCaptureDeviceInput : カメラからの映像を取得するInput
  • AVCaptureVideoPreviewLayer : カメラからの映像をpreviewするlayer

Interface

というわけで対象とするViewControllerの@interfaceはこんな感じ。

ViewController.h
#import <UIKit/UIKit.h>

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import <AVFoundation/AVFoundation.h>

#import <AssetsLibrary/AssetsLibrary.h>     
#define CAPTURE_FRAMES_PER_SECOND       20

@interface ViewController : UIViewController
<AVCaptureFileOutputRecordingDelegate>
{
    BOOL WeAreRecording;

    AVCaptureSession *CaptureSession;
    AVCaptureMovieFileOutput *MovieFileOutput;
    AVCaptureDeviceInput *VideoInputDevice;
}

@property (retain) AVCaptureVideoPreviewLayer *PreviewLayer;

- (void) CameraSetOutputProperties;
- (AVCaptureDevice *) CameraWithPosition:(AVCaptureDevicePosition) Position;
// 録画を始めたり終えたりするイベント
- (IBAction)StartStopButtonPressed:(id)sender;
// Back CameraとFront Cameraを切り替えるやつ
- (IBAction)CameraToggleButtonPressed:(id)sender;

@end

Implementation

#import "ViewController.h"

@implementation ViewController

@synthesize PreviewLayer;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // AVCaptureSesstionを作る
    CaptureSession = [[AVCaptureSession alloc] init];

    // カメラデバイスを取得する
    AVCaptureDevice *VideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    if (VideoDevice)
    {
        NSError *error;
        // カメラからの入力を作成する
        VideoInputDevice = [AVCaptureDeviceInput deviceInputWithDevice:VideoDevice error:&error];
        if (!error)
        {
            if ([CaptureSession canAddInput:VideoInputDevice])
                // Sessionに追加
                [CaptureSession addInput:VideoInputDevice];
            else
                NSLog(@"Couldn't add video input");
        }
        else
        {
            NSLog(@"Couldn't create video input");
        }
    }
    else
    {
        NSLog(@"Couldn't create video capture device");
    }

    // 動画録画なのでAudioデバイスも取得する    
    AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    NSError *error = nil;
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioCaptureDevice error:&error];
    if (audioInput)
    {
        // 同じように追加
        // 参考ではこんな感じになってたけど、厳密には上のVideoInputDeviceと同じようにやった方がいいと思う。
        [CaptureSession addInput:audioInput];
    }

    // PreviewLayerを設定する  
    [self setPreviewLayer:[[AVCaptureVideoPreviewLayer alloc] initWithSession:CaptureSession]];

    PreviewLayer.orientation = AVCaptureVideoOrientationPortrait;
    // 引き伸ばし方とか設定。ここではアスペクト比が維持されるが、必要に応じてトリミングされる設定を適用
    [[self PreviewLayer] setVideoGravity:AVLayerVideoGravityResizeAspectFill];


    // ファイル用のOutputを作成
    MovieFileOutput = [[AVCaptureMovieFileOutput alloc] init];

    // 動画の長さ
    Float64 TotalSeconds = 60;
    // 一秒あたりのFrame数
    int32_t preferredTimeScale = 30;    
    // 動画の最大長さ
    CMTime maxDuration = CMTimeMakeWithSeconds(TotalSeconds, preferredTimeScale);   
    MovieFileOutput.maxRecordedDuration = maxDuration;
    // 動画が必要とする容量   
    MovieFileOutput.minFreeDiskSpaceLimit = 1024 * 1024;                        
    // sessionに追加
    if ([CaptureSession canAddOutput:MovieFileOutput])
        [CaptureSession addOutput:MovieFileOutput];

    // CameraDeviceの設定(後述)
    [self CameraSetOutputProperties];       


    // 画像の質を設定。詳しくはドキュメントを読んでください
    [CaptureSession setSessionPreset:AVCaptureSessionPresetMedium];
    if ([CaptureSession canSetSessionPreset:AVCaptureSessionPreset640x480])     //Check size based configs are supported before setting them
        [CaptureSession setSessionPreset:AVCaptureSessionPreset640x480];


    // StoryBoard使えばこんなの要らない?   
    CGRect layerRect = [[[self view] layer] bounds];
    [PreviewLayer setBounds:layerRect];
    [PreviewLayer setPosition:CGPointMake(CGRectGetMidX(layerRect),
                                          CGRectGetMidY(layerRect))];

    // さっきLayerを設定したやつをaddSubviewして貼り付ける
    UIView *CameraView = [[UIView alloc] init];
    [[self view] addSubview:CameraView];
    [self.view sendSubviewToBack:CameraView];

    [[CameraView layer] addSublayer:PreviewLayer];


    // sessionをスタートさせる
    [CaptureSession startRunning];
}

// サポートする画面の向き
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIDeviceOrientationPortrait);
}



- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    WeAreRecording = NO;
}



- (void) CameraSetOutputProperties
{
    // ドキュメントには書いてなかったけど、このConnectionっていうのを貼らないとうまく動いてくれないっぽい
    AVCaptureConnection *CaptureConnection = [MovieFileOutput connectionWithMediaType:AVMediaTypeVideo];

    // Portraitに設定。これはあくまでもカメラ側からファイルへの出力。カメラーロールで再生した時にどの向きであって欲しいかを設定
    if ([CaptureConnection isVideoOrientationSupported])
    {
        AVCaptureVideoOrientation orientation = AVCaptureVideoOrientationLandscapeRight;        
        [CaptureConnection setVideoOrientation:orientation];
    }

    //ここから下はお好みで
    CMTimeShow(CaptureConnection.videoMinFrameDuration);
    CMTimeShow(CaptureConnection.videoMaxFrameDuration);

    if (CaptureConnection.supportsVideoMinFrameDuration)
        CaptureConnection.videoMinFrameDuration = CMTimeMake(1, CAPTURE_FRAMES_PER_SECOND);
    if (CaptureConnection.supportsVideoMaxFrameDuration)
        CaptureConnection.videoMaxFrameDuration = CMTimeMake(1, CAPTURE_FRAMES_PER_SECOND);

    CMTimeShow(CaptureConnection.videoMinFrameDuration);
    CMTimeShow(CaptureConnection.videoMaxFrameDuration);
}


// カメラ切り替えの時に必要
- (AVCaptureDevice *) CameraWithPosition:(AVCaptureDevicePosition) Position
{
    NSArray *Devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *Device in Devices)
    {
        if ([Device position] == Position)
        {
            return Device;
        }
    }
    return nil;
}



// Camera切り替えアクション
- (IBAction)CameraToggleButtonPressed:(id)sender
{
    if ([[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count] > 1)        //Only do if device has multiple cameras
    {
        NSError *error;
        AVCaptureDeviceInput *NewVideoInput;
        AVCaptureDevicePosition position = [[VideoInputDevice device] position];
        // 今が通常カメラなら顔面カメラに
        if (position == AVCaptureDevicePositionBack)
        {
            NewVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self CameraWithPosition:AVCaptureDevicePositionFront] error:&error];
        }
        // 今が顔面カメラなら通常カメラに
        else if (position == AVCaptureDevicePositionFront)
        {
            NewVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self CameraWithPosition:AVCaptureDevicePositionBack] error:&error];
        }

        if (NewVideoInput != nil)
        {
            // beginConfiguration忘れずに!
            [CaptureSession beginConfiguration];            // 一度削除しないとダメっぽい
            [CaptureSession removeInput:VideoInputDevice];
            if ([CaptureSession canAddInput:NewVideoInput])
            {
                [CaptureSession addInput:NewVideoInput];
                VideoInputDevice = NewVideoInput;
            }
            else
            {
                [CaptureSession addInput:VideoInputDevice];
            }

            //Set the connection properties again
            [self CameraSetOutputProperties];


            [CaptureSession commitConfiguration];
        }
    }
}




- (IBAction)StartStopButtonPressed:(id)sender
{

    if (!WeAreRecording)
    {

        WeAreRecording = YES;

        //保存する先のパスを作成
        NSString *outputPath = [[NSString alloc] initWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"];
        NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:outputPath];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        if ([fileManager fileExistsAtPath:outputPath])
        {
            NSError *error;
            if ([fileManager removeItemAtPath:outputPath error:&error] == NO)
            {
                //上書きは基本できないので、あったら削除しないとダメ
            }
        }
        //録画開始
        [MovieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];
    }
    else
    {
        WeAreRecording = NO;

        [MovieFileOutput stopRecording];
    }
}



- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray *)connections
                error:(NSError *)error
{

    BOOL RecordedSuccessfully = YES;
    if ([error code] != noErr)
    {
        // A problem occurred: Find out if the recording was successful.
        id value = [[error userInfo] objectForKey:AVErrorRecordingSuccessfullyFinishedKey];
        if (value)
        {
            RecordedSuccessfully = [value boolValue];
        }
    }
    if (RecordedSuccessfully)
    {
        //書き込んだのは/tmp以下なのでカメラーロールの下に書き出す
        ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
        if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputFileURL])
        {
            [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL
                                        completionBlock:^(NSURL *assetURL, NSError *error)
             {
                 if (error)
                 {

                 }
             }];
        }

    }
}



@end

大事なこと

これをいろいろカスタマイズしたりいじっていて困ったことをいくつか

  • sessionのstartとFileOutputのstartRecordingはsessionのstartを先に行わないとエラーになる。今回はボタンを押して録画開始しているので必ずstartRecordingが後になるけれど、画面表示と同時に録画したいときにはviewDidLoadの中での順番に注意
  • startRecordingはすでにファイルが存在していると上書きとか賢いことはやってくれない。サンプルコードでも上がっているけれど、もし、同じ名前で録画したければ、前のものを消さないとうまくいかない

参考