I've been attempting to use AVAssetWriter to crop and export a video in an iOS app. Everything has been going swimmingly up until I attempted to add audio to the output. The current code (see below) returns the following error
NSInvalidArgumentException', reason: '* -[AVAssetReader startReading] cannot be called again after reading has already started'
From what I've written below, this should be using a different reader than the one that was initialized, and so I don't understand the issue. This may be a simple issue of needing another pair of eyes on it, so here's the code.
-(void)cropVideoAsset:(NSURL *)fileURL
{
NSString *outputPath = [NSString stringWithFormat:#"%#%#", NSTemporaryDirectory(), #"croppedVideo.mov"];
toUrl = [NSURL fileURLWithPath:outputPath];
if([[NSFileManager defaultManager] fileExistsAtPath:outputPath])
{[[NSFileManager defaultManager] removeItemAtPath:outputPath error:nil];}
AVAsset *asset = [AVAsset assetWithURL:fileURL];
NSLog(#"asset: %f", CMTimeGetSeconds(asset.duration));
NSDictionary* compressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:
AVVideoProfileLevelH264Main31, AVVideoProfileLevelKey,
[NSNumber numberWithInt:2500000], AVVideoAverageBitRateKey,
[NSNumber numberWithInt: 5], AVVideoMaxKeyFrameIntervalKey,
nil];
NSDictionary* videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
AVVideoCodecH264, AVVideoCodecKey,
[NSNumber numberWithInt:500], AVVideoWidthKey,
[NSNumber numberWithInt:500], AVVideoHeightKey,
AVVideoScalingModeResizeAspectFill, AVVideoScalingModeKey,
compressionSettings, AVVideoCompressionPropertiesKey,
nil];
AVAssetWriterInput *writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
writerInput.expectsMediaDataInRealTime = true;
NSError *error;
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:toUrl fileType:AVFileTypeMPEG4 error:&error];
[assetWriter addInput:writerInput];
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error];
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
writerInput.transform = videoTrack.preferredTransform;
NSDictionary *videoOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput *asset_reader_output = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:videoTrack outputSettings:videoOptions];
[assetReader addOutput:asset_reader_output];
//audio setup
NSDictionary *audioSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatMPEG4AAC], AVFormatIDKey,
[NSNumber numberWithInt:1], AVNumberOfChannelsKey,
[NSNumber numberWithFloat:44100.0], AVSampleRateKey,
[NSNumber numberWithInt:64000], AVEncoderBitRateKey,
nil];
AVAssetWriterInput *audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
AVAssetReader *audioReader = [AVAssetReader assetReaderWithAsset:asset error:&error];
AVAssetTrack *audioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
AVAssetReaderOutput *readerOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:nil];
[audioReader addOutput:readerOutput];
audioWriterInput.expectsMediaDataInRealTime = true;
[assetWriter addInput:audioWriterInput];
[assetWriter startWriting];
[assetWriter startSessionAtSourceTime:kCMTimeZero];
[assetReader startReading];
dispatch_queue_t _processingQueue = dispatch_queue_create("assetAudioWriterQueue", NULL);
[writerInput requestMediaDataWhenReadyOnQueue:_processingQueue usingBlock:
^{
while(writerInput.readyForMoreMediaData)
{
switch(assetReader.status)
{
case AVAssetReaderStatusReading:
{
CMSampleBufferRef sampleBuffer = [asset_reader_output copyNextSampleBuffer];
if (sampleBuffer)
{
BOOL result = [writerInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
if (!result)
{
[SVProgressHUD dismiss];
[self enableView];
NSString *errorMessage = [NSString stringWithFormat:#"We're sorry, an error occurred during the video capture.\n\n%#\n\nPlease try again.", assetWriter.error.localizedDescription];
[UIAlertView simpleAlertWithTitle:#"Video Error" andMessage:errorMessage];
[assetReader cancelReading];
break;
}
}
}
break;
case AVAssetReaderStatusCompleted:
{
[self handleAudioWithReader:audioReader writer:assetWriter input:audioWriterInput andOutput:readerOutput];
}
break;
case AVAssetReaderStatusFailed:
[SVProgressHUD dismiss];
[self enableView];
[UIAlertView simpleAlertWithTitle:#"Video Error" andMessage:#"We're sorry, an error occurred during the video capture. Please try again."];
[assetWriter cancelWriting];
break;
}
}
}];
}
and the requisite call
-(void)handleAudioWithReader:(AVAssetReader *)audioReader writer:(AVAssetWriter *)videoWriter input:(AVAssetWriterInput *)audioWriterInput andOutput:(AVAssetReaderOutput *)readerOutput
{
[audioReader startReading];
[videoWriter startSessionAtSourceTime:kCMTimeZero];
dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
[audioWriterInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^
{
NSLog(#"Asset Writer ready :%d", audioWriterInput.readyForMoreMediaData);
while (audioWriterInput.readyForMoreMediaData)
{
CMSampleBufferRef nextBuffer;
if([audioReader status] == AVAssetReaderStatusReading && (nextBuffer = [readerOutput copyNextSampleBuffer]))
{
if (nextBuffer)
{
NSLog(#"NextBuffer");
[audioWriterInput appendSampleBuffer:nextBuffer];
}
}
else
{
[audioWriterInput markAsFinished];
switch ([audioReader status])
{
case AVAssetReaderStatusCompleted:
[videoWriter finishWriting];
[audioWriterInput markAsFinished];
dispatch_async(dispatch_get_main_queue(),^{[self saveAssetToLibraryAtURL:toUrl];});
break;
}
}
}
}];
}
Related
I am able to record a video using AVCaptureSession.But i want to
reduce it size.How can i do this ? I am getting final URL in Delegate
"captureOutput" method.
VideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
VideoInputDevice = [AVCaptureDeviceInput deviceInputWithDevice:VideoDevice error:&error];
if (!error)
{
if ([CaptureSession canAddInput:VideoInputDevice])
{
[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");
}
AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
I hope this method will be helpful for u
- (void)compressingVideoWithSoundWithInputURL:(NSURL*)inputURL outputURL:(NSURL*)outputURL andResolution:(CGSize)resolution
{
self.myVideoWriter = nil;
[[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
//setup video writer
AVAsset *videoAsset = [[AVURLAsset alloc] initWithURL:inputURL options:nil];
AVAssetTrack *videoTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
NSDictionary *videoWriterCompressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1250000], AVVideoAverageBitRateKey, nil];
NSDictionary *videoWriterSettings = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey, videoWriterCompressionSettings, AVVideoCompressionPropertiesKey, [NSNumber numberWithFloat:resolution.width], AVVideoWidthKey, [NSNumber numberWithFloat:resolution.height], AVVideoHeightKey, nil];
AVAssetWriterInput* videoWriterInput = [AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:videoWriterSettings];
videoWriterInput.expectsMediaDataInRealTime = YES;
videoWriterInput.transform = videoTrack.preferredTransform;
NSError* writerError = nil;
self.myVideoWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&writerError];
if (writerError) {
NSLog(#"Writer error: %#", writerError);
NSString *message = [NSString stringWithFormat:#"VideoEditor. compressingVideoWithInputURL: outputURL: andResolution:. Writer error: %#", writerError];
FLog(message);
return;
}
[self.myVideoWriter addInput:videoWriterInput];
//setup video reader
NSDictionary *videoReaderSettings = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput *videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:videoReaderSettings];
AVAssetReader *videoReader = [[AVAssetReader alloc] initWithAsset:videoAsset error:nil];
[videoReader addOutput:videoReaderOutput];
//setup audio writer
AVAssetWriterInput* audioWriterInput = [AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeAudio
outputSettings:nil];
audioWriterInput.expectsMediaDataInRealTime = NO;
[self.myVideoWriter addInput:audioWriterInput];
//setup audio reader
AVAssetTrack* audioTrack = [[videoAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
AVAssetReaderOutput *audioReaderOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:nil];
AVAssetReader *audioReader = [AVAssetReader assetReaderWithAsset:videoAsset error:nil];
[audioReader addOutput:audioReaderOutput];
[self.myVideoWriter startWriting];
//start writing from video reader
[videoReader startReading];
[self.myVideoWriter startSessionAtSourceTime:kCMTimeZero];
dispatch_queue_t processingQueue = dispatch_queue_create("processingQueue1", DISPATCH_QUEUE_SERIAL);
[videoWriterInput requestMediaDataWhenReadyOnQueue:processingQueue usingBlock:
^{
while ([videoWriterInput isReadyForMoreMediaData]) {
CMSampleBufferRef sampleBuffer;
if ([videoReader status] == AVAssetReaderStatusReading &&
(sampleBuffer = [videoReaderOutput copyNextSampleBuffer])) {
[videoWriterInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
else {
[videoWriterInput markAsFinished];
if ([videoReader status] == AVAssetReaderStatusCompleted) {
if ([audioReader status] == AVAssetReaderStatusReading || [audioReader status] == AVAssetReaderStatusCompleted) {
}
else{
//start writing from audio reader
[audioReader startReading];
[self.myVideoWriter startSessionAtSourceTime:kCMTimeZero];
dispatch_queue_t processingQueue = dispatch_queue_create("processingQueue2", NULL);
[audioWriterInput requestMediaDataWhenReadyOnQueue:processingQueue usingBlock:^{
while (audioWriterInput.readyForMoreMediaData) {
CMSampleBufferRef sampleBuffer;
if ([audioReader status] == AVAssetReaderStatusReading &&
(sampleBuffer = [audioReaderOutput copyNextSampleBuffer])) {
[audioWriterInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
else {
[audioWriterInput markAsFinished];
if ([audioReader status] == AVAssetReaderStatusCompleted)
{
[self finishWritingAction];
break;
}
}
}
}
];
}
}
}
}
}
];
}
You want to use an AVAssetExportSession. You code will look something like this:
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:myAsset presetName:exportPreset];
exporter.videoComposition = _videoComposition;
exporter.outputFileType = AVFileTypeQuickTimeMovie;
exporter.shouldOptimizeForNetworkUse = NO;
url = [url URLByAppendingPathExtension:CFBridgingRelease(UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)(exporter.outputFileType), kUTTagClassFilenameExtension))];
exporter.outputURL = url;
[exporter exportAsynchronouslyWithCompletionHandler:^{
// handle the completion in a way meaningful to your app
}];
You export preset should be one of these:
AVF_EXPORT NSString *const AVAssetExportPreset640x480 NS_AVAILABLE(10_7, 4_0);
AVF_EXPORT NSString *const AVAssetExportPreset960x540 NS_AVAILABLE(10_7, 4_0);
AVF_EXPORT NSString *const AVAssetExportPreset1280x720 NS_AVAILABLE(10_7, 4_0);
AVF_EXPORT NSString *const AVAssetExportPreset1920x1080 NS_AVAILABLE(10_7, 5_0);
AVF_EXPORT NSString *const AVAssetExportPreset3840x2160 NS_AVAILABLE(10_10, NA);
I want to record a audio file and upload to server in .wav format, but recorder doesn't allow to me to record a file into wav format.
for recording i have used the code :
NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue:[NSNumber numberWithInt:kAudioFormatMPEG4AAC] forKey:AVFormatIDKey];
[recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];
// Initiate and prepare the recorder
recorder = [[AVAudioRecorder alloc] initWithURL:outputFileURL settings:recordSetting error:nil];
recorder.delegate = self;
recorder.meteringEnabled = YES;
[recorder prepareToRecord];
Another way i have found, convert file from caff to wav after recording but this also not working for me.
I have used the code for convert file from caff to wav:
-(BOOL)exportAssetAsWaveFormat:(NSURL*)filePath
{
NSError *error = nil ;
NSDictionary *audioSetting = [NSDictionary dictionaryWithObjectsAndKeys:
[ NSNumber numberWithFloat:44100.0], AVSampleRateKey,
[ NSNumber numberWithInt:2], AVNumberOfChannelsKey,
[ NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
[ NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
[ NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
[ NSNumber numberWithBool:0], AVLinearPCMIsBigEndianKey,
[ NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
[ NSData data], AVChannelLayoutKey, nil ];
// NSString *audioFilePath = filePath;
AVURLAsset * URLAsset = [[AVURLAsset alloc] initWithURL:recorder.url options:nil];
if (!URLAsset) return NO ;
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:URLAsset error:&error];
if (error) return NO;
NSArray *tracks = [URLAsset tracksWithMediaType:AVMediaTypeAudio];
if (![tracks count]) return NO;
AVAssetReaderAudioMixOutput *audioMixOutput = [AVAssetReaderAudioMixOutput
assetReaderAudioMixOutputWithAudioTracks:tracks
audioSettings :audioSetting];
if (![assetReader canAddOutput:audioMixOutput]) return NO ;
[assetReader addOutput :audioMixOutput];
if (![assetReader startReading]) return NO;
NSString *title = #"WavConverted";
NSArray *docDirs = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDir = [docDirs objectAtIndex: 0];
NSString *outPath = [[docDir stringByAppendingPathComponent :title]
stringByAppendingPathExtension:#"wav" ];
NSURL *outURL = [NSURL fileURLWithPath:outPath];
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outURL
fileType:AVFileTypeWAVE
error:&error];
if (error) return NO;
AVAssetWriterInput *assetWriterInput = [ AVAssetWriterInput assetWriterInputWithMediaType :AVMediaTypeAudio
outputSettings:audioSetting];
assetWriterInput. expectsMediaDataInRealTime = NO;
if (![assetWriter canAddInput:assetWriterInput]) return NO ;
[assetWriter addInput :assetWriterInput];
if (![assetWriter startWriting]) return NO;
[assetWriter startSessionAtSourceTime:kCMTimeZero ];
dispatch_queue_t queue = dispatch_queue_create( "assetWriterQueue", NULL );
[assetWriterInput requestMediaDataWhenReadyOnQueue:queue usingBlock:^{
NSLog(#"start");
while (1)
{
if ([assetWriterInput isReadyForMoreMediaData]) {
CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
if (sampleBuffer) {
[assetWriterInput appendSampleBuffer :sampleBuffer];
CFRelease(sampleBuffer);
} else {
[assetWriterInput markAsFinished];
break;
}
}
}
[assetWriter finishWriting];
NSLog(#"finish %#",assetWriter);
}];
return YES;
// dispatch_release(queue);
}
Thanks in advance.
Any help would be really appreciated.
You are uploading the file to a server, but I recommend you to not use the wav format because it's bigger in size than any other formats. Use caf, m4a, or any other formats instead.
You can record a wav file by setting AVFormatIDKey to kAudioFormatLinearPCM; no other encoding format will work.
NSURL *url = [NSURL fileURLWithPath: outPath];
NSError *err = nil;
audioRecorder = [[ AVAudioRecorder alloc] initWithURL:url
settings: audioSetting
error:&err];
//prepare to record
[audioRecorder setDelegate:self];
[audioRecorder prepareToRecord];
audioRecorder.meteringEnabled = YES;
[audioRecorder recordForDuration:(NSTimeInterval)10000000000];
[audioRecorder record];
Try this
NSString *wavFilePath = [[NSBundle mainBundle] pathForResource:#"sampleaudio" ofType:#"wav"];
NSURL *assetURL = [NSURL fileURLWithPath:wavFilePath];
AVURLAsset *songAsset = [AVURLAsset URLAssetWithURL:assetURL options:nil];
NSError *assetError = nil;
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:songAsset
error:&assetError]
;
if (assetError) {
NSLog (#"error: %#", assetError);
return;
}
AVAssetReaderOutput *assetReaderOutput = [AVAssetReaderAudioMixOutput
assetReaderAudioMixOutputWithAudioTracks:songAsset.tracks
audioSettings: nil];
if (! [assetReader canAddOutput: assetReaderOutput]) {
NSLog (#"can't add reader output... die!");
return;
}
[assetReader addOutput: assetReaderOutput];
NSString *strcafFileName = [NSString stringWithFormat:#"%#.caf",[wavFilePath stringByDeletingPathExtension]];
NSString *cafFilePath = [delegate.strCassettePathSide stringByAppendingPathComponent:strcafFileName];
NSURL *exportURL = [NSURL fileURLWithPath:cafFilePath];
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:exportURL
fileType:AVFileTypeCoreAudioFormat
error:&assetError];
if (assetError)
{
NSLog (#"error: %#", assetError);
return;
}
AudioChannelLayout channelLayout;
memset(&channelLayout, 0, sizeof(AudioChannelLayout));
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
[NSNumber numberWithFloat:11025], AVSampleRateKey,
[NSNumber numberWithInt:2], AVNumberOfChannelsKey,
[NSData dataWithBytes:&channelLayout length:sizeof(AudioChannelLayout)], AVChannelLayoutKey,
[NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
[NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
[NSNumber numberWithBool:NO],AVLinearPCMIsFloatKey,
[NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
nil];
AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
outputSettings:outputSettings];
if ([assetWriter canAddInput:assetWriterInput])
{
[assetWriter addInput:assetWriterInput];
}
else
{
NSLog(#"can't add asset writer input... die!");
return;
}
assetWriterInput.expectsMediaDataInRealTime = NO;
[assetWriter startWriting];
[assetReader startReading];
AVAssetTrack *soundTrack = [songAsset.tracks objectAtIndex:0];
CMTime startTime = CMTimeMake (0, soundTrack.naturalTimeScale);
[assetWriter startSessionAtSourceTime: startTime];
__block UInt64 convertedByteCount = 0;
dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
[assetWriterInput requestMediaDataWhenReadyOnQueue:mediaInputQueue
usingBlock: ^
{
while (assetWriterInput.readyForMoreMediaData)
{
CMSampleBufferRef nextBuffer = [assetReaderOutput copyNextSampleBuffer];
if (nextBuffer)
{
// append buffer
[assetWriterInput appendSampleBuffer: nextBuffer];
convertedByteCount += CMSampleBufferGetTotalSampleSize (nextBuffer);
CMSampleBufferInvalidate(nextBuffer);
CFRelease(nextBuffer);
nextBuffer = NULL;
}
else
{
[assetWriterInput markAsFinished];
// [assetWriter finishWriting];
[assetReader cancelReading];
break;
}
}
}];
I'm attempting to combine a number of video files into a single file with specific codec settings. I used to use the AVAssetExportSession for this, but I now need more control over the codec than the AVAssetExportSession offers.
Below I've posted the createFinalVideo: function that handles the combination of the video files.
The approach I've taken is attempting to write to the same file with an AVAssetWriter while simply starting the session at the location where the next video should be appended. I know this will not work because the AVAssetWriter apparently doesn't allow for this behavior.
Previously, I had the AVAssetWriter defined outside of the for loop, and I was attempting to add a new input for each video file (each pass of the for loop). However, it appears that AVAssetWriter doesn't allow for adding new inputs after [AVAssetWriter startWriting] has been called.
My question is how do I do what I'm trying to do the right way?
/**
* Final video creation. Merges audio-only and video-only files.
**/
-(void)createFinalVideo:(id)args
{
ENSURE_SINGLE_ARG(args, NSDictionary);
// presentation id
NSString * presID = [args objectForKey:#"presID"];
// array of video paths
NSArray * videoPathsArray = [args objectForKey:#"videoPaths"];
videoSuccessCallback = [args objectForKey:#"videoSuccess"];
videoCancelCallback = [args objectForKey:#"videoCancel"];
videoErrorCallback = [args objectForKey:#"videoError"];
NSError * error = nil;
NSFileManager * fileMgr = [NSFileManager defaultManager];
NSString * bundleDirectory = [[NSBundle mainBundle] bundlePath];
NSString * documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents"];
/*********************************************************************/
/* BEGIN: merge all videos into a final MP4 */
/*********************************************************************/
// create the final video output file as MP4 file
NSString * finalOutputFilePath = [NSString stringWithFormat:#"%#/%#/final_video.mp4", documentsDirectory, presID];
NSURL * finalOutputFileUrl = [NSURL fileURLWithPath:finalOutputFilePath];
// delete file if it exists
if ([fileMgr fileExistsAtPath:finalOutputFilePath]) {
[fileMgr removeItemAtPath:finalOutputFilePath error:nil];
}
float renderWidth = 640, renderHeight = 480;
NSDictionary *videoCleanApertureSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:renderWidth], AVVideoCleanApertureWidthKey,
[NSNumber numberWithInt:renderHeight], AVVideoCleanApertureHeightKey,
[NSNumber numberWithInt:10], AVVideoCleanApertureHorizontalOffsetKey,
[NSNumber numberWithInt:10], AVVideoCleanApertureVerticalOffsetKey,
nil];
NSDictionary *codecSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:1960000], AVVideoAverageBitRateKey,
[NSNumber numberWithInt:24],AVVideoMaxKeyFrameIntervalKey,
videoCleanApertureSettings, AVVideoCleanApertureKey,
AVVideoProfileLevelH264Baseline30, AVVideoProfileLevelKey,
nil];
NSDictionary *videoCompressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:
AVVideoCodecH264, AVVideoCodecKey,
codecSettings,AVVideoCompressionPropertiesKey,
[NSNumber numberWithInt:renderWidth], AVVideoWidthKey,
[NSNumber numberWithInt:renderHeight], AVVideoHeightKey,
AVVideoScalingModeResizeAspect, AVVideoScalingModeKey,
nil];
NSError *aerror = nil;
// next start time for adding to the compositions
CMTime nextStartTime = kCMTimeZero;
// loop through the video paths and add videos to the composition
for (NSString * path in videoPathsArray) {
// wait for each video to finish writing before continuing
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// create video writer
AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:finalOutputFileUrl fileType:AVFileTypeQuickTimeMovie error:nil];
NSParameterAssert(videoWriter);
NSLog(#"at the top of the for loop");
NSLog(#"%#", path);
AVAssetWriterInput* videoWriterInput = [AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:videoCompressionSettings];
NSParameterAssert(videoWriterInput);
NSParameterAssert([videoWriter canAddInput:videoWriterInput]);
videoWriterInput.expectsMediaDataInRealTime = YES;
[videoWriter addInput:videoWriterInput];
AVAssetWriterInput* audioWriterInput = [AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeAudio
outputSettings:nil];
NSParameterAssert(audioWriterInput);
NSParameterAssert([videoWriter canAddInput:audioWriterInput]);
audioWriterInput.expectsMediaDataInRealTime = NO;
[videoWriter addInput:audioWriterInput];
[videoWriter startWriting];
// video setup
AVAsset *avAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil];
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:avAsset error:&aerror];
AVAssetTrack *videoTrack = [[avAsset tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:0];
CMTime videoDuration = avAsset.duration;
// Wait until the duration is actually available
int durationAttempts = 5;
while(CMTimeGetSeconds(videoDuration) == 0 && durationAttempts > 0) {
durationAttempts--;
[NSThread sleepForTimeInterval:0.3];
videoDuration = avAsset.duration;
}
NSLog(#"[INFO] MODULE-VIDUTILS video duration in secs: %f", CMTimeGetSeconds(videoDuration));
//videoWriterInput.transform = videoTrack.preferredTransform;
NSDictionary *videoOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput *asset_reader_output = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:videoOptions];
[reader addOutput:asset_reader_output];
//audio setup
AVAssetReader *audioReader = [AVAssetReader assetReaderWithAsset:avAsset error:nil];
AVAssetTrack* audioTrack = [[avAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
AVAssetReaderOutput *readerOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:nil];
[audioReader addOutput:readerOutput];
NSLog(#"startSessionAtSourceTime: %f", CMTimeGetSeconds(nextStartTime));
[videoWriter startSessionAtSourceTime:nextStartTime];
// set next start time
nextStartTime = CMTimeAdd(nextStartTime, videoDuration);
[reader startReading];
dispatch_queue_t _processingQueue = dispatch_queue_create("AVAssetWriterQueue", DISPATCH_QUEUE_SERIAL);
[videoWriterInput requestMediaDataWhenReadyOnQueue:_processingQueue usingBlock:^{
while ([videoWriterInput isReadyForMoreMediaData]) {
CMSampleBufferRef sampleBuffer;
if ([reader status] == AVAssetReaderStatusReading &&
(sampleBuffer = [asset_reader_output copyNextSampleBuffer])) {
BOOL result = [videoWriterInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
if (!result) {
[reader cancelReading];
NSLog(#"NO RESULT");
NSLog (#"[INFO] MODULE-VIDUTILS createFinalVideo AVAssetWriterInputStatusFailed: %#", videoWriter.error);
if (videoErrorCallback != nil) {
[self _fireEventToListener:#"videoError" withObject:nil listener:videoErrorCallback thisObject:nil];
}
return;
break;
}
} else {
[videoWriterInput markAsFinished];
switch ([reader status]) {
case AVAssetReaderStatusReading:
// the reader has more for other tracks, even if this one is done
break;
case AVAssetReaderStatusCompleted:
[audioReader startReading];
[videoWriter startSessionAtSourceTime:nextStartTime];
NSLog(#"Request");
NSLog(#"Asset Writer ready :%d", audioWriterInput.readyForMoreMediaData);
while (audioWriterInput.readyForMoreMediaData) {
CMSampleBufferRef nextBuffer;
if ([audioReader status] == AVAssetReaderStatusReading && (nextBuffer = [readerOutput copyNextSampleBuffer])) {
NSLog(#"Ready");
if (nextBuffer) {
NSLog(#"NextBuffer");
[audioWriterInput appendSampleBuffer:nextBuffer];
}
} else {
[audioWriterInput markAsFinished];
//dictionary to hold duration
if ([audioReader status] == AVAssetReaderStatusCompleted) {
NSLog (#"[INFO] MODULE-VIDUTILS createFinalVideo AVAssetReaderStatusCompleted");
[videoWriter finishWritingWithCompletionHandler:^{
switch([videoWriter status]) {
case AVAssetWriterStatusCompleted:
NSLog (#"[INFO] MODULE-VIDUTILS createFinalVideo AVAssetWriterStatusCompleted");
dispatch_semaphore_signal(semaphore);
break;
case AVAssetWriterStatusCancelled:
NSLog (#"[INFO] MODULE-VIDUTILS createFinalVideo AVAssetWriterStatusCancelled");
if (videoSuccessCallback != nil) {
[self _fireEventToListener:#"videoCancel" withObject:nil listener:videoCancelCallback thisObject:nil];
}
return;
break;
case AVAssetWriterStatusFailed:
NSLog (#"[INFO] MODULE-VIDUTILS createFinalVideo AVAssetWriterStatusFailed");
if (videoSuccessCallback != nil) {
[self _fireEventToListener:#"videoError" withObject:nil listener:videoErrorCallback thisObject:nil];
}
return;
break;
}
}];
break;
}
}
}
break;
case AVAssetReaderStatusFailed:
NSLog (#"[INFO] MODULE-VIDUTILS createFinalVideo AVAssetReaderStatusFailed, #%", reader.error);
if (videoSuccessCallback != nil) {
[self _fireEventToListener:#"videoError" withObject:nil listener:videoErrorCallback thisObject:nil];
}
[videoWriter cancelWriting];
return;
break;
}
break;
}
}
}];
// wait for the writing to finish
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(#"Write Ended");
}
NSLog(#"got here -- should have waited for all videos to complete first");
// call success if we got here
if (videoSuccessCallback != nil) {
[self _fireEventToListener:#"videoSuccess" withObject:nil listener:videoSuccessCallback thisObject:nil];
}
}
I found a replacement for AVAssetExportSession called SDAVAssetExportSession that allows you to specify settings instead of using presets.
I am attempting to use a combination of the sample provided for encoding asset writer and the sample provided from pixelBufferFromCGImage to overlay a UIImage atop a AVAsset that I'm exporting.
The problem is, despite the True result of this call
[adaptor appendPixelBuffer:buffer withPresentationTime:kCMTimeZero];
The exported avasset is corrupted, yeilding an unexpected size and a subsequent access to it failing with 'This media may be damaged'.
The export itself, if I avoid attempting to use the appendPixelBuffer call, is successful as desired. However, locating it before the dispatch queue or within it yeilds the same failure.
Hopefully not redundantly posting here, but other samples here in stack overflow don't seem to address this particular combination failure. Thanks, code below
Export code:
AVAsset *sourceAsset = [AVAsset assetWithURL:outputUrl];
NSError *error = nil;
NSString *fileName = [NSString stringWithFormat:#"non_transform_%f.mov", [[NSDate date] timeIntervalSince1970]];
NSString *combinedPath = [NSString stringWithFormat:#"file://localhost%#/%#", [[GroupDiskManager sharedManager] getFolderPath], fileName];
NSURL *outputURL = [NSURL URLWithString:combinedPath];
NSLog(#"combined path: %#", combinedPath);
AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&error];
AVAssetTrack *videoTrack = [[sourceAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
AVAssetTrack *audioTrack = [[sourceAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
AVVideoCodecH264, AVVideoCodecKey,
[NSNumber numberWithInt:1280], AVVideoWidthKey,
[NSNumber numberWithInt:720], AVVideoHeightKey,
nil];
AVAssetWriterInput* videoWriterInput = [[AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:videoSettings] retain];
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
[attributes setObject:[NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(NSString*)kCVPixelBufferPixelFormatTypeKey];
[attributes setObject:[NSNumber numberWithUnsignedInt:1280] forKey:(NSString*)kCVPixelBufferWidthKey];
[attributes setObject:[NSNumber numberWithUnsignedInt:720] forKey:(NSString*)kCVPixelBufferHeightKey];
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput
sourcePixelBufferAttributes:attributes];
NSParameterAssert(videoWriterInput);
NSParameterAssert([videoWriter canAddInput:videoWriterInput]);
videoWriterInput.expectsMediaDataInRealTime = NO;
[videoWriter addInput:videoWriterInput];
NSError *aerror = nil;
NSDictionary *videoOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput *asset_reader_output = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:videoOptions];
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:sourceAsset error:&aerror];
[reader addOutput:asset_reader_output];
AVAssetWriterInput* audioWriterInput = [[AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeAudio
outputSettings:nil] retain];
AVAssetReader *audioReader = [[AVAssetReader assetReaderWithAsset:sourceAsset error:&error] retain];
AVAssetReaderOutput *audioReaderOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:nil];
[audioReader addOutput:audioReaderOutput];
NSParameterAssert(audioWriterInput);
NSParameterAssert([videoWriter canAddInput:audioWriterInput]);
audioWriterInput.expectsMediaDataInRealTime = YES;
[videoWriter addInput:audioWriterInput];
[videoWriter startWriting];
[videoWriter startSessionAtSourceTime:kCMTimeZero];
[reader startReading];
CVPixelBufferRef buffer = [ImageToMovieManager pixelBufferFromCGImage:[UIImage imageNamed:#"234_1280x720_3.jpg"].CGImage size:CGSizeMake(1280, 720)];
BOOL theResult = [adaptor appendPixelBuffer:buffer withPresentationTime:kCMTimeZero];
if (theResult == NO) //failes on 3GS, but works on iphone 4
NSLog(#"failed to append buffer");
if(buffer) {
CVBufferRelease(buffer);
}
dispatch_queue_t _processingQueue = dispatch_queue_create("_processingQueue", NULL);
[videoWriterInput requestMediaDataWhenReadyOnQueue:_processingQueue usingBlock:
^{
NSLog(#"requestMediaDataWhenReadyOnQueue");
[self retain];
while ([videoWriterInput isReadyForMoreMediaData]) {
CMSampleBufferRef sampleBuffer;
if ([reader status] == AVAssetReaderStatusReading &&
(sampleBuffer = [asset_reader_output copyNextSampleBuffer])) {
BOOL result = [videoWriterInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
if (!result) {
NSLog(#" result == nil Cancel!");
NSLog(#"videoWriter.error: %#", videoWriter.error);
[reader cancelReading];
break;
}
} else {
NSLog(#"[videoWriterInput markAsFinished]");
[videoWriterInput markAsFinished];
switch ([reader status]) {
case AVAssetReaderStatusReading:
NSLog(#"reading");
// the reader has more for other tracks, even if this one is done
break;
case AVAssetReaderStatusCompleted:
NSLog(#"AVAssetReaderStatusCompleted");
[audioReader startReading];
dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
[audioWriterInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^
{
while (audioWriterInput.readyForMoreMediaData) {
CMSampleBufferRef nextBuffer;
if ([audioReader status] == AVAssetReaderStatusReading &&
(nextBuffer = [audioReaderOutput copyNextSampleBuffer])) {
if (nextBuffer) {
[audioWriterInput appendSampleBuffer:nextBuffer];
}
}else{
[audioWriterInput markAsFinished];
switch ([audioReader status]) {
case AVAssetReaderStatusCompleted:
NSLog(#"AVAssetReaderStatusCompleted!!");
[videoWriter finishWriting];
[VideoManager videoSavedWithURL:outputURL withError:(NSError *)error];
break;
}
}
}
}
];
break;
case AVAssetReaderStatusFailed:
NSLog(#"AVAssetReaderStatusFailed");
[videoWriter cancelWriting];
break;
}
break;
}
}
}
];
pixelBufferFromCGImageCode
CGSize frameSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image));
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:NO], kCVPixelBufferCGBitmapContextCompatibilityKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameSize.width,
frameSize.height, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, (CFDictionaryRef) options,
&pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, frameSize.width,
frameSize.height, 8, 4*frameSize.width, rgbColorSpace,
kCGImageAlphaNoneSkipLast);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
At the very least, the pixelFormat should be specified as kCVPixelFormatType_32BGRA not kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange.
I'm trying to take a local m4a or mp3 file and compress/down-sample this file (for the purposes of making a smaller file).
Originally, I was using the AVAssetExportSession to export an AVAsset to a temp directory, but I didn't have any control over compression/down-sampling (you can only use presets, which of them, only .wav file formats support quality degradation).
Then, following several examples here on SO, I tried using AVAssetReader/AVAssetWriter to preform this 'export'.
I create my reader/writer as such:
NSString *exportPath = [NSHomeDirectory() stringByAppendingPathComponent:#"out.m4a"];
NSURL *exportURL = [NSURL fileURLWithPath:outPath];
// reader
NSError *readerError = nil;
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:asset
error:&readerError];
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
AVAssetReaderTrackOutput *readerOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track
outputSettings:nil];
[reader addOutput:readerOutput];
// writer
NSError *writerError = nil;
AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:exportURL
fileType:AVFileTypeAppleM4A
error:&writerError];
AudioChannelLayout channelLayout;
memset(&channelLayout, 0, sizeof(AudioChannelLayout));
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
// use different values to affect the downsampling/compression
NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: kAudioFormatMPEG4AAC], AVFormatIDKey,
[NSNumber numberWithFloat:44100.0], AVSampleRateKey,
[NSNumber numberWithInt:2], AVNumberOfChannelsKey,
[NSNumber numberWithInt:128000], AVEncoderBitRateKey,
[NSData dataWithBytes:&channelLayout length:sizeof(AudioChannelLayout)], AVChannelLayoutKey,
nil];
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio
outputSettings:outputSettings];
[writerInput setExpectsMediaDataInRealTime:NO];
[writer addInput:writerInput];
And then I start writing...
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
[reader startReading];
dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
[writerInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^{
NSLog(#"Asset Writer ready : %d", writerInput.readyForMoreMediaData);
while (writerInput.readyForMoreMediaData) {
CMSampleBufferRef nextBuffer;
if ([reader status] == AVAssetReaderStatusReading && (nextBuffer = [readerOutput copyNextSampleBuffer])) {
if (nextBuffer) {
NSLog(#"Adding buffer");
[writerInput appendSampleBuffer:nextBuffer];
}
} else {
[writerInput markAsFinished];
switch ([reader status]) {
case AVAssetReaderStatusReading:
break;
case AVAssetReaderStatusFailed:
[writer cancelWriting];
break;
case AVAssetReaderStatusCompleted:
NSLog(#"Writer completed");
[writer endSessionAtSourceTime:asset.duration];
[writer finishWriting];
NSData *data = [NSData dataWithContentsOfFile:exportPath];
NSLog(#"Data: %#", data);
break;
}
break;
}
}
}];
When I'm done writing, the data i've supposedly written to the exportURL is null, and the writer reports a successful completion. Any ideas what might be going wrong?
Update The writer status after calling appendSampleBuffer: is AVAssetWriterStatusFailed, though the readers status is successful, and seems to read through the entire file.
I came across the solution:
Using NSHomeDirectory() in NSString *exportPath = [NSHomeDirectory() stringByAppendingPathComponent:#"out.m4a"] was causing the writer to not be able to create the file. Not exactly sure why, or what I would need to do to allow this to work, but changing NSHomeDirectiory() to NSTemporaryDirectory() has solved my problems in the meantime.