TUIMovieManager.m 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. // Created by Tencent on 2023/06/09.
  2. // Copyright © 2023 Tencent. All rights reserved.
  3. #import "TUIMovieManager.h"
  4. #import <TIMCommon/TIMDefine.h>
  5. @interface TUIMovieManager () {
  6. BOOL _readyToRecordVideo;
  7. BOOL _readyToRecordAudio;
  8. dispatch_queue_t _movieWritingQueue;
  9. NSURL *_movieURL;
  10. AVAssetWriter *_movieWriter;
  11. AVAssetWriterInput *_movieAudioInput;
  12. AVAssetWriterInput *_movieVideoInput;
  13. }
  14. @end
  15. @implementation TUIMovieManager
  16. - (instancetype)init {
  17. self = [super init];
  18. if (self) {
  19. _movieWritingQueue = dispatch_queue_create("com.tui.Movie.Writing.Queue", DISPATCH_QUEUE_SERIAL);
  20. _movieURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"TUICaptureTempMovie.mp4"]];
  21. _referenceOrientation = AVCaptureVideoOrientationPortrait;
  22. }
  23. return self;
  24. }
  25. - (void)start:(void (^)(NSError *error))handle {
  26. @weakify(self);
  27. dispatch_async(_movieWritingQueue, ^{
  28. @strongify(self);
  29. [self removeFile:self->_movieURL];
  30. NSError *error;
  31. if (!self->_movieWriter) {
  32. self->_movieWriter = [[AVAssetWriter alloc] initWithURL:self->_movieURL fileType:AVFileTypeMPEG4 error:&error];
  33. }
  34. handle(error);
  35. });
  36. }
  37. - (void)stop:(void (^)(NSURL *url, NSError *error))handle {
  38. @weakify(self);
  39. dispatch_async(_movieWritingQueue, ^{
  40. @strongify(self);
  41. self->_readyToRecordVideo = NO;
  42. self->_readyToRecordAudio = NO;
  43. if (self->_movieWriter && self->_movieWriter.status == AVAssetWriterStatusWriting) {
  44. @weakify(self);
  45. [self->_movieWriter finishWritingWithCompletionHandler:^() {
  46. @strongify(self);
  47. @weakify(self);
  48. dispatch_async(dispatch_get_main_queue(), ^{
  49. @strongify(self);
  50. if (self->_movieWriter.status == AVAssetWriterStatusCompleted) {
  51. handle(self->_movieURL, nil);
  52. } else {
  53. handle(nil, self->_movieWriter.error);
  54. }
  55. self->_movieWriter = nil;
  56. });
  57. }];
  58. } else {
  59. [self->_movieWriter cancelWriting];
  60. self->_movieWriter = nil;
  61. dispatch_async(dispatch_get_main_queue(), ^{
  62. handle(nil, [NSError errorWithDomain:@"com.tui.Movie.Writing" code:0 userInfo:@{NSLocalizedDescriptionKey : @"AVAssetWriter status error"}]);
  63. });
  64. }
  65. });
  66. }
  67. - (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer {
  68. CFRetain(buffer);
  69. @weakify(self);
  70. dispatch_async(_movieWritingQueue, ^{
  71. @strongify(self);
  72. if (connection == video) {
  73. if (!self->_readyToRecordVideo) {
  74. self->_readyToRecordVideo = [self setupAssetWriterVideoInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
  75. }
  76. if ([self inputsReadyToRecord]) {
  77. [self writeSampleBuffer:buffer ofType:AVMediaTypeVideo];
  78. }
  79. } else if (connection == audio) {
  80. if (!self->_readyToRecordAudio) {
  81. self->_readyToRecordAudio = [self setupAssetWriterAudioInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
  82. }
  83. if ([self inputsReadyToRecord]) {
  84. [self writeSampleBuffer:buffer ofType:AVMediaTypeAudio];
  85. }
  86. }
  87. CFRelease(buffer);
  88. });
  89. }
  90. - (void)writeSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType {
  91. if (_movieWriter.status == AVAssetWriterStatusUnknown) {
  92. if ([_movieWriter startWriting]) {
  93. [_movieWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
  94. } else {
  95. NSLog(@"%@", _movieWriter.error);
  96. }
  97. }
  98. if (_movieWriter.status == AVAssetWriterStatusWriting) {
  99. if (mediaType == AVMediaTypeVideo) {
  100. if (!_movieVideoInput.isReadyForMoreMediaData) {
  101. return;
  102. }
  103. if (![_movieVideoInput appendSampleBuffer:sampleBuffer]) {
  104. NSLog(@"%@", _movieWriter.error);
  105. }
  106. } else if (mediaType == AVMediaTypeAudio) {
  107. if (!_movieAudioInput.isReadyForMoreMediaData) {
  108. return;
  109. }
  110. if (![_movieAudioInput appendSampleBuffer:sampleBuffer]) {
  111. NSLog(@"%@", _movieWriter.error);
  112. }
  113. }
  114. }
  115. }
  116. - (BOOL)inputsReadyToRecord {
  117. return _readyToRecordVideo && _readyToRecordAudio;
  118. }
  119. - (NSError *)setupAssetWriterAudioInput:(CMFormatDescriptionRef)currentFormatDescription {
  120. size_t aclSize = 0;
  121. const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription);
  122. const AudioChannelLayout *channelLayout = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize);
  123. NSData *dataLayout = aclSize > 0 ? [NSData dataWithBytes:channelLayout length:aclSize] : [NSData data];
  124. NSDictionary *settings = @{
  125. AVFormatIDKey : [NSNumber numberWithInteger:kAudioFormatMPEG4AAC],
  126. AVSampleRateKey : [NSNumber numberWithFloat:currentASBD->mSampleRate],
  127. AVChannelLayoutKey : dataLayout,
  128. AVNumberOfChannelsKey : [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame],
  129. AVEncoderBitRatePerChannelKey : [NSNumber numberWithInt:64000]
  130. };
  131. if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeAudio]) {
  132. _movieAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:settings];
  133. _movieAudioInput.expectsMediaDataInRealTime = YES;
  134. if ([_movieWriter canAddInput:_movieAudioInput]) {
  135. [_movieWriter addInput:_movieAudioInput];
  136. } else {
  137. return _movieWriter.error;
  138. }
  139. } else {
  140. return _movieWriter.error;
  141. }
  142. return nil;
  143. }
  144. - (NSError *)setupAssetWriterVideoInput:(CMFormatDescriptionRef)currentFormatDescription {
  145. CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription);
  146. NSUInteger numPixels = dimensions.width * dimensions.height;
  147. CGFloat bitsPerPixel = numPixels < (640 * 480) ? 4.05 : 11.0;
  148. NSDictionary *compression =
  149. @{AVVideoAverageBitRateKey : [NSNumber numberWithInteger:numPixels * bitsPerPixel], AVVideoMaxKeyFrameIntervalKey : [NSNumber numberWithInteger:30]};
  150. NSDictionary *settings = @{
  151. AVVideoCodecKey : AVVideoCodecH264,
  152. AVVideoWidthKey : [NSNumber numberWithInteger:dimensions.width],
  153. AVVideoHeightKey : [NSNumber numberWithInteger:dimensions.height],
  154. AVVideoCompressionPropertiesKey : compression
  155. };
  156. if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeVideo]) {
  157. _movieVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:settings];
  158. _movieVideoInput.expectsMediaDataInRealTime = YES;
  159. _movieVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
  160. if ([_movieWriter canAddInput:_movieVideoInput]) {
  161. [_movieWriter addInput:_movieVideoInput];
  162. } else {
  163. return _movieWriter.error;
  164. }
  165. } else {
  166. return _movieWriter.error;
  167. }
  168. return nil;
  169. }
  170. - (CGAffineTransform)transformFromCurrentVideoOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
  171. CGFloat orientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:orientation];
  172. CGFloat videoOrientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:self.currentOrientation];
  173. CGFloat angleOffset;
  174. if (self.currentDevice.position == AVCaptureDevicePositionBack) {
  175. angleOffset = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
  176. } else {
  177. angleOffset = orientationAngleOffset - videoOrientationAngleOffset + M_PI_2;
  178. }
  179. CGAffineTransform transform = CGAffineTransformMakeRotation(angleOffset);
  180. return transform;
  181. }
  182. - (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
  183. CGFloat angle = 0.0;
  184. switch (orientation) {
  185. case AVCaptureVideoOrientationPortrait:
  186. angle = 0.0;
  187. break;
  188. case AVCaptureVideoOrientationPortraitUpsideDown:
  189. angle = M_PI;
  190. break;
  191. case AVCaptureVideoOrientationLandscapeRight:
  192. angle = -M_PI_2;
  193. break;
  194. case AVCaptureVideoOrientationLandscapeLeft:
  195. angle = M_PI_2;
  196. break;
  197. }
  198. return angle;
  199. }
  200. - (void)removeFile:(NSURL *)fileURL {
  201. NSFileManager *fileManager = [NSFileManager defaultManager];
  202. NSString *filePath = fileURL.path;
  203. if ([fileManager fileExistsAtPath:filePath]) {
  204. NSError *error;
  205. BOOL success = [fileManager removeItemAtPath:filePath error:&error];
  206. if (!success) {
  207. NSAssert(NO, error.localizedDescription);
  208. NSLog(@"Failed to delete file:%@", error);
  209. } else {
  210. NSLog(@"Succeed to delete file");
  211. }
  212. }
  213. }
  214. @end