KSVideoRecordManager.m 19 KB

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