Classes/Utilities/MovieRecorder.m

 
/*
     File: MovieRecorder.m
 Abstract: Real-time movie recorder which is totally non-blocking
  Version: 2.2
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 
 */
 
#import "MovieRecorder.h"
 
#import <AVFoundation/AVAssetWriter.h>
#import <AVFoundation/AVAssetWriterInput.h>
 
#import <AVFoundation/AVMediaFormat.h>
#import <AVFoundation/AVVideoSettings.h>
#import <AVFoundation/AVAudioSettings.h>
 
#include <objc/runtime.h> // for objc_loadWeak() and objc_storeWeak()
 
#define LOG_STATUS_TRANSITIONS 0
 
typedef NS_ENUM( NSInteger, MovieRecorderStatus ) {
    MovieRecorderStatusIdle = 0,
    MovieRecorderStatusPreparingToRecord,
    MovieRecorderStatusRecording,
    MovieRecorderStatusFinishingRecordingPart1, // waiting for inflight buffers to be appended
    MovieRecorderStatusFinishingRecordingPart2, // calling finish writing on the asset writer
    MovieRecorderStatusFinished,    // terminal state
    MovieRecorderStatusFailed       // terminal state
}; // internal state machine
 
 
@interface MovieRecorder ()
{
    MovieRecorderStatus _status;
 
    __weak id <MovieRecorderDelegate> _delegate; // __weak doesn't actually do anything under non-ARC
    dispatch_queue_t _delegateCallbackQueue;
    
    dispatch_queue_t _writingQueue;
    
    NSURL *_URL;
    
    AVAssetWriter *_assetWriter;
    BOOL _haveStartedSession;
    
    CMFormatDescriptionRef _audioTrackSourceFormatDescription;
    AVAssetWriterInput *_audioInput;
    
    CMFormatDescriptionRef _videoTrackSourceFormatDescription;
    CGAffineTransform _videoTrackTransform;
    AVAssetWriterInput *_videoInput;
}
@end
 
@implementation MovieRecorder
 
#pragma mark -
#pragma mark API
 
- (id)initWithURL:(NSURL *)URL
{
    if ( ! URL ) {
        [self release];
        return nil;
    }
    
    if ( self = [super init] ) {
        _writingQueue = dispatch_queue_create( "com.apple.sample.movierecorder.writing", DISPATCH_QUEUE_SERIAL );
        _videoTrackTransform = CGAffineTransformIdentity;
        _URL = [URL retain];
    }
    return self;
}
 
- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription transform:(CGAffineTransform)transform
{
    if ( formatDescription == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
        return;         
    }
    
    @synchronized( self ) {
        if ( _status != MovieRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if ( _videoTrackSourceFormatDescription ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one video track" userInfo:nil];
            return;
        }
        
        _videoTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
        _videoTrackTransform = transform;
    }
}
 
- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription
{
    if ( formatDescription == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
        return;         
    }
    
    @synchronized( self ) {
        if ( _status != MovieRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if ( _audioTrackSourceFormatDescription ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one audio track" userInfo:nil];
            return;
        }
        
        _audioTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
    }
}
 
- (id<MovieRecorderDelegate>)delegate
{
    id <MovieRecorderDelegate> delegate = nil;
    @synchronized( self ) {
        delegate = objc_loadWeak( &_delegate ); // unnecessary under ARC, just assign to delegate directly
    }
    return delegate;
}
 
- (void)setDelegate:(id<MovieRecorderDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue; // delegate is weak referenced
{
    if ( delegate && ( delegateCallbackQueue == NULL ) )
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Caller must provide a delegateCallbackQueue" userInfo:nil];
    
    @synchronized( self ) {
        objc_storeWeak( &_delegate, delegate ); // unnecessary under ARC, just assign to _delegate directly
        if ( delegateCallbackQueue != _delegateCallbackQueue  ) {
            if ( delegateCallbackQueue )
                [delegateCallbackQueue retain];
            if ( _delegateCallbackQueue )
                [_delegateCallbackQueue release];
            _delegateCallbackQueue = delegateCallbackQueue;
        }
    }
}
 
- (void)prepareToRecord
{
    @synchronized( self ) {
        if ( _status != MovieRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Already prepared, cannot prepare again" userInfo:nil];
            return;
        }
        
        [self transitionToStatus:MovieRecorderStatusPreparingToRecord error:nil];
    }
    
    dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0 ), ^{
        @autoreleasepool {
            NSError *error = nil;
            // AVAssetWriter will not write over an existing file.
            [[NSFileManager defaultManager] removeItemAtURL:_URL error:NULL];
            
            _assetWriter = [[AVAssetWriter alloc] initWithURL:_URL fileType:AVFileTypeQuickTimeMovie error:&error];
            
            // Create and add inputs
            if ( ! error && _videoTrackSourceFormatDescription ) {
                [self setupAssetWriterVideoInput:_videoTrackSourceFormatDescription transform:_videoTrackTransform error:&error];
            }
            
            if ( ! error && _audioTrackSourceFormatDescription ) {
                [self setupAssetWriterAudioInput:_audioTrackSourceFormatDescription error:&error];
            }
            
            if ( ! error ) {
                BOOL success = [_assetWriter startWriting];
                if ( ! success )
                    error = _assetWriter.error;
            }
            
            @synchronized( self ) {
                if ( error )
                    [self transitionToStatus:MovieRecorderStatusFailed error:error];
                else
                    [self transitionToStatus:MovieRecorderStatusRecording error:nil];
            }
        }
    });
}
 
- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
}
 
- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime
{
    CMSampleBufferRef sampleBuffer = NULL;
    
    CMSampleTimingInfo timingInfo = {0,};
    timingInfo.duration = kCMTimeInvalid;
    timingInfo.decodeTimeStamp = kCMTimeInvalid;
    timingInfo.presentationTimeStamp = presentationTime;
    
    OSStatus err = CMSampleBufferCreateForImageBuffer( kCFAllocatorDefault, pixelBuffer, true, NULL, NULL, _videoTrackSourceFormatDescription, &timingInfo, &sampleBuffer );
    if ( sampleBuffer ) {
        [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
        CFRelease( sampleBuffer );
    }
    else {
        NSString *exceptionReason = [NSString stringWithFormat:@"sample buffer create failed (%i)", (int)err];
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:exceptionReason userInfo:nil];
        return;
    }
}
 
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio];
}
 
- (void)finishRecording
{
    @synchronized( self ) {
        BOOL shouldFinishRecording = NO;
        switch ( _status ) {
            case MovieRecorderStatusIdle:
            case MovieRecorderStatusPreparingToRecord:
            case MovieRecorderStatusFinishingRecordingPart1:
            case MovieRecorderStatusFinishingRecordingPart2:
            case MovieRecorderStatusFinished:
                @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not recording" userInfo:nil];
                break;
            case MovieRecorderStatusFailed:
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when finishRecording is called and we are in an error state.
                NSLog( @"Recording has failed, nothing to do" );
                break;
            case MovieRecorderStatusRecording:
                shouldFinishRecording = YES;
                break;
        }
        
        if ( shouldFinishRecording )
            [self transitionToStatus:MovieRecorderStatusFinishingRecordingPart1 error:nil];
        else
            return;
    }
    
    dispatch_async( _writingQueue, ^{
        @autoreleasepool {
            @synchronized( self ) {
                // We may have transitioned to an error state as we appended inflight buffers. In that case there is nothing to do now.
                if ( _status != MovieRecorderStatusFinishingRecordingPart1 )
                    return ;
                
                // It is not safe to call -[AVAssetWriter finishWriting*] concurrently with -[AVAssetWriterInput appendSampleBuffer:]
                // We transition to MovieRecorderStatusFinishingRecordingPart2 while on _writingQueue, which guarantees that no more buffers will be appended.
                [self transitionToStatus:MovieRecorderStatusFinishingRecordingPart2 error:nil];
            }
            
            dispatch_block_t completionHandler = ^{
                @synchronized( self ) {
                    NSError *error = _assetWriter.error;
                    if ( error )
                        [self transitionToStatus:MovieRecorderStatusFailed error:error];
                    else
                        [self transitionToStatus:MovieRecorderStatusFinished error:nil];
                }               
            };
 
            [_assetWriter finishWritingWithCompletionHandler:completionHandler];
        }
    });
}
 
- (void)dealloc
{
    objc_storeWeak( &_delegate, nil ); // unregister _delegate as a weak reference
    
    if ( _delegateCallbackQueue )
        [_delegateCallbackQueue release];
    
    if ( _writingQueue )
        [_writingQueue release];
    
    [self teardownAssetWriterAndInputs];
 
    if ( _audioTrackSourceFormatDescription )
        CFRelease( _audioTrackSourceFormatDescription );
    if ( _videoTrackSourceFormatDescription )
        CFRelease( _videoTrackSourceFormatDescription );
 
    [_URL release];
    
    [super dealloc];
}
 
#pragma mark -
#pragma mark Internal
 
- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer ofMediaType:(NSString *)mediaType
{
    if ( sampleBuffer == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL sample buffer" userInfo:nil];
        return;         
    }
    
    @synchronized( self ) {
        if ( _status < MovieRecorderStatusRecording ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not ready to record yet" userInfo:nil];
            return; 
        }
    }
    
    CFRetain( sampleBuffer );
    dispatch_async( _writingQueue, ^{
        @autoreleasepool {
            @synchronized( self ) {
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when samples are appended and we are no longer recording.
                // Instead of throwing an exception we just release the sample buffers and return.
                if ( _status > MovieRecorderStatusFinishingRecordingPart1 ) {
                    CFRelease( sampleBuffer );
                    return;
                }
            }
            
            if ( ! _haveStartedSession ) {
                [_assetWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
                _haveStartedSession = YES;
            }
            
            AVAssetWriterInput *input = ( mediaType == AVMediaTypeVideo ) ? _videoInput : _audioInput;
            
            if ( input.readyForMoreMediaData ) {
                BOOL success = [input appendSampleBuffer:sampleBuffer];
                if ( ! success ) {
                    NSError *error = _assetWriter.error;
                    @synchronized( self ) {
                        [self transitionToStatus:MovieRecorderStatusFailed error:error];
                    }
                }
            }
            else {
                NSLog( @"%@ input not ready for more media data, dropping buffer", mediaType );
            }
            CFRelease( sampleBuffer );
        }
    });
}
 
// call under @synchonized( self )
- (void)transitionToStatus:(MovieRecorderStatus)newStatus error:(NSError *)error
{
    BOOL shouldNotifyDelegate = NO;
    
#if LOG_STATUS_TRANSITIONS
    NSLog( @"MovieRecorder state transition: %@->%@", [self stringForStatus:_status], [self stringForStatus:newStatus] );
#endif
    
    if ( newStatus != _status ) {
        // terminal states
        if ( ( newStatus == MovieRecorderStatusFinished ) || ( newStatus == MovieRecorderStatusFailed ) ) {
            shouldNotifyDelegate = YES;
            // make sure there are no more sample buffers in flight before we tear down the asset writer and inputs
            
            dispatch_async( _writingQueue, ^{
                [self teardownAssetWriterAndInputs];
                if ( newStatus == MovieRecorderStatusFailed ) {
                    [[NSFileManager defaultManager] removeItemAtURL:_URL error:NULL];
                }
            });
 
#if LOG_STATUS_TRANSITIONS
            if ( error )
                NSLog( @"MovieRecorder error: %@, code: %i", error, (int)error.code );
#endif
        }
        else if ( newStatus == MovieRecorderStatusRecording ) {
            shouldNotifyDelegate = YES;
        }
        
        _status = newStatus;
    }
 
    if ( shouldNotifyDelegate && [self delegate] ) {
        dispatch_async( _delegateCallbackQueue, ^{
            @autoreleasepool {
                switch ( newStatus ) {
                    case MovieRecorderStatusRecording:
                        [[self delegate] movieRecorderDidFinishPreparing:self];
                        break;
                    case MovieRecorderStatusFinished:
                        [[self delegate] movieRecorderDidFinishRecording:self];
                        break;
                    case MovieRecorderStatusFailed:
                        [[self delegate] movieRecorder:self didFailWithError:error];
                        break;
                    default:
                        break;
                }
            }
        });
    }
}
 
#if LOG_STATUS_TRANSITIONS
 
- (NSString *)stringForStatus:(MovieRecorderStatus)status
{
    NSString *statusString = nil;
    
    switch ( status ) {
        case MovieRecorderStatusIdle:
            statusString = @"Idle";
            break;
        case MovieRecorderStatusPreparingToRecord:
            statusString = @"PreparingToRecord";
            break;
        case MovieRecorderStatusRecording:
            statusString = @"Recording";
            break;
        case MovieRecorderStatusFinishingRecordingPart1:
            statusString = @"FinishingRecordingPart1";
            break;
        case MovieRecorderStatusFinishingRecordingPart2:
            statusString = @"FinishingRecordingPart2";
            break;
        case MovieRecorderStatusFinished:
            statusString = @"Finished";
            break;
        case MovieRecorderStatusFailed:
            statusString = @"Failed";
            break;
        default:
            statusString = @"Unknown";
            break;
    }
    return statusString;
    
}
 
#endif // LOG_STATUS_TRANSITIONS
 
- (BOOL)setupAssetWriterAudioInput:(CMFormatDescriptionRef)audioFormatDescription error:(NSError **)errorOut
{
    BOOL supportsFormatHint = [AVAssetWriterInput instancesRespondToSelector:@selector(initWithMediaType:outputSettings:sourceFormatHint:)]; // supported on iOS 6 and later
    
    NSDictionary *audioCompressionSettings = nil;
    
    if ( supportsFormatHint ) {
        audioCompressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                    [NSNumber numberWithInteger:kAudioFormatMPEG4AAC], AVFormatIDKey,
                                    nil];
    }
    else {
        const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDescription);
        
        size_t aclSize = 0;
        const AudioChannelLayout *currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(audioFormatDescription, &aclSize);
        NSData *currentChannelLayoutData = nil;
        
        // AVChannelLayoutKey must be specified, but if we don't know any better give an empty data and let AVAssetWriter decide.
        if ( currentChannelLayout && aclSize > 0 )
            currentChannelLayoutData = [NSData dataWithBytes:currentChannelLayout length:aclSize];
        else
            currentChannelLayoutData = [NSData data];
        
        audioCompressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                    [NSNumber numberWithInteger:kAudioFormatMPEG4AAC], AVFormatIDKey,
                                    [NSNumber numberWithFloat:currentASBD->mSampleRate], AVSampleRateKey,
                                    [NSNumber numberWithInt:64000], AVEncoderBitRatePerChannelKey,
                                    [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame], AVNumberOfChannelsKey,
                                    currentChannelLayoutData, AVChannelLayoutKey,
                                    nil];
    }
    if ( [_assetWriter canApplyOutputSettings:audioCompressionSettings forMediaType:AVMediaTypeAudio] ) {
        if ( supportsFormatHint )
            _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings sourceFormatHint:audioFormatDescription];
        else
            _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings];
        _audioInput.expectsMediaDataInRealTime = YES;
        if ( [_assetWriter canAddInput:_audioInput] )
            [_assetWriter addInput:_audioInput];
        else {
            if ( errorOut )
                *errorOut = [[self class] cannotSetupInputError];
            return NO;
        }
    }
    else {
        if ( errorOut )
            *errorOut = [[self class] cannotSetupInputError];
        return NO;
    }
    
    return YES;
}
 
- (BOOL)setupAssetWriterVideoInput:(CMFormatDescriptionRef)videoFormatDescription transform:(CGAffineTransform)transform error:(NSError **)errorOut
{
    BOOL supportsFormatHint = [AVAssetWriterInput instancesRespondToSelector:@selector(initWithMediaType:outputSettings:sourceFormatHint:)]; // supported on iOS 6 and later
    
    float bitsPerPixel;
    CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(videoFormatDescription);
    int numPixels = dimensions.width * dimensions.height;
    int bitsPerSecond;
    
    // Assume that lower-than-SD resolutions are intended for streaming, and use a lower bitrate
    if ( numPixels < (640 * 480) )
        bitsPerPixel = 4.05; // This bitrate approximately matches the quality produced by AVCaptureSessionPresetMedium or Low.
    else
        bitsPerPixel = 10.1; // This bitrate approximately matches the quality produced by AVCaptureSessionPresetHigh.
    
    bitsPerSecond = numPixels * bitsPerPixel;
    
    NSDictionary *videoCompressionSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                              AVVideoCodecH264, AVVideoCodecKey,
                                              [NSNumber numberWithInteger:dimensions.width], AVVideoWidthKey,
                                              [NSNumber numberWithInteger:dimensions.height], AVVideoHeightKey,
                                              [NSDictionary dictionaryWithObjectsAndKeys:
                                               [NSNumber numberWithInteger:bitsPerSecond], AVVideoAverageBitRateKey,
                                               [NSNumber numberWithInteger:30], AVVideoMaxKeyFrameIntervalKey,
                                               nil], AVVideoCompressionPropertiesKey,
                                              nil];
    if ( [_assetWriter canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo] ) {
        if ( supportsFormatHint )
            _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings sourceFormatHint:videoFormatDescription];
        else
            _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings];
        _videoInput.expectsMediaDataInRealTime = YES;
        _videoInput.transform = transform;
        if ( [_assetWriter canAddInput:_videoInput] )
            [_assetWriter addInput:_videoInput];
        else {
            if ( errorOut )
                *errorOut = [[self class] cannotSetupInputError];
            return NO;
        }
    }
    else {
        if ( errorOut )
            *errorOut = [[self class] cannotSetupInputError];
        return NO;
    }
    
    return YES;
}
 
+ (NSError *)cannotSetupInputError
{
    NSString *localizedDescription = NSLocalizedString( @"Recording cannot be started", nil );
    NSString *localizedFailureReason = NSLocalizedString( @"Cannot setup asset writer input.", nil );
    NSDictionary *errorDict = [NSDictionary dictionaryWithObjectsAndKeys:
                               localizedDescription, NSLocalizedDescriptionKey,
                               localizedFailureReason, NSLocalizedFailureReasonErrorKey,
                               nil];
    return [NSError errorWithDomain:@"com.apple.dts.samplecode" code:0 userInfo:errorDict];
}
 
- (void)teardownAssetWriterAndInputs
{
    [_videoInput release];
    _videoInput = nil;
    [_audioInput release];
    _audioInput = nil;
    [_assetWriter release];
    _assetWriter = nil;
}
 
@end