【iOS】iPhoneからMP4ファイルの解像度やビットレートなどを変更して書き出す方法
今回は、予め用意したMP4のファイルをiPhoneで読み込み、解像度やビットレートなどの変更を加えて別のMP4ファイルに出力したいという事がありましたので、その方法を書きたいと思います。
一般的にファイルの出力を行う方法として、
exportAsynchronouslyWithCompletionHandler:
メソッドを使用する方法が説明されていますが、この方法だと予め用意されたプリセットのみでしか出力ファイルを作成できないため、例えばSNSなどで要求されているフォーマットに対応できなくなります。という事で、詳細な設定を行うために、
requestMediaDataWhenReadyOnQueue:usingBlock:
メソッドを使用します。
今回は、はじめにMP4ファイルを用意いただき、Resoursesフォルダに追加しておいてください。
コード
それではコードから。
まず、ヘッダーファイルです。
1
2
3
4
5
6
7
|
#import <UIKit/UIKit.h> #import <AVFoundation/AVFoundation.h> #import <AssetsLibrary/AssetsLibrary.h> @interface
ExportMovieViewController : UIViewController @end |
次に実装コードです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
|
#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に作成したファイルを追加しています。
という感じになります。この方法ではフレームレートを変更した場合にオリジナルのムービーの長さを維持できません。これを維持しようとすると、かなり高度な方法で映像を補完しなければなりませんので、また考えが浮かんだら作ってみたいと思います。
簡易的なスローを作成する場合には、使用できるのではないかと思います。
今回はここまで。
次回はまだ内容を考えていませんが、アプリを作っている最中に見つけた事を書きたいと思います