class
PGCameraMovieWriter
@property (nonatomic, copy) void(^finishedWithMovieAtURL)(NSURL *url, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success);
@property (nonatomic, readonly) NSTimeInterval currentDuration;
@property (nonatomic, readonly) bool isRecording;
@property (nonatomic, assign) bool liveUpload;
- (instancetype)initWithVideoTransform:(CGAffineTransform)videoTransform videoOutputSettings:(NSDictionary *)videoSettings audioOutputSettings:(NSDictionary *)audioSettings;
- (void)startRecording;
- (void)stopRecordingWithCompletion:(void (^)(void))completion;
- (void)_processSampleBuffer:(CMSampleBufferRef)sampleBuffer;
+ (NSString *)outputFileType;
- (instancetype)initWithVideoTransform:(CGAffineTransform)videoTransform videoOutputSettings:(NSDictionary *)videoSettings audioOutputSettings:(NSDictionary *)audioSettings
{
self = [super init];
if (self != nil)
{
_videoTransform = videoTransform;
_videoOutputSettings = videoSettings;
_audioOutputSettings = audioSettings;
_queue = [[SQueue alloc] init];
_delayedAudioSamples = [[NSMutableArray alloc] init];
}
return self;
}
// start recording
// add inputs(AVAssetWriterInput) to writer(AVAssetWriter) and begin writing
- (void)startRecording
{
[_queue dispatch:^
{
if (_isRecording || _finishedWriting)
return;
_captureStartTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
NSString *path = [PGCameraMovieWriter tempOutputPath];
if ([[NSFileManager defaultManager] fileExistsAtPath:path])
[[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
_assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:path] fileType:[PGCameraMovieWriter outputFileType] error:&error];
if (_assetWriter == nil && error != nil)
{
TGLegacyLog(@"ERROR: camera movie writer failed to initialize: %@", error);
return;
}
_videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:_videoOutputSettings];
_videoInput.expectsMediaDataInRealTime = true;
_videoInput.transform = _videoTransform;
if ([_assetWriter canAddInput:_videoInput])
{
[_assetWriter addInput:_videoInput];
}
else
{
TGLegacyLog(@"ERROR: camera movie writer failed to add video input");
return;
}
if (_audioOutputSettings != nil)
{
_audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:_audioOutputSettings];
_audioInput.expectsMediaDataInRealTime = true;
if ([_assetWriter canAddInput:_audioInput])
{
[_assetWriter addInput:_audioInput];
}
else
{
TGLegacyLog(@"ERROR: camera movie writer failed to add audio input");
return;
}
}
[_assetWriter startWriting];
_isRecording = true;
}];
}
// stop recording
- (void)stopRecordingWithCompletion:(void (^)(void))completion
{
[_queue dispatch:^
{
if (fabs(CFAbsoluteTimeGetCurrent() - _captureStartTime) < 0.5)
return;
_stopIminent = true;
_finishCompletion = completion;
if (_assetWriter.status == AVAssetWriterStatusUnknown || _assetWriter.status > AVAssetWriterStatusCompleted)
{
TGDispatchOnMainThread(^
{
if (self.finishedWithMovieAtURL != nil)
self.finishedWithMovieAtURL(nil, CGAffineTransformIdentity, CGSizeZero, 0.0, false);
TGLegacyLog(@"ERROR: camera movie writer failed to write movie: %@", _assetWriter.error);
_assetWriter = nil;
});
return;
}
if (_audioOutputSettings == nil)
[self _finishWithCompletion];
}];
}
// do finish writing
- (void)_finishWithCompletion
{
_isRecording = false;
__weak PGCameraMovieWriter *weakSelf = self;
[_assetWriter finishWritingWithCompletionHandler:^
{
__strong PGCameraMovieWriter *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_finishedWriting = true;
TGDispatchOnMainThread(^
{
if (strongSelf->_assetWriter.status == AVAssetWriterStatusCompleted)
{
if (strongSelf.finishedWithMovieAtURL != nil)
{
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:strongSelf->_assetWriter.outputURL options:nil];
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
CGSize dimensions = TGTransformDimensionsWithTransform(track.naturalSize, strongSelf->_videoTransform);
strongSelf.finishedWithMovieAtURL(strongSelf->_assetWriter.outputURL, strongSelf->_videoTransform, dimensions, strongSelf.currentDuration, true);
}
}
else
{
if (strongSelf.finishedWithMovieAtURL != nil)
strongSelf.finishedWithMovieAtURL(strongSelf->_assetWriter.outputURL, CGAffineTransformIdentity, CGSizeZero, 0.0, false);
TGLegacyLog(@"ERROR: camera movie writer failed to write movie: %@", strongSelf->_assetWriter.error);
}
strongSelf->_assetWriter = nil;
if (_finishCompletion != nil)
_finishCompletion();
});
}];
}
// process sample buffer
- (void)_processSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
CFRetain(sampleBuffer);
[_queue dispatch:^
{
CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDescription);
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
if (_assetWriter.status > AVAssetWriterStatusCompleted)
{
TGLegacyLog(@"WARNING: camera movie writer status is %d", _assetWriter.status);
if (_assetWriter.status == AVAssetWriterStatusFailed)
{
TGLegacyLog(@"ERROR: camera movie writer error: %@", _assetWriter.error);
_isRecording = false;
if (self.finishedWithMovieAtURL != nil)
self.finishedWithMovieAtURL(_assetWriter.outputURL, CGAffineTransformIdentity, CGSizeZero, 0.0, false);
}
return;
}
bool keepSample = false;
if (mediaType == kCMMediaType_Video)
{
if (!_startedWriting)
{
[_assetWriter startSessionAtSourceTime:timestamp];
_startTimeStamp = timestamp;
_startedWriting = true;
}
while (!_videoInput.readyForMoreMediaData)
{
NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:0.1];
[[NSRunLoop currentRunLoop] runUntilDate:maxDate];
}
bool success = [_videoInput appendSampleBuffer:sampleBuffer];
if (success)
_lastVideoTimeStamp = timestamp;
else
TGLegacyLog(@"ERROR: camera movie writer failed to append pixel buffer");
if (_audioOutputSettings != nil && _stopIminent && CMTimeCompare(_lastVideoTimeStamp, _lastAudioTimeStamp) != -1) {
[self _finishWithCompletion];
}
}
else if (mediaType == kCMMediaType_Audio && !_stopIminent)
{
if (!_startedWriting)
{
[_delayedAudioSamples addObject:(__bridge id _Nonnull)(sampleBuffer)];
keepSample = true;
}
else
{
if (_delayedAudioSamples.count > 0)
{
for (id sample in _delayedAudioSamples)
{
CMSampleBufferRef buffer = (__bridge CMSampleBufferRef)(sample);
[_audioInput appendSampleBuffer:buffer];
CFRelease(buffer);
}
_delayedAudioSamples = nil;
}
while (!_audioInput.isReadyForMoreMediaData)
{
NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:0.1];
[[NSRunLoop currentRunLoop] runUntilDate:maxDate];
}
bool success = [_audioInput appendSampleBuffer:sampleBuffer];
if (success)
_lastAudioTimeStamp = timestamp;
else
TGLegacyLog(@"ERROR: camera movie writer failed to append audio buffer");
}
}
if (!keepSample)
CFRelease(sampleBuffer);
}];
}
- (NSTimeInterval)currentDuration
{
return CMTimeGetSeconds(CMTimeSubtract(_lastVideoTimeStamp, _startTimeStamp));
}
+ (NSString *)outputFileType
{
return AVFileTypeMPEG4;
}
+ (NSString *)tempOutputPath
{
return [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"camvideo_%x.mp4", (int)arc4random()]];
}