KSVideoRecordManager.m 17 KB

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