KSVideoRecordManager.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. //
  2. // KSVideoRecordManager.m
  3. // TeacherDaya
  4. //
  5. // Created by Kyle on 2021/8/16.
  6. // Copyright © 2021 DayaMusic. All rights reserved.
  7. //
  8. #import "KSVideoRecordManager.h"
  9. #import <AVFoundation/AVFoundation.h>
  10. #import <AssetsLibrary/AssetsLibrary.h>
  11. #import "TZImageManager.h"
  12. #import "KSVideoEditor.h"
  13. @interface KSVideoRecordManager ()<AVCaptureFileOutputRecordingDelegate>
  14. //会话 负责输入和输出设备之间的数据传递
  15. @property (nonatomic, strong) AVCaptureSession *captureSession;
  16. @property (nonatomic, strong) AVCaptureDeviceInput *videoCaptureDeviceInput;
  17. @property (nonatomic, strong) AVCaptureDeviceInput *audioCaptureDeviceInput;
  18. // 视频流输出
  19. @property (nonatomic, strong) AVCaptureMovieFileOutput *captureMovieFileOutput;
  20. // 相机拍摄预览图层
  21. @property (nonatomic, strong) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;
  22. @property (nonatomic, strong) NSURL *videoFileURL;
  23. @property (nonatomic, assign) BOOL recordEnable;
  24. // 是否正在录制
  25. @property (nonatomic, assign) BOOL isRecording;
  26. @property (nonatomic, copy) KSVideoRecordCallback callback;
  27. @property (nonatomic, strong) PHAsset *videoAsset;
  28. @property (nonatomic, strong) NSString *presentName;
  29. @property (strong, nonatomic) MBProgressHUD *HUD;
  30. @property (nonatomic, strong) dispatch_queue_t videoRecordQueue;
  31. @property (nonatomic, assign) BOOL isChangeSession;
  32. @end
  33. @implementation KSVideoRecordManager
  34. - (instancetype)initSessionRecordCallback:(KSVideoRecordCallback)callback {
  35. self = [super init];
  36. if (self) {
  37. if (callback) {
  38. self.callback = callback;
  39. }
  40. }
  41. return self;
  42. }
  43. - (void)setIgnoreAudio:(BOOL)ignoreAudio {
  44. _ignoreAudio = ignoreAudio;
  45. [self resetSession];
  46. }
  47. - (BOOL)getSessionStatusisActive {
  48. if (self.captureSession && self.captureSession.isRunning) {
  49. return YES;
  50. }
  51. else {
  52. return NO;
  53. }
  54. }
  55. - (void)configSessiondisplayInView:(UIView *)containerView {
  56. _captureSession = [[AVCaptureSession alloc] init];
  57. // 设置YES 播放web伴奏会导致打断
  58. _captureSession.automaticallyConfiguresApplicationAudioSession = NO;
  59. // 初始化会话对象
  60. if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetiFrame1280x720]) {
  61. _captureSession.sessionPreset = AVCaptureSessionPresetiFrame1280x720;
  62. }
  63. else if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetInputPriority]) {
  64. _captureSession.sessionPreset = AVCaptureSessionPresetInputPriority;
  65. }
  66. NSError *error = nil;
  67. // 获取视频输出对象
  68. AVCaptureDevice *videoCaptureDevice = [self cameraDeviceWithPosition:(AVCaptureDevicePositionFront)];
  69. if (!videoCaptureDevice) {
  70. if (self.callback) {
  71. self.callback(NO, @"获取摄像头失败!");
  72. }
  73. }
  74. _videoCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoCaptureDevice error:&error];
  75. if (error) {
  76. if (self.callback) {
  77. self.callback(NO, @"获取视频设备输入出错!");
  78. }
  79. return;
  80. }
  81. if ([_captureSession canAddInput:_videoCaptureDeviceInput]) {
  82. [_captureSession addInput:_videoCaptureDeviceInput];
  83. // 设置帧率
  84. [self setMaxFrameRate:10 forDevice:_videoCaptureDeviceInput.device];
  85. }
  86. else {
  87. if (self.callback) {
  88. self.callback(NO, @"摄像头被占用!");
  89. }
  90. }
  91. if (_ignoreAudio == NO) {
  92. // 获取音频输入对象
  93. AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
  94. _audioCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error];
  95. if (error) {
  96. if (self.callback) {
  97. self.callback(NO, @"获取音频设备输入出错!");
  98. }
  99. return;
  100. }
  101. //将设备输入添加到会话中
  102. if ([_captureSession canAddInput:_audioCaptureDeviceInput]) {
  103. [_captureSession addInput:_audioCaptureDeviceInput];
  104. }
  105. else {
  106. if (self.callback) {
  107. self.callback(NO, @"麦克风被占用!");
  108. }
  109. }
  110. }
  111. // 初始化设备输出对象
  112. _captureMovieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
  113. _captureMovieFileOutput.movieFragmentInterval = kCMTimeInvalid;
  114. //将设备输出添加到会话中
  115. if ([_captureSession canAddOutput:_captureMovieFileOutput]) {
  116. AVCaptureConnection *captureConnection = [_captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
  117. //防抖功能
  118. if ([captureConnection isVideoStabilizationSupported]) {
  119. captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
  120. }
  121. [_captureSession addOutput:_captureMovieFileOutput];
  122. }
  123. //创建视频预览图层
  124. _captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
  125. containerView.layer.masksToBounds = YES;
  126. _captureVideoPreviewLayer.frame = containerView.bounds;
  127. _captureVideoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
  128. _captureVideoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
  129. [containerView.layer addSublayer:_captureVideoPreviewLayer];
  130. // 一定要在添加了 input 和 output之后~
  131. AVCaptureConnection *captureConnection = [_captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
  132. captureConnection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
  133. [self startSession];
  134. }
  135. - (void)removeDisplay {
  136. [self stopSession];
  137. if (_captureVideoPreviewLayer) {
  138. [_captureVideoPreviewLayer removeFromSuperlayer];
  139. }
  140. }
  141. - (void)resetSession {
  142. if ([_captureSession isRunning]) {
  143. if (_ignoreAudio == NO) {
  144. [_captureSession beginConfiguration];
  145. self.isChangeSession = YES;
  146. if (_audioCaptureDeviceInput) {
  147. [_captureSession removeInput:_audioCaptureDeviceInput];
  148. }
  149. NSError *error = nil;
  150. // 获取音频输入对象
  151. AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
  152. _audioCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error];
  153. if (error) {
  154. if (self.callback) {
  155. self.callback(NO, @"获取音频设备输入出错!");
  156. }
  157. return;
  158. }
  159. if ([_captureSession canAddInput:_audioCaptureDeviceInput]) {
  160. [_captureSession addInput:_audioCaptureDeviceInput];
  161. }
  162. else {
  163. if (self.callback) {
  164. self.callback(NO, @"麦克风被占用!");
  165. }
  166. }
  167. [_captureSession commitConfiguration];
  168. self.isChangeSession = NO;
  169. }
  170. }
  171. else {
  172. [self startSession];
  173. }
  174. }
  175. - (void)startSession {
  176. @weakObj(self);
  177. dispatch_async(self.videoRecordQueue, ^{
  178. @strongObj(self);
  179. if (!self.captureSession.running) {
  180. [self.captureSession startRunning];
  181. }
  182. });
  183. }
  184. - (void)stopSession {
  185. if (_captureSession && _isChangeSession == NO) {
  186. @weakObj(self);
  187. dispatch_async(self.videoRecordQueue, ^{
  188. @strongObj(self);
  189. if (self.captureSession.running) {
  190. [self.captureSession stopRunning];
  191. }
  192. });
  193. self.captureSession = nil;
  194. }
  195. }
  196. - (void)startRecord {
  197. if (_captureMovieFileOutput) {
  198. [self clearVideoFile];
  199. // 开始录制
  200. [self.captureMovieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:[self getRecordFilePath]] recordingDelegate:self];
  201. }
  202. }
  203. - (void)stopRecord {
  204. if (_captureMovieFileOutput) {
  205. [self.captureMovieFileOutput stopRecording];
  206. }
  207. [self resetSession];
  208. }
  209. - (void)removeVideoWithPath:(NSString *)videoUrl {
  210. NSFileManager *fileMamager = [NSFileManager defaultManager];
  211. if ([fileMamager fileExistsAtPath:videoUrl]) {
  212. [fileMamager removeItemAtPath:videoUrl error:nil];
  213. }
  214. }
  215. /**取得指定位置的摄像头*/
  216. - (AVCaptureDevice *)cameraDeviceWithPosition:(AVCaptureDevicePosition)position {
  217. NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
  218. for (AVCaptureDevice *camera in cameras) {
  219. if ([camera position] == position) {
  220. return camera;
  221. }
  222. }
  223. return nil;
  224. }
  225. // 切换摄像头
  226. - (void)swapCameras {
  227. // Assume the session is already running
  228. NSArray *inputs =self.captureSession.inputs;
  229. for (AVCaptureDeviceInput *input in inputs ) {
  230. AVCaptureDevice *device = input.device;
  231. if ( [device hasMediaType:AVMediaTypeVideo] ) {
  232. AVCaptureDevicePosition position = device.position;
  233. AVCaptureDevice *newCamera =nil;
  234. AVCaptureDeviceInput *newInput =nil;
  235. if (position ==AVCaptureDevicePositionFront)
  236. newCamera = [self cameraDeviceWithPosition:AVCaptureDevicePositionBack];
  237. else
  238. newCamera = [self cameraDeviceWithPosition:AVCaptureDevicePositionFront];
  239. newInput = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:nil];
  240. // beginConfiguration ensures that pending changes are not applied immediately
  241. [self.captureSession beginConfiguration];
  242. [self.captureSession removeInput:input];
  243. [self.captureSession addInput:newInput];
  244. // Changes take effect once the outermost commitConfiguration is invoked.
  245. [self.captureSession commitConfiguration];
  246. break;
  247. }
  248. }
  249. }
  250. #pragma mark -------- AVCaptureFileOutputRecordingDelegate ----------
  251. - (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections {
  252. NSLog(@"开始录制");
  253. _isRecording = YES;
  254. }
  255. - (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error {
  256. if (error) {
  257. NSLog(@"error desc :%@", error.description);
  258. }
  259. NSLog(@"录制结束");
  260. _isRecording = NO;
  261. if (_isChangeSession == NO) {
  262. @weakObj(self);
  263. dispatch_async(self.videoRecordQueue, ^{
  264. @strongObj(self);
  265. if (self.captureSession.running) {
  266. [self.captureSession stopRunning];
  267. }
  268. });
  269. }
  270. // 暂时存储文件地址
  271. self.videoFileURL = outputFileURL;
  272. [self resetSession];
  273. self.skipSaveRecord = NO;
  274. if (self.callback) {
  275. self.callback(YES, @"");
  276. }
  277. }
  278. - (void)saveVideoCallback:(KSVideoRecordCallback)callback {
  279. // 保存文件
  280. if (self.skipSaveRecord == NO) {
  281. if (_ignoreAudio == NO) {
  282. [self saveVideoToAsset:self.videoFileURL callback:^(BOOL isSuccess, NSString * _Nullable message) {
  283. callback(isSuccess, message);
  284. }];
  285. }
  286. else {
  287. [self addBackgroundMuisc:self.audioUrl callback:^(BOOL isSuccess, NSString * _Nullable message) {
  288. callback(isSuccess, message);
  289. }];
  290. }
  291. }
  292. }
  293. // 生成文件 合并音轨
  294. - (void)addBackgroundMuisc:(NSURL *)audioUrl callback:(KSVideoRecordCallback)callback {
  295. AVURLAsset* audioAsset =[AVURLAsset URLAssetWithURL:audioUrl options:nil];
  296. CMTime audioDuration = audioAsset.duration;
  297. float audioDurationSeconds = CMTimeGetSeconds(audioDuration);
  298. NSLog(@"%f",audioDurationSeconds);
  299. [KSVideoEditor addBackgroundMiusicWithVideoUrlStr:self.videoFileURL audioUrl:audioUrl bgAudioUrl:self.bgAudioUrl start:0 end:audioDurationSeconds isOriginalSound:NO oriVolume:0 newVolume:100 completion:^(NSString * _Nonnull outPath, BOOL isSuccess) {
  300. if (isSuccess) {
  301. [self saveVideoToAsset:[NSURL fileURLWithPath:outPath] callback:^(BOOL isSuccess, NSString * _Nullable message) {
  302. if (callback) {
  303. callback(isSuccess, message);
  304. }
  305. }];
  306. }
  307. else {
  308. }
  309. }];
  310. }
  311. // 保存到相册
  312. - (void)saveVideoToAsset:(NSURL *)videoUrl callback:(KSVideoRecordCallback)callback {
  313. [LOADING_MANAGER MBShowInWindow:@"视频处理中..."];
  314. [[TZImageManager manager] saveVideoWithUrl:videoUrl completion:^(PHAsset *asset, NSError *error) {
  315. if (!error) {
  316. self.videoAsset = asset;
  317. dispatch_main_async_safe(^{
  318. [LOADING_MANAGER removeHUD];
  319. if (callback) {
  320. callback(YES, @"已保存到相册");
  321. }
  322. else if (self.callback) {
  323. self.callback(YES, @"已保存到相册");
  324. }
  325. // 重置
  326. [self resetSession];
  327. });
  328. }
  329. else {
  330. dispatch_main_async_safe(^{
  331. [LOADING_MANAGER removeHUD];
  332. if (callback) {
  333. callback(NO, @"保存视频错误");
  334. }
  335. else if (self.callback) {
  336. self.callback(NO, @"保存视频错误");
  337. }
  338. // 重置
  339. [self resetSession];
  340. });
  341. }
  342. }];
  343. }
  344. - (void)clearVideoFile {
  345. if (_isRecording) {
  346. return;
  347. }
  348. NSURL *fileUrl = [NSURL fileURLWithPath:[self getRecordFilePath]];
  349. if (fileUrl) {
  350. [self removeVideoWithPath:fileUrl.path];
  351. }
  352. self.videoFileURL = nil;
  353. }
  354. // 上传视频
  355. - (void)uploadRecordVideoSuccess:(void (^)(NSString * _Nonnull))success failure:(void (^)(NSString * _Nonnull))faliure {
  356. if (self.videoAsset) {
  357. [[TZImageManager manager] getVideoOutputPathWithAsset:self.videoAsset presetName:self.presentName success:^(NSString *outputPath) {
  358. NSLog(@"视频导出到本地完成,沙盒路径为:%@",outputPath);
  359. NSData *outputData = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:outputPath]]; //压缩后的视频
  360. NSLog(@"导出后的视频:%@",[NSString stringWithFormat:@"%.2fM",(CGFloat)outputData.length/(1024*1024)]);
  361. // 上传
  362. dispatch_main_async_safe(^{
  363. [self sendVideoActionWith:outputPath success:success failure:faliure];
  364. });
  365. } failure:^(NSString *errorMessage, NSError *error) {
  366. dispatch_main_async_safe(^{
  367. faliure(@"视频导出失败");
  368. });
  369. NSLog(@"视频导出失败:%@,error:%@",errorMessage, error);
  370. }];
  371. }
  372. else {
  373. faliure(@"未找到视频资源");
  374. }
  375. }
  376. - (void)sendVideoActionWith:(NSString *)fileUrl success:(void (^)(NSString * _Nonnull))success failure:(void (^)(NSString * _Nonnull))faliure {
  377. [self hudTipWillShow:YES];
  378. NSData *fileData = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:fileUrl]];
  379. NSString *suffix = [NSString stringWithFormat:@".%@",[fileUrl pathExtension]];
  380. [[KSUploadManager shareInstance] configBucketName:@"daya"];
  381. [[KSUploadManager shareInstance] videoUpload:fileData fileName:@"video" fileSuffix:suffix progress:^(int64_t bytesWritten, int64_t totalBytes) {
  382. // 显示进度
  383. if (self.HUD) {
  384. self.HUD.progress = bytesWritten / totalBytes;// progress是回调进度
  385. }
  386. } successCallback:^(NSMutableArray * _Nonnull fileUrlArray) {
  387. [self hudTipWillShow:NO];
  388. NSString *fileUrl = [fileUrlArray lastObject];
  389. success(fileUrl);
  390. } faliure:^(NSError * _Nullable error, NSString * _Nullable descMessaeg) {
  391. [self hudTipWillShow:NO];
  392. faliure(descMessaeg);
  393. }];
  394. }
  395. - (void)hudTipWillShow:(BOOL)willShow{
  396. if (willShow) {
  397. UIWindow *keyWindow = [NSObject getKeyWindow];
  398. if (!_HUD) {
  399. _HUD = [MBProgressHUD showHUDAddedTo:keyWindow animated:YES];
  400. _HUD.label.textColor = [UIColor whiteColor];
  401. _HUD.mode = MBProgressHUDModeDeterminateHorizontalBar;
  402. _HUD.label.text = @"正在上传视频...";
  403. _HUD.contentColor = [UIColor whiteColor];
  404. _HUD.removeFromSuperViewOnHide = YES;
  405. _HUD.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
  406. _HUD.bezelView.backgroundColor = [UIColor colorWithHexString:@"#000000" alpha:0.8];
  407. }else{
  408. _HUD.progress = 0;
  409. [keyWindow addSubview:_HUD];
  410. [_HUD showAnimated:YES];
  411. }
  412. }else{
  413. [_HUD hideAnimated:YES];
  414. }
  415. }
  416. #pragma mark ------ 设置录制地址
  417. - (NSString *)getRecordFilePath {
  418. NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject] stringByAppendingPathComponent:@"AccompanyVideoData"];
  419. NSFileManager *fileManager = [NSFileManager defaultManager];
  420. BOOL isDir = FALSE;
  421. BOOL isDirExist = [fileManager fileExistsAtPath:path isDirectory:&isDir];
  422. if(!(isDirExist && isDir)) {
  423. BOOL bCreateDir = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
  424. if(!bCreateDir){
  425. NSLog(@"创建文件夹失败!");
  426. }
  427. NSLog(@"创建文件夹成功,文件路径%@",path);
  428. }
  429. NSString *songName = @"recordSong";
  430. NSString *fileName = [NSString stringWithFormat:@"%@.mp4",songName];
  431. NSString *filePath = [path stringByAppendingPathComponent:fileName];
  432. return filePath;
  433. }
  434. - (dispatch_queue_t)videoRecordQueue {
  435. if (!_videoRecordQueue) {
  436. _videoRecordQueue = dispatch_queue_create("com.Colexiu.videoRecord", DISPATCH_QUEUE_SERIAL);
  437. }
  438. return _videoRecordQueue;
  439. }
  440. - (void)setMaxFrameRate:(Float64)maxFrameRate forDevice:(AVCaptureDevice *)device {
  441. @try
  442. {
  443. NSError *lockError = nil;
  444. if ([device lockForConfiguration:&lockError])
  445. {
  446. NSArray *videoSupportedFrameRateRanges = device.activeFormat.videoSupportedFrameRateRanges;
  447. AVFrameRateRange *minFrameRateRange = videoSupportedFrameRateRanges.firstObject;
  448. for (AVFrameRateRange *range in device.activeFormat.videoSupportedFrameRateRanges)
  449. {
  450. if (range.minFrameRate < minFrameRateRange.minFrameRate)
  451. {
  452. minFrameRateRange = range;
  453. }
  454. }
  455. CMTime minFrameDuration = CMTimeMake(1, maxFrameRate);
  456. //超出范围就取默认值
  457. if (maxFrameRate < minFrameRateRange.minFrameRate || maxFrameRate > minFrameRateRange.maxFrameRate)
  458. {
  459. minFrameDuration = kCMTimeInvalid;
  460. }
  461. device.activeVideoMinFrameDuration = minFrameDuration;
  462. [device unlockForConfiguration];
  463. NSLog(@"OnboardingScan--打印帧数最小: %@, 最大:%@", @((int32_t)device.activeVideoMinFrameDuration.timescale).stringValue, @((int32_t)device.activeVideoMaxFrameDuration.timescale).stringValue);
  464. }
  465. } @catch (NSException *exception) {
  466. } @finally {
  467. }
  468. }
  469. @end