Files
featherVoice/TUIKit/TUIChat/CommonUI/Camera/TUIMovieManager.m

236 lines
9.0 KiB
Mathematica
Raw Permalink Normal View History

2025-08-08 10:49:36 +08:00
// 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