236 lines
9.0 KiB
Mathematica
236 lines
9.0 KiB
Mathematica
|
|
|
|||
|
|
// Created by Tencent on 2023/06/09.
|
|||
|
|
// Copyright © 2023 Tencent. All rights reserved.
|
|||
|
|
|
|||
|
|
#import "TUIMovieManager.h"
|
|||
|
|
#import <TIMCommon/TIMDefine.h>
|
|||
|
|
|
|||
|
|
@interface TUIMovieManager () {
|
|||
|
|
BOOL _readyToRecordVideo;
|
|||
|
|
BOOL _readyToRecordAudio;
|
|||
|
|
dispatch_queue_t _movieWritingQueue;
|
|||
|
|
|
|||
|
|
NSURL *_movieURL;
|
|||
|
|
AVAssetWriter *_movieWriter;
|
|||
|
|
AVAssetWriterInput *_movieAudioInput;
|
|||
|
|
AVAssetWriterInput *_movieVideoInput;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@end
|
|||
|
|
|
|||
|
|
@implementation TUIMovieManager
|
|||
|
|
|
|||
|
|
- (instancetype)init {
|
|||
|
|
self = [super init];
|
|||
|
|
if (self) {
|
|||
|
|
_movieWritingQueue = dispatch_queue_create("com.tui.Movie.Writing.Queue", DISPATCH_QUEUE_SERIAL);
|
|||
|
|
_movieURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"TUICaptureTempMovie.mp4"]];
|
|||
|
|
_referenceOrientation = AVCaptureVideoOrientationPortrait;
|
|||
|
|
}
|
|||
|
|
return self;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)start:(void (^)(NSError *error))handle {
|
|||
|
|
@weakify(self);
|
|||
|
|
dispatch_async(_movieWritingQueue, ^{
|
|||
|
|
@strongify(self);
|
|||
|
|
[self removeFile:self->_movieURL];
|
|||
|
|
NSError *error;
|
|||
|
|
if (!self->_movieWriter) {
|
|||
|
|
self->_movieWriter = [[AVAssetWriter alloc] initWithURL:self->_movieURL fileType:AVFileTypeMPEG4 error:&error];
|
|||
|
|
}
|
|||
|
|
handle(error);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)stop:(void (^)(NSURL *url, NSError *error))handle {
|
|||
|
|
@weakify(self);
|
|||
|
|
dispatch_async(_movieWritingQueue, ^{
|
|||
|
|
@strongify(self);
|
|||
|
|
self->_readyToRecordVideo = NO;
|
|||
|
|
self->_readyToRecordAudio = NO;
|
|||
|
|
|
|||
|
|
if (self->_movieWriter && self->_movieWriter.status == AVAssetWriterStatusWriting) {
|
|||
|
|
@weakify(self);
|
|||
|
|
[self->_movieWriter finishWritingWithCompletionHandler:^() {
|
|||
|
|
@strongify(self);
|
|||
|
|
@weakify(self);
|
|||
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|||
|
|
@strongify(self);
|
|||
|
|
if (self->_movieWriter.status == AVAssetWriterStatusCompleted) {
|
|||
|
|
handle(self->_movieURL, nil);
|
|||
|
|
} else {
|
|||
|
|
handle(nil, self->_movieWriter.error);
|
|||
|
|
}
|
|||
|
|
self->_movieWriter = nil;
|
|||
|
|
});
|
|||
|
|
}];
|
|||
|
|
} else {
|
|||
|
|
[self->_movieWriter cancelWriting];
|
|||
|
|
self->_movieWriter = nil;
|
|||
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|||
|
|
handle(nil, [NSError errorWithDomain:@"com.tui.Movie.Writing" code:0 userInfo:@{NSLocalizedDescriptionKey : @"AVAssetWriter status error"}]);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer {
|
|||
|
|
CFRetain(buffer);
|
|||
|
|
@weakify(self);
|
|||
|
|
dispatch_async(_movieWritingQueue, ^{
|
|||
|
|
@strongify(self);
|
|||
|
|
if (connection == video) {
|
|||
|
|
if (!self->_readyToRecordVideo) {
|
|||
|
|
self->_readyToRecordVideo = [self setupAssetWriterVideoInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
|
|||
|
|
}
|
|||
|
|
if ([self inputsReadyToRecord]) {
|
|||
|
|
[self writeSampleBuffer:buffer ofType:AVMediaTypeVideo];
|
|||
|
|
}
|
|||
|
|
} else if (connection == audio) {
|
|||
|
|
if (!self->_readyToRecordAudio) {
|
|||
|
|
self->_readyToRecordAudio = [self setupAssetWriterAudioInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
|
|||
|
|
}
|
|||
|
|
if ([self inputsReadyToRecord]) {
|
|||
|
|
[self writeSampleBuffer:buffer ofType:AVMediaTypeAudio];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CFRelease(buffer);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)writeSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType {
|
|||
|
|
if (_movieWriter.status == AVAssetWriterStatusUnknown) {
|
|||
|
|
if ([_movieWriter startWriting]) {
|
|||
|
|
[_movieWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
|
|||
|
|
} else {
|
|||
|
|
NSLog(@"%@", _movieWriter.error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (_movieWriter.status == AVAssetWriterStatusWriting) {
|
|||
|
|
if (mediaType == AVMediaTypeVideo) {
|
|||
|
|
if (!_movieVideoInput.isReadyForMoreMediaData) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (![_movieVideoInput appendSampleBuffer:sampleBuffer]) {
|
|||
|
|
NSLog(@"%@", _movieWriter.error);
|
|||
|
|
}
|
|||
|
|
} else if (mediaType == AVMediaTypeAudio) {
|
|||
|
|
if (!_movieAudioInput.isReadyForMoreMediaData) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (![_movieAudioInput appendSampleBuffer:sampleBuffer]) {
|
|||
|
|
NSLog(@"%@", _movieWriter.error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (BOOL)inputsReadyToRecord {
|
|||
|
|
return _readyToRecordVideo && _readyToRecordAudio;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (NSError *)setupAssetWriterAudioInput:(CMFormatDescriptionRef)currentFormatDescription {
|
|||
|
|
size_t aclSize = 0;
|
|||
|
|
const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription);
|
|||
|
|
const AudioChannelLayout *channelLayout = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize);
|
|||
|
|
NSData *dataLayout = aclSize > 0 ? [NSData dataWithBytes:channelLayout length:aclSize] : [NSData data];
|
|||
|
|
NSDictionary *settings = @{
|
|||
|
|
AVFormatIDKey : [NSNumber numberWithInteger:kAudioFormatMPEG4AAC],
|
|||
|
|
AVSampleRateKey : [NSNumber numberWithFloat:currentASBD->mSampleRate],
|
|||
|
|
AVChannelLayoutKey : dataLayout,
|
|||
|
|
AVNumberOfChannelsKey : [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame],
|
|||
|
|
AVEncoderBitRatePerChannelKey : [NSNumber numberWithInt:64000]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeAudio]) {
|
|||
|
|
_movieAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:settings];
|
|||
|
|
_movieAudioInput.expectsMediaDataInRealTime = YES;
|
|||
|
|
if ([_movieWriter canAddInput:_movieAudioInput]) {
|
|||
|
|
[_movieWriter addInput:_movieAudioInput];
|
|||
|
|
} else {
|
|||
|
|
return _movieWriter.error;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
return _movieWriter.error;
|
|||
|
|
}
|
|||
|
|
return nil;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (NSError *)setupAssetWriterVideoInput:(CMFormatDescriptionRef)currentFormatDescription {
|
|||
|
|
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription);
|
|||
|
|
NSUInteger numPixels = dimensions.width * dimensions.height;
|
|||
|
|
CGFloat bitsPerPixel = numPixels < (640 * 480) ? 4.05 : 11.0;
|
|||
|
|
NSDictionary *compression =
|
|||
|
|
@{AVVideoAverageBitRateKey : [NSNumber numberWithInteger:numPixels * bitsPerPixel], AVVideoMaxKeyFrameIntervalKey : [NSNumber numberWithInteger:30]};
|
|||
|
|
NSDictionary *settings = @{
|
|||
|
|
AVVideoCodecKey : AVVideoCodecH264,
|
|||
|
|
AVVideoWidthKey : [NSNumber numberWithInteger:dimensions.width],
|
|||
|
|
AVVideoHeightKey : [NSNumber numberWithInteger:dimensions.height],
|
|||
|
|
AVVideoCompressionPropertiesKey : compression
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeVideo]) {
|
|||
|
|
_movieVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:settings];
|
|||
|
|
_movieVideoInput.expectsMediaDataInRealTime = YES;
|
|||
|
|
_movieVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
|
|||
|
|
if ([_movieWriter canAddInput:_movieVideoInput]) {
|
|||
|
|
[_movieWriter addInput:_movieVideoInput];
|
|||
|
|
} else {
|
|||
|
|
return _movieWriter.error;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
return _movieWriter.error;
|
|||
|
|
}
|
|||
|
|
return nil;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (CGAffineTransform)transformFromCurrentVideoOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
|
|||
|
|
CGFloat orientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:orientation];
|
|||
|
|
CGFloat videoOrientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:self.currentOrientation];
|
|||
|
|
CGFloat angleOffset;
|
|||
|
|
if (self.currentDevice.position == AVCaptureDevicePositionBack) {
|
|||
|
|
angleOffset = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
|
|||
|
|
} else {
|
|||
|
|
angleOffset = orientationAngleOffset - videoOrientationAngleOffset + M_PI_2;
|
|||
|
|
}
|
|||
|
|
CGAffineTransform transform = CGAffineTransformMakeRotation(angleOffset);
|
|||
|
|
return transform;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
|
|||
|
|
CGFloat angle = 0.0;
|
|||
|
|
switch (orientation) {
|
|||
|
|
case AVCaptureVideoOrientationPortrait:
|
|||
|
|
angle = 0.0;
|
|||
|
|
break;
|
|||
|
|
case AVCaptureVideoOrientationPortraitUpsideDown:
|
|||
|
|
angle = M_PI;
|
|||
|
|
break;
|
|||
|
|
case AVCaptureVideoOrientationLandscapeRight:
|
|||
|
|
angle = -M_PI_2;
|
|||
|
|
break;
|
|||
|
|
case AVCaptureVideoOrientationLandscapeLeft:
|
|||
|
|
angle = M_PI_2;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
return angle;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)removeFile:(NSURL *)fileURL {
|
|||
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|||
|
|
NSString *filePath = fileURL.path;
|
|||
|
|
if ([fileManager fileExistsAtPath:filePath]) {
|
|||
|
|
NSError *error;
|
|||
|
|
BOOL success = [fileManager removeItemAtPath:filePath error:&error];
|
|||
|
|
if (!success) {
|
|||
|
|
NSAssert(NO, error.localizedDescription);
|
|||
|
|
NSLog(@"Failed to delete file:%@", error);
|
|||
|
|
} else {
|
|||
|
|
NSLog(@"Succeed to delete file");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@end
|