BradLarson / GPUImage

An open source iOS framework for GPU-based image and video processing
http://www.sunsetlakesoftware.com/2012/02/12/introducing-gpuimage-framework
BSD 3-Clause "New" or "Revised" License
20.25k stars 4.61k forks source link

[GPUImageMovieWriter startRecording] causes [AVAssetWriter startWriting] Cannot call method when status is 3' on iOS 8 Beta 2 #1658

Open GQAdonis opened 10 years ago

GQAdonis commented 10 years ago

I have a working application that essentially uses the same code as the SimpleVideoFilter sample except I allow 2 minutes of recording and control the time using NSTimer instead of a GCD "dispatch_after" call.

In addition, the recording does not start right away. The user presses a button, which results in a call to [GPUImageMovieWriter startRecording] after camera capture has already started.

On iOS 7.x, things work perfectly. However, on iOS 8 Beta 2, the recording never actually starts, and the app is shut down with an "NSInternalInconsistencyException" related to the audio sample buffer processing when [AVAssetWriter startWriting] is called.

I know this is a lot of code, but I have listed the code for the class that handles ALL the capture and recording below:


#import "VideoComposition.h"
#import "Utils.h"

@interface VideoComposition() {
    GPUImageView *_imageView;
    GPUImageMovieWriter *_movieWriter;
    NSURL *_fileURL;
    NSString *_name;
    GPUImageFilter *_imageFilter;
    NSString *_thumbnailFileName;
    BOOL _isMultiFile;
    NSMutableArray *_fileParts;

    NSTimer *_currentTimer;
}

@end

@implementation VideoComposition

@synthesize camera = _camera;

-(id) initWithImageView:(GPUImageView*)view name:(NSString *)name {
    self  = [super init];
    if (self) {
        _movieWriter = nil;

        _currentTimer = nil;

        _thumbnailFileName = nil;

        _imageView = view;

        self.isRecording = NO;

        _isMultiFile = NO;

        _fileParts = [NSMutableArray array];

        self.camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
        //_camera.delegate = self;

        _camera.outputImageOrientation = [[UIApplication sharedApplication] statusBarOrientation];
        _camera.horizontallyMirrorFrontFacingCamera = NO;
        _camera.horizontallyMirrorRearFacingCamera = NO;

        _imageFilter = [[GPUImageFilter alloc] init];

        [_camera addTarget:_imageFilter];

        _name = name;
        NSString *fileName = [Utils GetUUID];
        NSString *pathComponent = [NSString stringWithFormat:@"Documents/%@.mp4", fileName];
        NSString *pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:pathComponent];
        self.localFile = [NSString stringWithFormat:@"%@.mp4", fileName];
        unlink([pathToMovie UTF8String]); // If a file already exists, AVAssetWriter won't let you record new frames, so delete the old movie
        _fileURL = [NSURL fileURLWithPath:pathToMovie];

        [self setMovieWriterForCurrentOrientation];

        [_imageFilter addTarget:_imageView];

        [_camera startCameraCapture];
    }
    return self;
}

-(void) setMovieWriterForCurrentOrientation {
    if (self.isRecording) {
        return;
    }

    if (_movieWriter) {
        // remove the current one...
        [_imageFilter removeTarget:_movieWriter];
        _movieWriter = nil;
    }

    if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
        _movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:_fileURL size:CGSizeMake(480.0, 640.0)];
    } else {
        _movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:_fileURL size:CGSizeMake(640.0, 480.0)];
    }
    _movieWriter.encodingLiveVideo = YES;
    [_imageFilter addTarget:_movieWriter];

    /*__block GPUImageFilter *weakFilter = _imageFilter;
    __block GPUImageMovieWriter *weakWriter = _movieWriter;

    [_movieWriter setCompletionBlock:^{
        [weakFilter removeTarget:weakWriter];
        [weakWriter finishRecording];
    }];*/
}

-(NSString*) name {
    return _name;
}

/*-(GPUImageVideoCamera *) camera {
    return _camera;
}*/

-(void) recordingTimerFired:(NSTimer*)timer {
    if (self.isRecording) {
        [self stopRecording];
    }

    if (_currentTimer) {
        [_currentTimer invalidate];
        _currentTimer = nil;
    }
}

-(void) startRecording {
    if (self.isRecording) {
        return;
    }

    [UIApplication sharedApplication].idleTimerDisabled = YES;

    self.isRecording = YES;

    unlink([self.localFile UTF8String]); // If a file already exists, AVAssetWriter won't let you record new frames, so delete the old movie

    double delayToStartRecording = 0.1;
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, delayToStartRecording * NSEC_PER_SEC);
    dispatch_after(startTime, dispatch_get_main_queue(), ^(void){
        NSLog(@"Start recording");

        BOOL resetTorch = _camera.inputCamera.torchActive;

        if (resetTorch && !_camera.inputCamera.torchActive) {
            NSError *error = nil;
            [_camera.inputCamera lockForConfiguration:&error];
            [_camera.inputCamera setTorchMode:AVCaptureTorchModeOn];
            [_camera.inputCamera unlockForConfiguration];
        }

        if (self.delegate && [self.delegate respondsToSelector:@selector(recordingStarted:)]) {
            [self.delegate recordingStarted:self];
        }
        _camera.audioEncodingTarget = _movieWriter;
        [_movieWriter startRecording];

        if (_currentTimer) {
            [_currentTimer invalidate];
            _currentTimer = nil;
        }

        double delayInSeconds = 120.0;

        NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:delayInSeconds];
        _currentTimer = [[NSTimer alloc] initWithFireDate:fireDate interval:0.1 target:self selector:@selector(recordingTimerFired:) userInfo:nil repeats:NO];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addTimer:_currentTimer forMode:NSDefaultRunLoopMode];
    });

}

-(UIImage*)imageCrop:(UIImage*)original
{
    UIImage *ret = nil;

    // This calculates the crop area.

    float originalWidth  = original.size.width;
    float originalHeight = original.size.height;

    float edge = fminf(originalWidth, originalHeight);

    float posX = (originalWidth   - edge) / 2.0f;
    float posY = (originalHeight  - edge) / 2.0f;

    CGRect cropSquare = CGRectMake(posX, posY,
                                   edge, edge);

    if(original.imageOrientation == UIImageOrientationLeft ||
       original.imageOrientation == UIImageOrientationRight)
    {
        cropSquare = CGRectMake(posY, posX,
                                edge, edge);

    }
    else
    {
        cropSquare = CGRectMake(posX, posY,
                                edge, edge);
    }

    // This performs the image cropping.

    CGImageRef imageRef = CGImageCreateWithImageInRect([original CGImage], cropSquare);

    ret = [UIImage imageWithCGImage:imageRef
                              scale:original.scale
                        orientation:original.imageOrientation];

    CGImageRelease(imageRef);

    return ret;
}

-(void) createThumbnail {
    AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:_fileURL options:nil];

    CMTime thumbnailTime = [asset duration];
    thumbnailTime.value = 0;

    AVAssetImageGenerator *generate = [[AVAssetImageGenerator alloc] initWithAsset:asset];
    generate.appliesPreferredTrackTransform = YES;
    NSError *err = NULL;

    CGImageRef imgRef = [generate copyCGImageAtTime:thumbnailTime actualTime:NULL error:&err];
    UIImage *img = [[UIImage alloc] initWithCGImage:imgRef];

    if (img) {
        // make a square...
        img = [self imageCrop:img];

        NSData *imageData = UIImagePNGRepresentation(img);

        NSString *fileName = [Utils GetUUID];
        NSString *pathComponent = [NSString stringWithFormat:@"Documents/%@.png", fileName];
        NSString *fullPath = [NSHomeDirectory() stringByAppendingPathComponent:pathComponent];
        _thumbnailFileName = [NSString stringWithFormat:@"%@.png", fileName];

        unlink([fullPath UTF8String]); // If a file already exists, AVAssetWriter won't let you record new frames, so delete the old movie

        [imageData writeToFile:fullPath atomically:YES];
    }

    CGImageRelease(imgRef);
}

-(void) createMultifileOutput {
    NSError *error = nil;

    // create a NEW file based on the parts...
    AVMutableComposition *composition = [AVMutableComposition composition];
    CMTime insertionPoint = kCMTimeZero;

    for (NSURL *fileUrl in _fileParts) {
        AVURLAsset *asset = [AVURLAsset assetWithURL:fileUrl];

        [composition insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration)
                             ofAsset:asset
                              atTime:insertionPoint
                               error:&error];
        if (error) {
            NSLog(@"Error occurred inserting time range: %@, Duration: %f", [ error localizedDescription], CMTimeGetSeconds(asset.duration));
            error = nil;
        }

        insertionPoint = CMTimeAdd(insertionPoint, asset.duration);
    }

    if (!error) {
        AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:composition presetName:AVAssetExportPresetHighestQuality];

        NSString *fileName = [Utils GetUUID];
        NSString *pathComponent = [NSString stringWithFormat:@"Documents/%@.mp4", fileName];
        NSString *pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:pathComponent];
        self.localFile = [NSString stringWithFormat:@"%@.mp4", fileName];
        unlink([pathToMovie UTF8String]); // If a file already exists, AVAssetWriter won't let you record new frames, so delete the old movie
        _fileURL = [NSURL fileURLWithPath:pathToMovie];

        exportSession.outputURL = _fileURL;
        exportSession.outputFileType = AVFileTypeMPEG4;
        [exportSession exportAsynchronouslyWithCompletionHandler:^{
            NSError *exportError = nil;
            switch (exportSession.status) {
                case AVAssetExportSessionStatusFailed:{
                    NSLog (@"FAIL");
                    exportError = [NSError errorWithDomain:@"VideoComposition" code:AVAssetExportSessionStatusFailed userInfo:@{NSLocalizedDescriptionKey: @"Export of multiple files failed."}];
                    break;
                }
                case AVAssetExportSessionStatusCompleted: {
                    NSLog (@"SUCCESS");

                    // delete all the parts...
                    NSError *removeError = nil;
                    NSFileManager *fileManager = [NSFileManager defaultManager];
                    for (NSURL *fileUrl in _fileParts) {
                        [fileManager removeItemAtURL:fileUrl error:&removeError];
                    }
                }
            };

            if (self.delegate && [self.delegate respondsToSelector:@selector(recordingStopped:error:)]) {
                [self.delegate recordingStopped:self error:exportError];
            }
        }];
    }
}

-(void) stopRecording {
    self.isRecording = NO;

    [_imageFilter removeTarget:_movieWriter];
    _camera.audioEncodingTarget = nil;

    [_movieWriter finishRecordingWithCompletionHandler:^{
        NSLog(@"Movie completed");

        [_camera stopCameraCapture];

        NSError *error = nil;

        if (!_isMultiFile) {
            // if the recording is not long enough, generate the error
            AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:_fileURL options:nil];

            CMTime thumbnailTime = [asset duration];
            float seconds = thumbnailTime.value / thumbnailTime.timescale;
            if (seconds < 5.0f) {
                NSError *attributesError = nil;
                NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[_fileURL path] error:&attributesError];
                NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
                NSLog(@"Size of file: %@", fileSizeNumber);

                error = [NSError errorWithDomain:@"VideoComposition" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Video is less than 5 seconds long."}];
            } else {
                [self createThumbnail];
            }

            if (self.delegate && [self.delegate respondsToSelector:@selector(recordingStopped:error:)]) {
                [self.delegate recordingStopped:self error:error];
            }
        } else {
            [self createMultifileOutput];
        }

        [UIApplication sharedApplication].idleTimerDisabled = NO;
    }];

}

- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer {

}

-(NSString*) thumbnailFileName {
    return _thumbnailFileName;
}

-(void) rotateCamera {
    if (self.camera.frontFacingCameraPresent == NO)
        return;

    if (!self.isRecording) {
        if (self.camera.inputCamera.torchActive) {
            NSError *error = nil;
            [self.camera.inputCamera lockForConfiguration:&error];
            [self.camera.inputCamera setTorchMode:AVCaptureTorchModeOff];
            [self.camera.inputCamera unlockForConfiguration];
        }
        [self.camera rotateCamera];

    } else {
        if (!_isMultiFile) {
            _isMultiFile = YES;

            // add the current file to the list and terminate the recording...
            [_fileParts addObject:_fileURL];
        }

        [_imageFilter removeTarget:_movieWriter];
        [_imageFilter removeTarget:_imageView];
        self.camera.audioEncodingTarget = nil;

        [_movieWriter finishRecording];

        [self createThumbnail];

        [self.camera stopCameraCapture];

        // if the recording is not long enough, generate the error
        AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:_fileURL options:nil];

        CMTime thumbnailTime = [asset duration];
        float seconds = thumbnailTime.value / thumbnailTime.timescale;

        if (self.camera.cameraPosition == AVCaptureDevicePositionBack) {
            self.camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionFront];
        } else {
            self.camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
        }

        _camera.outputImageOrientation = [[UIApplication sharedApplication] statusBarOrientation];
        _camera.horizontallyMirrorFrontFacingCamera = NO;
        _camera.horizontallyMirrorRearFacingCamera = NO;

        _imageFilter = [[GPUImageFilter alloc] init];

        [_camera addTarget:_imageFilter];

        NSString *fileName = [Utils GetUUID];
        NSString *pathComponent = [NSString stringWithFormat:@"Documents/%@.mp4", fileName];
        NSString *pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:pathComponent];
        self.localFile = [NSString stringWithFormat:@"%@.mp4", fileName];
        unlink([pathToMovie UTF8String]); // If a file already exists, AVAssetWriter won't let you record new frames, so delete the old movie
        _fileURL = [NSURL fileURLWithPath:pathToMovie];

        if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
            _movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:_fileURL size:CGSizeMake(480.0, 640.0)];
        } else {
            _movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:_fileURL size:CGSizeMake(640.0, 480.0)];
        }

        _movieWriter.encodingLiveVideo = YES;

        [_imageFilter addTarget:_movieWriter];
        [_imageFilter addTarget:_imageView];

        _camera.audioEncodingTarget = _movieWriter;

        [_camera startCameraCapture];

        [_fileParts addObject:_fileURL];

        double delayInSeconds = 120.0 - seconds;
        double delayToStartRecording = 0.1;
        dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, delayToStartRecording * NSEC_PER_SEC);
        dispatch_after(startTime, dispatch_get_main_queue(), ^(void){
            [_movieWriter startRecording];

            if (_currentTimer) {
                [_currentTimer invalidate];
                _currentTimer = nil;
            }

            /*dispatch_time_t stopTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
            dispatch_after(stopTime, recordingQueue, ^(void){

                if (self.isRecording) {
                    [self stopRecording];
                }

            });*/

            NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:delayInSeconds];
            _currentTimer = [[NSTimer alloc] initWithFireDate:fireDate interval:0.1 target:self selector:@selector(recordingTimerFired:) userInfo:nil repeats:NO];
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addTimer:_currentTimer forMode:NSDefaultRunLoopMode];
        });
    }
}
@end
neoplastic commented 10 years ago

I'm getting this in Beta 3 too. I just get spammed "Couldn't write a frame" in the console.

louistran99 commented 10 years ago

I am getting the same crash with iOS8. Writing video is fine in iOS7.

lukasmataj commented 10 years ago

Same issue here. I'm creating movie writer like this:

CGSize movieSize = CGSizeMake(640.0, 640.0);
self.movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:self.fileURL size:movieSize fileType:AVFileTypeQuickTimeMovie outputSettings:[self outputSettingsWithVideoSize:movieSize]];
self.movieWriter.encodingLiveVideo = YES;
[self.movieWriter setHasAudioTrack:NO audioSettings:nil];

[self.outputFilter addTarget:self.movieWriter];
self.videoCamera.audioEncodingTarget = nil;

outputFilter is simple crop filter. When I compile app with iOS7 SDK, app logs "Couldn't write a frame" message. With iOS8 SDK, app crashes with exception 'NSInternalInconsistencyException', reason: 'Incomplete filter FBO: 36055' in function [GPUImageMovieWriter createDataFBO]. Even if don't use any additional filter or try change other settings (audio, video), it still crashes. On iOS7 everything works fine.

acerbetti commented 10 years ago

I'm having the same situation. iOS 7 works fine, iOS 8 crash immediately with error 'Incomplete filter FBO: 36055'

acerbetti commented 10 years ago

I was using the pod and my app was crashing all the time on iOS8. I've tried to download the latest code from master and it's working for me.

Please update the pod file

lukasmataj commented 10 years ago

You're right. When I added GPUImage in old-fashioned way from latest source, everything worked. Now we can hope that pod will be updated as soon as possible.

girijeshkumar2007 commented 9 years ago

I am also getting same error in iOS 8 and Above

MrBendel commented 9 years ago

I had this issue but it turns out my GPUImageMovieWriter wasn't being released. Don't know if this helps anyone else running into the issue, but once I resolved the retain / release issue, the error went away.

luckyarthur commented 7 years ago

Anyone gets any solutions here, it seems to happen quite often