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

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

【iOS】iPhoneからMP4ファイルの解像度やビットレートなどを変更して書き出す方法

こんにちは、andyです。
今回は、予め用意したMP4のファイルをiPhoneで読み込み、解像度やビットレートなどの変更を加えて別のMP4ファイルに出力したいという事がありましたので、その方法を書きたいと思います。


一般的にファイルの出力を行う方法として、


exportAsynchronouslyWithCompletionHandler:


メソッドを使用する方法が説明されていますが、この方法だと予め用意されたプリセットのみでしか出力ファイルを作成できないため、例えばSNSなどで要求されているフォーマットに対応できなくなります。という事で、詳細な設定を行うために、


requestMediaDataWhenReadyOnQueue:usingBlock:


メソッドを使用します。


今回は、はじめにMP4ファイルを用意いただき、Resoursesフォルダに追加しておいてください。

コード

それではコードから。
まず、ヘッダーファイルです。

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

@interface ExportMovieViewController : UIViewController

@end



次に実装コードです。

#import "ExportMovieViewController.h"

@interface ExportMovieViewController ()
{
    AVAssetWriter* videoWriter;
    int writeFrames;
    NSMutableDictionary* presets;
}

@end

@implementation ExportMovieViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
//解説-1
    presets = [NSMutableDictionary dictionary];
    [presets setObject:[NSNumber numberWithFloat:480.0f] forKey:@"width"];
    [presets setObject:[NSNumber numberWithFloat:480.0f] forKey:@"height"];
    [presets setObject:@"MP4" forKey:@"video_format"];
    [presets setObject:[NSNumber numberWithFloat:1500000.0f] forKey:@"video_bitrate"];
    [presets setObject:[NSNumber numberWithFloat:30.00f] forKey:@"framerate"];
	
    AVMutableComposition* comp = [self makeComposition];
    if (comp) {
        [self exportWithComposition:comp];
    }
    
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

//解説-2
- (AVMutableComposition*)makeComposition
{
    AVMutableComposition* comp = [AVMutableComposition composition];
    AVMutableCompositionTrack* compVideoTrack = [comp addMutableTrackWithMediaType:AVMediaTypeVideo
                                                                  preferredTrackID:kCMPersistentTrackID_Invalid];

    NSBundle* bundle = [NSBundle mainBundle];
    NSString* path = [bundle pathForResource:@"[Resourcesフォルダ内のファイル名]" ofType:@"mp4"];
    AVURLAsset* asset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:path]];

    AVAssetTrack* videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
    [compVideoTrack insertTimeRange:videoTrack.timeRange
                            ofTrack:videoTrack
                             atTime:kCMTimeZero error:nil];
    
    return comp;
}

//解説-3
- (NSDictionary*)getVideoCompressionSettings
{
    NSDictionary *videoCleanApertureSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                                [NSNumber numberWithFloat:[[presets objectForKey:@"width"] floatValue]], AVVideoCleanApertureWidthKey,
                                                [NSNumber numberWithFloat:[[presets objectForKey:@"height"] floatValue]], AVVideoCleanApertureHeightKey,
                                                [NSNumber numberWithInt:10], AVVideoCleanApertureHorizontalOffsetKey,
                                                [NSNumber numberWithInt:10], AVVideoCleanApertureVerticalOffsetKey,
                                                nil];
    
    
    NSDictionary *codecSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                   [NSNumber numberWithFloat:[[presets objectForKey:@"video_bitrate"] floatValue]], AVVideoAverageBitRateKey,
                                   [NSNumber numberWithFloat:[[presets objectForKey:@"framerate"] floatValue]],AVVideoMaxKeyFrameIntervalKey,
                                   videoCleanApertureSettings, AVVideoCleanApertureKey,
                                   nil];
    
    
    
    NSDictionary *videoCompressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                              AVVideoCodecH264, AVVideoCodecKey,
                                              codecSettings,AVVideoCompressionPropertiesKey,
                                              [NSNumber numberWithFloat:[[presets objectForKey:@"width"] floatValue]], AVVideoWidthKey,
                                              [NSNumber numberWithFloat:[[presets objectForKey:@"height"] floatValue]], AVVideoHeightKey,
                                              nil];
    return videoCompressionSettings;
}

- (void)exportWithComposition:(AVMutableComposition*)comp
{
    NSError *error = nil;
    NSString *exportPath = [NSHomeDirectory() stringByAppendingPathComponent:@"tmp/temp.mov"];
    NSURL *exportUrl = [NSURL fileURLWithPath:exportPath];
    
    if ([[NSFileManager defaultManager] fileExistsAtPath:exportPath])
    {
        [[NSFileManager defaultManager] removeItemAtPath:exportPath error:nil];
    }
    
//解説-4
    videoWriter = [[AVAssetWriter alloc] initWithURL:exportUrl fileType:AVFileTypeQuickTimeMovie error:&error];
    
    AVAssetWriterInput* videoWriterInput = [AVAssetWriterInput
                                            assetWriterInputWithMediaType:AVMediaTypeVideo
                                            outputSettings:[self getVideoCompressionSettings]];
    
    videoWriterInput.expectsMediaDataInRealTime = YES;
    [videoWriter addInput:videoWriterInput];

//解説-5
    NSError *aerror = nil;
    AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:comp error:&aerror];
    AVAssetTrack *videoTrack = [[comp tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:0];
    
    videoWriterInput.transform = videoTrack.preferredTransform;
    NSDictionary *videoOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];

    AVAssetReaderTrackOutput *readerVideoOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:videoOptions];
    
    [reader addOutput:readerVideoOutput];

    [videoWriter startWriting];
    [videoWriter startSessionAtSourceTime:kCMTimeZero];
    [reader startReading];
    
    writeFrames = 0;

    dispatch_queue_t _processingQueue = dispatch_queue_create("assetVideoWriterQueue", NULL);
    
    __weak AVAssetWriterInput* weakWriterInput = videoWriterInput;
    __weak AVAssetWriter* weakWriter = videoWriter;
    [videoWriterInput requestMediaDataWhenReadyOnQueue:_processingQueue usingBlock:
     ^{
         bool isError = NO;

//解説-6
         while ([weakWriterInput isReadyForMoreMediaData]){
             CMSampleBufferRef videoSampleBuffer = [readerVideoOutput copyNextSampleBuffer];
             if (videoSampleBuffer) {
                 CMSampleBufferRef newSampleBuffer = [self offsetTimmingWithSampleBufferForVideo:videoSampleBuffer];
                 BOOL result = [weakWriterInput appendSampleBuffer:newSampleBuffer];
                 
                 if (!result) {
                     [reader cancelReading];
                     NSLog(@"NO RESULT");
                     NSLog(@"videoWriter.error: %@", weakWriter.error);
                     isError = YES;
                     break;
                 }
                 CFRelease(videoSampleBuffer);
                 CFRelease(newSampleBuffer);
                 writeFrames++;
             } else {
                 [weakWriterInput markAsFinished];
                 break;
             }
         }
 
//解説-7
         while (1) {
             if ([reader status] == AVAssetReaderStatusCompleted) {
                 if (!isError) {
                     dispatch_async(dispatch_get_main_queue(), ^(){
                         NSLog(@"AVAssetReaderStatusCompleted");
                         [self videoWriterFinish];
                     });
                 }
                 break;
             }
         }
     }];
}

- (void)videoWriterFinish
{
    [videoWriter finishWritingWithCompletionHandler:^(){
        NSString *exportPath = [NSHomeDirectory() stringByAppendingPathComponent:@"tmp/temp.mov"];
        NSURL *exportUrl = [NSURL fileURLWithPath:exportPath];
        
        ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
        [library writeVideoAtPathToSavedPhotosAlbum:exportUrl
                                                completionBlock:^(NSURL *assetURL, NSError *assetError) {
                                                    
                                                    if (assetError) {
                                                        NSLog(@"export error!!!!");
                                                    } else {
                                                        NSLog(@"export finished!!");
                                                    }
                                                    
                                                    NSFileManager *manager = [NSFileManager defaultManager];
                                                    if ([manager fileExistsAtPath:assetURL.absoluteString isDirectory:NO]) {
                                                        [manager removeItemAtPath:assetURL.absoluteString error:nil];
                                                    }
                                                    
                                                    
                                                }];
    }];
}

- (CMSampleBufferRef)offsetTimmingWithSampleBufferForVideo:(CMSampleBufferRef)sampleBuffer
{
    CMSampleBufferRef newSampleBuffer;
    float framerate = [[presets objectForKey:@"framerate"] floatValue];
    CMSampleTimingInfo sampleTimingInfo;
    sampleTimingInfo.duration = CMTimeMake(100, framerate * 100);
    sampleTimingInfo.presentationTimeStamp = CMTimeMake((writeFrames + 0) * 100, framerate * 100);
    sampleTimingInfo.decodeTimeStamp = kCMTimeInvalid;
    
    CMSampleBufferCreateCopyWithNewTiming(kCFAllocatorDefault,
                                          sampleBuffer,
                                          1,
                                          &sampleTimingInfo,
                                          &newSampleBuffer);
    
    return newSampleBuffer;
}

@end


コード解説

それでは解説です。

解説−1

この部分が最終的に出力されるMP4ファイルの設定値です。
解像度の部分は、オリジナルファイルの縦横比と比率が異なる場合には、画像が変形されますの注意が必要です。予めAVMutableCompositionを作成する段階でクロップなどの処理を行ってから出力する必要があります。
ビットレートの部分は、設定されたビットレートよりも少し小さくなるようです。これはアベレージ値のためかもしれません。
フレームレートの部分は、オリジナルファイルのフレームレートと異なる場合に再生されるムービーの長さが変化します。これは、1フレームあたりの再生時間を変更しているためです。

解説-2

リソースからAVMutableCompositionを作成しています。
予め複数の動画をつなげて出力したい場合などは、この部分にコードを追加していきます。

解説-3

AVAssetWriterでファイル出力する際のビデオの設定を作成しています。先ほど解説-1で設定した値を使用しています。

解説-4

AVAssetWriterを作成し、AVAssetWriterInputを追加しています。AVAssetWriterInputでは、先ほど解説-3で説明した設定を使用しています。

解説-5

この部分で、ファイルからデータを読み出すためのAVAssetReaderを作成しています。解説-2で作成したAVMutableCompositionからビデオトラックを取り出し、AVAssetReaderTrackOutputを作成しています。

解説-6

この部分でビデオをバッファリングしています。ここで前回のブログで紹介したCMSampleBufferRefのタイムスタンプ差し替えを行っています。始めはこの処理を行わずに出力していたのですが、その方法だとフレームレートが設定した値になりませんでした。タイムスタンプ上のtimeScale値を表示させてみると、最初のフレームで1、最終のフレームで0という値が返ってきていたためにそれが原因のようです。通常この値はフレームレートの値になります。

解説-7

AVAssetReaderの作業が終了した時点でPhotoAlbumに作成したファイルを追加しています。


という感じになります。この方法ではフレームレートを変更した場合にオリジナルのムービーの長さを維持できません。これを維持しようとすると、かなり高度な方法で映像を補完しなければなりませんので、また考えが浮かんだら作ってみたいと思います。
簡易的なスローを作成する場合には、使用できるのではないかと思います。


今回はここまで。
次回はまだ内容を考えていませんが、アプリを作っている最中に見つけた事を書きたいと思います。


それでは。

コメントをどうぞ

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


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

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

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

03-3541-1230

info@nvtrlab.jp

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