KSVideoRecordManager.m 15 KB

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