rFlex / SCRecorder

iOS camera engine with Vine-like tap to record, animatable filters, slow motion, segments editing
Apache License 2.0
3.06k stars 582 forks source link

Memory leak on prepareCamera #214

Open loretoparisi opened 9 years ago

loretoparisi commented 9 years ago

I'm trying to restart automatically a new SCRecordSession as soon the previous session is completed (getting the didCompleteRecordSession delegate). It seems that this causes a memory leak:

The code is like:

- (void)recorder:(SCRecorder *)recorder didCompleteRecordSession:(SCRecordSession *)recordSession {

    [self captureVideoBuffer:nil]; // to get last capture
    _recorder.recordSession = nil;
    [self prepareCamera];  /// this is causing the leak
    [self updateTimeRecordedLabel];
    [_recorder record];

}

The leak seamed (see latest comment) to be caused by prepareCamera and so due to the init of the new SCRecordSession:

if (_recorder.recordSession == nil) {
        SCRecordSession *session = [SCRecordSession recordSession];
        _recorder.recordSession = session;
    }

I need the prepareCamera, otherwise I'm not getting the last video buffer updated:

UIImage *__lastImage = [_recorder snapshotOfLastAppendedVideoBuffer];

Looking at the last implementation, I have also tried to reference the passed SCRecordSession object and clean it up like:

- (void)recorder:(SCRecorder *)recorder didCompleteRecordSession:(SCRecordSession *)recordSession {
    //[self saveAndShowSession:recordSession];

    [self captureVideoBuffer:nil];

    _recordSession = recordSession;
    if (_recordSession != nil) {
        _recorder.recordSession = nil;
        // If the recordSession was saved, we don't want to completely destroy it
        if ([[SCRecordSessionManager sharedInstance] isSaved:recordSession]) {
            [recordSession endRecordSegment:nil];
        } else {
            [recordSession cancelSession:nil];
        }
    }

    [self prepareCamera];
    [self updateTimeRecordedLabel];

    [_recorder record];

}

But it keeps leaking. Maybe I'm wrong on how to initialize a new SCRecordSession automatically.

loretoparisi commented 9 years ago

I did further tests. What it seems to leak is actually something inside

- (UIImage *)_imageFromSampleBufferHolder:(SCSampleBufferHolder *)sampleBufferHolder {
    __block CMSampleBufferRef sampleBuffer = nil;
    dispatch_sync(_videoQueue, ^{
        sampleBuffer = sampleBufferHolder.sampleBuffer;

        if (sampleBuffer != nil) {
            CFRetain(sampleBuffer);
        }
    });

    if (sampleBuffer == nil) {
        return nil;
    }

    CVPixelBufferRef buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:buffer];

    if (_context == nil) {
        _context = [CIContext contextWithOptions:nil];
    }

    CGImageRef cgImage = [_context createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(buffer), CVPixelBufferGetHeight(buffer))];

    UIImage *image = [UIImage imageWithCGImage:cgImage];

    CGImageRelease(cgImage);
    CFRelease(sampleBuffer);

    return image;

}

called by

- (UIImage *)snapshotOfLastAppendedVideoBuffer {
    return [self _imageFromSampleBufferHolder:_lastAppendedVideoBuffer];
}

that I'm calling in the

-(UIImage*)captureVideoBuffer:(id)sender {

    UIImage *__lastImage = [_recorder snapshotOfLastAppendedVideoBuffer];
    if(!__lastImage) {
        return nil;
    }

in the didCompleteRecordSession delegate

loretoparisi commented 9 years ago

I have added a auto release pool like:

- (void)imageFromVideoBuffer:(void(^)(UIImage* image))completion {
    @autoreleasepool {
        CMSampleBufferRef sampleBuffer = _lastAppendedVideoBuffer.sampleBuffer;
        if (sampleBuffer != nil) {
            CFRetain(sampleBuffer);
            CIImage *ciImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
            _lastAppendedVideoBuffer.sampleBuffer = nil;
            if (_context == nil) {
                _context = [CIContext contextWithOptions:nil];
            }
            CVPixelBufferRef buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
            CGImageRef cgImage = [_context createCGImage:ciImage fromRect:
                                  CGRectMake(0, 0, CVPixelBufferGetWidth(buffer), CVPixelBufferGetHeight(buffer))];
            __block UIImage *image = [UIImage imageWithCGImage:cgImage];

            CGImageRelease(cgImage);
            CFRelease(sampleBuffer);

            [self analyzeImageBuffer:image sampleBuffer:sampleBuffer completion:^(UIImage *image) {
                if(completion) completion(image);
            }];

            return;
        }
        if(completion) completion(nil);
    }
}

Here for another discussion about that: http://stackoverflow.com/questions/32685756/memory-leak-in-cmsamplebuffergetimagebuffer

loretoparisi commented 9 years ago

I confirm that calling the _imageFromSampleBufferHolder the memory leaks the

sampleBuffer retained from the SCSampleBufferHolder:

__block CMSampleBufferRef sampleBuffer = nil;
        dispatch_sync(_videoQueue, ^{
            sampleBuffer = sampleBufferHolder.sampleBuffer;
            if (sampleBuffer != nil) {
                CFRetain(sampleBuffer);
            }
        });

The leak constantly increases the memory allocation every X seconds.

memory_leak_screcorder

This could be due to a wrong way to restart a recording session that is ended:

- (void)recorder:(SCRecorder *)recorder didCompleteRecordSession:(SCRecordSession *)recordSession {

    NSLog(@"didCompleteRecordSession %f", CMTimeGetSeconds( recordSession.segmentsDuration ));

    [self captureVideoBuffer]; // this will call the _imageFromSampleBufferHolder

    _recordSession = recordSession;
    if (_recordSession != nil) { // cancel record session
        _recorder.recordSession = nil;
        [recordSession cancelSession:nil];
    }

    [self prepareCamera];
    [self updateTimeRecordedLabel];

    [_recorder record];

}

I have set the max recording time to 5 seconds:

_recorder.maxRecordDuration = CMTimeMake(5, 1);

loretoparisi commented 9 years ago

So I did another test! I put in the AVCapture output callback to get the sampleBuffer.

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    if (captureOutput == _videoOutput) {
        _lastVideoBuffer.sampleBuffer = sampleBuffer;
        id<CIImageRenderer> imageRenderer = _CIImageRenderer;

        dispatch_async(dispatch_get_main_queue(), ^{
            @autoreleasepool {
                CIImage *ciImage = nil;
                ciImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
                if(_context==nil) {
                    _context = [CIContext contextWithOptions:nil];
                }
                CGImageRef processedCGImage = [_context createCGImage:ciImage
                                                             fromRect:[ciImage extent]];
                //UIImage *image=[UIImage imageWithCGImage:processedCGImage];
                CGImageRelease(processedCGImage);
                NSLog(@"Captured image %@", ciImage);
            }
        });

The code that leaks is the createCGImage:ciImage:

CGImageRef processedCGImage = [_context createCGImage:ciImage
                                                             fromRect:[ciImage extent]];

I have taken this from the generateImageFromSampleBufferHolder

mjgaylord commented 9 years ago

@loretoparisi is it possible your issue is related to this? https://forums.developer.apple.com/message/50981#50981

If it is, it seems to have been resolved in iOS9.1 beta 3

loretoparisi commented 9 years ago

@mjgaylord that's a super finding! I will check it with the latest beta. Back with some results.