お使いのブラウザは、バージョンが古すぎます。

このサイトは、Internet Explore8・Internet Explore9には対応しておりません。
恐れ入りますが、お使いのブラウザをバージョンアップしていただきますよう宜しくお願いいたします。

【iOS】 Vineと同じように繋ぎ撮りできるカメラを作る

こんにちは、andyです。


今回は、Twitterに動画を投稿できるアプリ、Vineのカメラ機能にある繋ぎ撮りできるカメラを作ってみたいと思います。今回カメラ部分に関しては、あまり細かく作っていません。繫ぎ撮り部分のみ参考にしてください。


このカメラは、Portrait固定で1080×1920サイズ、映像のみのムービーファイルを作成できるようになっています。ファイルは一時的にサンドボックスに作成された後、PhotoAlbumに保存されますので、確認は写真アプリなどで行ってください。


使い方は簡単。startボタンで録画が開始され、pauseで一時停止、一時停止から再スタートする場合はpauseボタン、録画を終了する場合はstopボタンです。


コードの解説の前に、storyboardで、次のパーツを配置します。
UIImageView *previewWindow
UIButton *start
UIButton *pause
UIButton *stop


配置したらSampleCameraViewControllerと接続しておいてください。
それでは、コードです。


SampleCameraViewController.h

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>

@interface SampleCameraViewController : UIViewController<AVCaptureVideoDataOutputSampleBufferDelegate>

@property (weak, nonatomic) IBOutlet UIImageView *previewWindow;
@property (weak, nonatomic) IBOutlet UIButton *start;
@property (weak, nonatomic) IBOutlet UIButton *pause;
@property (weak, nonatomic) IBOutlet UIButton *stop;

- (IBAction)startCapture;
- (IBAction)pauseCapture;
- (IBAction)endCapture;

@end



SampleCameraViewController.m

#import "SampleCameraViewController.h"

@interface SampleCameraViewController ()
{
    CALayer* previewLayer;
    bool isPause, isRecording, isWritting;
    AVAssetWriter* writer;
    AVAssetWriterInput* videoWriterInput;
    AVCaptureSession *session;
    int writeFrames;
    AVCaptureVideoPreviewLayer* captureVideoPreviewLayer;
}

@end

@implementation SampleCameraViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self makeSession];
    
    if (previewLayer == nil) {
        previewLayer = [self.previewWindow layer];
    }
    
    captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    captureVideoPreviewLayer.frame = previewLayer.bounds;
    
    [previewLayer addSublayer:captureVideoPreviewLayer];
}

- (void)makeSession
{
    session = [[AVCaptureSession alloc] init];
    
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error = nil;
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
    
    [device lockForConfiguration:nil];
    device.activeVideoMinFrameDuration = CMTimeMake(1, 30);
    [device unlockForConfiguration];
    
    [session addInput:videoInput];
    
    AVCaptureVideoDataOutput* videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    [session addOutput:videoDataOutput];
    
    videoDataOutput.videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                     [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],kCVPixelBufferPixelFormatTypeKey,
                                     nil];

//----- 解説-1 -----
    dispatch_queue_t videoQueue = dispatch_queue_create("VideoQueue", DISPATCH_QUEUE_SERIAL);
    [videoDataOutput setSampleBufferDelegate:self queue:videoQueue];
    
    AVCaptureConnection* videoConnection = nil;
    for ( AVCaptureConnection *connection in [videoDataOutput connections] )
    {
        NSLog(@"%@", connection);
        for ( AVCaptureInputPort *port in [connection inputPorts] )
        {
            NSLog(@"%@", port);
            if ( [[port mediaType] isEqual:AVMediaTypeVideo] )
            {
                videoConnection = connection;
            }
        }
    }
    
    if ([videoConnection isVideoOrientationSupported]) {
        [videoConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
        NSLog(@"orientation:%d", [[UIDevice currentDevice] orientation]);
    } else {
        NSLog(@"オリエンテーションできません");
    }
    
    [session startRunning];
}

- (void)removeSession
{
    [session stopRunning];
}

- (void)makeWriter
{
    NSString *pathString = [NSHomeDirectory() stringByAppendingPathComponent:@"tmp/capture.mov"];
    NSURL* exportURL = [NSURL fileURLWithPath:pathString];
    
    if ([[NSFileManager defaultManager] fileExistsAtPath:exportURL.path])
    {
        [[NSFileManager defaultManager] removeItemAtPath:exportURL.path error:nil];
    }
    
    NSError* error;
    writer = [[AVAssetWriter alloc] initWithURL:exportURL
                                       fileType:AVFileTypeQuickTimeMovie
                                          error:&error];
    
    NSDictionary* videoSetting = [NSDictionary dictionaryWithObjectsAndKeys:
                                  AVVideoCodecH264, AVVideoCodecKey,
                                  [NSNumber numberWithInt:1080], AVVideoWidthKey,
                                  [NSNumber numberWithInt:1920], AVVideoHeightKey,
                                  nil];
    videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
                                                          outputSettings:videoSetting];
    videoWriterInput.expectsMediaDataInRealTime = YES;
    
    [writer addInput:videoWriterInput];
}


- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

//----- 解説-2 -----
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    if ((isPause) && (isRecording)) { return; }
    if( !CMSampleBufferDataIsReady(sampleBuffer) )
    {
        NSLog( @"sampleBufferの準備ができていません" );
        return;
    }
    
    if( isRecording == YES ) {
        isWritting = YES;
        NSLog(@"recording");
        if( writer.status != AVAssetWriterStatusWriting  ) {
            [writer startWriting];
            
//----- 解説-3 -----
            [writer startSessionAtSourceTime:kCMTimeZero];
        }

        if( [videoWriterInput isReadyForMoreMediaData] ) {
            CFRetain(sampleBuffer);
            CMSampleBufferRef newSampleBuffer = [self offsetTimmingWithSampleBufferForVideo:sampleBuffer];
            [videoWriterInput appendSampleBuffer:newSampleBuffer];
            CFRelease(sampleBuffer);
            CFRelease(newSampleBuffer);
        }
        
//----- 解説-4 -----
        writeFrames++;
        
    } else {
        if( writer.status == AVAssetWriterStatusWriting  ) {
            if (isWritting) {
            [writer finishWritingWithCompletionHandler:^{
                NSLog(@"finish");
                NSString *pathString = [NSHomeDirectory() stringByAppendingPathComponent:@"tmp/capture.mov"];
                NSURL* exportURL = [NSURL fileURLWithPath:pathString];
                ALAssetsLibrary* al = [[ALAssetsLibrary alloc] init];
                __weak id weakSelf = self;
                __weak AVCaptureVideoPreviewLayer* weakPreviewLayer =  captureVideoPreviewLayer;
                [al writeVideoAtPathToSavedPhotosAlbum:exportURL
                                       completionBlock:^(NSURL *assetURL, NSError *assetError) {
                                           if (assetError) {
                                               NSLog(@"export error!!!!");
                                           }
                                           
                                           NSFileManager *manager = [NSFileManager defaultManager];
                                           if ([manager fileExistsAtPath:assetURL.absoluteString isDirectory:NO]) {
                                               [manager removeItemAtPath:assetURL.absoluteString error:nil];
                                           }
                                           [weakSelf removeSession];
                                           writer = nil;
                                           [weakSelf makeSession];
                                           weakPreviewLayer.session = session;
                 }];
            }];
            }
            isWritting = NO;
        }
    }
}

//----- 解説-5 -----
- (CMSampleBufferRef)offsetTimmingWithSampleBufferForVideo:(CMSampleBufferRef)sampleBuffer
{
    CMSampleBufferRef newSampleBuffer;
    CMSampleTimingInfo sampleTimingInfo;
    sampleTimingInfo.duration = CMTimeMake(1, 30);
    sampleTimingInfo.presentationTimeStamp = CMTimeMake(writeFrames, 30);
    sampleTimingInfo.decodeTimeStamp = kCMTimeInvalid;
    
    CMSampleBufferCreateCopyWithNewTiming(kCFAllocatorDefault,
                                          sampleBuffer,
                                          1,
                                          &sampleTimingInfo,
                                          &newSampleBuffer);

    return newSampleBuffer;
}

- (IBAction)startCapture
{
    [self makeWriter];
    isRecording = YES;
    isPause = NO;
    writeFrames = 0;
}

- (IBAction)pauseCapture
{
    if (isPause) {
        isPause = NO;
    } else {
        isPause = YES;
    }
}

- (IBAction)endCapture
{
    isRecording = NO;
}

@end



それでは解説です。


[解説-1]
AV Foundationプログラミングガイドで公開されている動画のキャプチャ方法ではなく、静止画のキャプチャ方法などで解説されているsetSampleBufferDelegate:queue:を使います。この方法でキャプチャされる1枚ずつの画像の時間情報部分を変更します。
詳しくは、AV Foundationプログラミングガイドをみてください。


[解説-2]
setSampleBufferDelegate:queue:を使用しているために、このメソッドでイベントを受け取ります。このメソッドの引数sampleBufferに動画1フレーム分の映像、時間情報などが含まれています。


[解説-3]
この部分がまず重要です。通常は、sampleBufferに既に含まれている時間情報を録画開始時間に設定するのですが、
CMTime startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
[writer startSessionAtSourceTime:startTime];
今回は、
[writer startSessionAtSourceTime:kCMTimeZero];
にしています。これは0から新たにフレームの先頭時間を振り直すためです。そのため、録画スタート後のsampleBufferで渡されるデータのフレーム開始時間をすべて変更する処理が必要になります。


[解説-4]
writeFramesという変数は、[解説-3]で書いたフレームの開始時間をカウントするためのものです。そのため、初期値0から+1ずつ増加していきます。


[解説-5]
このメソッドでsampleBufferの時間情報を書き換えています。
sampleTimingInfo.duration
は、1フレームの長さを示しています。ここでは1/30秒です。
sampleTimingInfo.presentationTimeStamp
は、このフレームの開始時間です。ここではwriteFrames/30です。
後の情報はsampleBufferからもらい、新しいCMSampleBufferRefを作成して戻しています。


意外と映像処理に関する資料が少なくて海外サイトを彷徨いました。この記事を書くのに半日費やしてしまいました。やれやれ。


今回はこれで終わりです。
このソースかなり不完全なので、使用する際には注意してくださいね。あくまで繫ぎ撮りを実現させる方法を書いただけなので。


それではまた。

コメントをどうぞ

メールアドレスは公開されません。* が付いている欄は必須項目です。


お気軽にお問い合わせください。

日本VTR実験室では、お仕事のご依頼、ブログ・コラムのご感想などを受け付けております。
アプリ開発・コンテンツ制作でお困りでしたら、お気軽にご相談ください。
ご連絡お待ちしております。

お問い合わせはこちらから

03-3541-1230

info@nvtrlab.jp

電話受付対応時間:平日AM9:30〜PM6:00