TUIChatMediaDataProvider.m 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. //
  2. // TUIChatMediaDataProvider.m
  3. // TUIChat
  4. //
  5. // Created by harvy on 2022/12/20.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. #import "TUIChatMediaDataProvider.h"
  9. #import <AssetsLibrary/AssetsLibrary.h>
  10. #import <MobileCoreServices/MobileCoreServices.h>
  11. #import <Photos/Photos.h>
  12. #import <PhotosUI/PhotosUI.h>
  13. #import <SDWebImage/SDWebImage.h>
  14. #import <TIMCommon/TIMDefine.h>
  15. #import <TIMCommon/TUIUserAuthorizationCenter.h>
  16. #import <TIMCommon/NSTimer+TUISafe.h>
  17. #import <TUICore/TUITool.h>
  18. #import <TUICore/TUICore.h>
  19. #import "TUICameraViewController.h"
  20. #import "TUIChatConfig.h"
  21. #import "AlbumPicker.h"
  22. #import "MultimediaRecorder.h"
  23. #define kTUIChatMediaSelectImageMax 9
  24. @interface TUIChatMediaDataProvider () <PHPickerViewControllerDelegate,
  25. UINavigationControllerDelegate,
  26. UIImagePickerControllerDelegate,
  27. UIDocumentPickerDelegate,
  28. TUICameraViewControllerDelegate>
  29. @end
  30. @implementation TUIChatMediaDataProvider
  31. #pragma mark - Public API
  32. - (void)selectPhoto {
  33. if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
  34. __weak typeof(self) weakSelf = self;
  35. __strong typeof(weakSelf.listener) strongListener = weakSelf.listener;
  36. [[AlbumPicker sharedInstance].advancedAlbumPicker pickMediaWithCaller:self.presentViewController originalMediaPicked:^(NSDictionary *param) {
  37. if (param) {
  38. NSString * type = param[@"type"];
  39. if ([type isEqualToString:@"image"]) {
  40. // image do nothing
  41. }
  42. else if ([type isEqualToString:@"video"]) {
  43. TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
  44. if ([strongListener respondsToSelector:@selector(sendPlaceHolderUIMessage:)]) {
  45. [strongListener sendPlaceHolderUIMessage:placeHolderCellData];
  46. }
  47. TUIChatMediaTask * task = [[TUIChatMediaTask alloc] init];
  48. task.placeHolderCellData = placeHolderCellData;
  49. task.msgID = placeHolderCellData.msgID;
  50. task.conversationID = weakSelf.conversationID;
  51. if (placeHolderCellData.msgID.length > 0) {
  52. [TUIChatMediaSendingManager.sharedInstance addMediaTask: task forKey:placeHolderCellData.msgID];
  53. }
  54. }
  55. else {
  56. // do nothing
  57. }
  58. }
  59. } progressCallback:^(NSDictionary *param) {
  60. NSLog(@"%@,strongListener:%@",param,strongListener);
  61. } finishedCallback:^(NSDictionary *param) {
  62. if (param) {
  63. V2TIMMessage * message = param[@"message"];
  64. NSString * type = param[@"type"];
  65. if ([type isEqualToString:@"image"]) {
  66. if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
  67. [strongListener sendMessage:message placeHolderCellData:nil];
  68. }
  69. }
  70. else if ([type isEqualToString:@"video"]) {
  71. TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
  72. if (placeHolderCellData.msgID.length > 0) {
  73. [TUIChatMediaSendingManager.sharedInstance removeMediaTaskForKey:placeHolderCellData.msgID];
  74. }
  75. BOOL canSendByCurrentPage = NO;
  76. for (id<TUIChatMediaDataListener> currentVC in TUIChatMediaSendingManager.sharedInstance.mediaSendingControllers) {
  77. if ([currentVC.currentConversationID isEqualToString:self.conversationID]&&
  78. [currentVC respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
  79. if (currentVC.isPageAppears) {
  80. [currentVC sendMessage:message placeHolderCellData:placeHolderCellData];
  81. canSendByCurrentPage = YES;
  82. break;
  83. }
  84. }
  85. }
  86. if (!canSendByCurrentPage) {
  87. if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
  88. [strongListener sendMessage:message placeHolderCellData:placeHolderCellData];
  89. }
  90. }
  91. }
  92. else {
  93. // do nothing
  94. }
  95. }
  96. }];
  97. }
  98. else {
  99. //defalut AlbumPicker
  100. [self _selectPhoto];
  101. }
  102. }
  103. - (void)_selectPhoto {
  104. dispatch_async(dispatch_get_main_queue(), ^{
  105. if (@available(iOS 14.0, *)) {
  106. PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init];
  107. configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] ]];
  108. configuration.selectionLimit = kTUIChatMediaSelectImageMax;
  109. PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:configuration];
  110. picker.delegate = self;
  111. picker.modalPresentationStyle = UIModalPresentationFullScreen;
  112. picker.view.backgroundColor = [UIColor whiteColor];
  113. [self.presentViewController presentViewController:picker animated:YES completion:nil];
  114. } else {
  115. if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
  116. UIImagePickerController *picker = [[UIImagePickerController alloc] init];
  117. picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
  118. picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
  119. picker.delegate = self;
  120. [self.presentViewController presentViewController:picker animated:YES completion:nil];
  121. }
  122. }
  123. });
  124. }
  125. - (void)takePicture {
  126. if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
  127. [[MultimediaRecorder sharedInstance].advancedVideoRecorder takePhoneWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
  128. NSData *imageData = [NSData dataWithContentsOfURL:uri];
  129. UIImage *photo = [UIImage imageWithData:imageData];
  130. NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
  131. [[NSFileManager defaultManager] createFileAtPath:path
  132. contents:UIImagePNGRepresentation(photo) attributes:nil];
  133. if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
  134. [self.listener onProvideImage:path];
  135. }
  136. } failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
  137. }];
  138. }
  139. else {
  140. //defalut PhotoCamera
  141. [self _takePicture];
  142. }
  143. }
  144. - (void)_takePicture {
  145. __weak typeof(self) weakSelf = self;
  146. void (^actionBlock)(void) = ^(void) {
  147. TUICameraViewController *vc = [[TUICameraViewController alloc] init];
  148. vc.type = TUICameraMediaTypePhoto;
  149. vc.delegate = weakSelf;
  150. if (weakSelf.presentViewController.navigationController) {
  151. [weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
  152. } else {
  153. [weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
  154. }
  155. };
  156. if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
  157. dispatch_async(dispatch_get_main_queue(), ^{
  158. actionBlock();
  159. });
  160. } else {
  161. if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
  162. [TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
  163. dispatch_async(dispatch_get_main_queue(), ^{
  164. actionBlock();
  165. });
  166. }];
  167. };
  168. }
  169. }
  170. - (void)executeBlockWithMicroAndCameraAuth:(void(^)(void))block{
  171. if ([TUIUserAuthorizationCenter isEnableMicroAuthorization] && [TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
  172. dispatch_async(dispatch_get_main_queue(), block);
  173. } else {
  174. if (![TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
  175. [TUIUserAuthorizationCenter microStateActionWithPopCompletion:^{
  176. if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
  177. dispatch_async(dispatch_get_main_queue(), block);
  178. }
  179. }];
  180. }
  181. if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
  182. [TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
  183. if ([TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
  184. dispatch_async(dispatch_get_main_queue(), block);
  185. }
  186. }];
  187. }
  188. }
  189. }
  190. - (void)takeVideo {
  191. if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
  192. [[MultimediaRecorder sharedInstance].advancedVideoRecorder recordVideoWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
  193. if (uri) {
  194. if ([uri.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
  195. [self handleVideoPick:YES message:nil videoUrl:uri];
  196. return;
  197. }
  198. else if ([self isImageURL:uri]){
  199. NSData *imageData = [NSData dataWithContentsOfURL:uri];
  200. UIImage *photo = [UIImage imageWithData:imageData];
  201. NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
  202. [[NSFileManager defaultManager] createFileAtPath:path
  203. contents:UIImagePNGRepresentation(photo) attributes:nil];
  204. if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
  205. [self.listener onProvideImage:path];
  206. }
  207. }
  208. else {
  209. [self transcodeIfNeed:YES message:nil videoUrl:uri];
  210. }
  211. }
  212. } failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
  213. }];
  214. return;
  215. }
  216. else {
  217. //defalut VideoRecorder
  218. [self _takeVideo];
  219. }
  220. }
  221. - (void)_takeVideo {
  222. __weak typeof(self) weakSelf = self;
  223. void (^actionBlock)(void) = ^(void) {
  224. TUICameraViewController *vc = [[TUICameraViewController alloc] init];
  225. vc.type = TUICameraMediaTypeVideo;
  226. vc.videoMinimumDuration = 1.5;
  227. vc.delegate = weakSelf;
  228. if ([TUIChatConfig defaultConfig].maxVideoRecordDuration > 0) {
  229. vc.videoMaximumDuration = [TUIChatConfig defaultConfig].maxVideoRecordDuration;
  230. }
  231. if (weakSelf.presentViewController.navigationController) {
  232. [weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
  233. } else {
  234. [weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
  235. }
  236. };
  237. [self executeBlockWithMicroAndCameraAuth:actionBlock];
  238. }
  239. - (void)selectFile {
  240. UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ (NSString *)kUTTypeData ]
  241. inMode:UIDocumentPickerModeOpen];
  242. picker.delegate = self;
  243. [self.presentViewController presentViewController:picker animated:YES completion:nil];
  244. }
  245. - (BOOL)isImageURL:(NSURL *)url {
  246. NSArray *imageExtensions = @[@"jpg", @"jpeg", @"png", @"gif", @"bmp", @"tiff", @"webp", @"heic"];
  247. NSString *pathExtension = url.pathExtension.lowercaseString;
  248. return [imageExtensions containsObject:pathExtension];
  249. }
  250. #pragma mark - Private Do task
  251. - (void)handleImagePick:(BOOL)succ message:(NSString *)message imageData:(NSData *)imageData {
  252. static NSDictionary *imageFormatExtensionMap = nil;
  253. if (imageFormatExtensionMap == nil) {
  254. imageFormatExtensionMap = @{
  255. @(SDImageFormatUndefined) : @"",
  256. @(SDImageFormatJPEG) : @"jpeg",
  257. @(SDImageFormatPNG) : @"png",
  258. @(SDImageFormatGIF) : @"gif",
  259. @(SDImageFormatTIFF) : @"tiff",
  260. @(SDImageFormatWebP) : @"webp",
  261. @(SDImageFormatHEIC) : @"heic",
  262. @(SDImageFormatHEIF) : @"heif",
  263. @(SDImageFormatPDF) : @"pdf",
  264. @(SDImageFormatSVG) : @"svg",
  265. @(SDImageFormatBMP) : @"bmp",
  266. @(SDImageFormatRAW) : @"raw"
  267. };
  268. }
  269. dispatch_async(dispatch_get_main_queue(), ^{
  270. if (succ == NO || imageData == nil) {
  271. if ([self.listener respondsToSelector:@selector(onProvideImageError:)]) {
  272. [self.listener onProvideImageError:message];
  273. }
  274. return;
  275. }
  276. UIImage *image = [UIImage imageWithData:imageData];
  277. NSData *data = UIImageJPEGRepresentation(image, 1);
  278. NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
  279. NSString *extenionName = [imageFormatExtensionMap objectForKey:@(image.sd_imageFormat)];
  280. if (extenionName.length > 0) {
  281. path = [path stringByAppendingPathExtension:extenionName];
  282. }
  283. int32_t imageFormatSizeMax = 28 * 1024 * 1024;
  284. if (image.sd_imageFormat == SDImageFormatGIF) {
  285. imageFormatSizeMax = 10 * 1024 * 1024;
  286. }
  287. if (imageData.length > imageFormatSizeMax) {
  288. if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
  289. [self.listener onProvideFileError:TIMCommonLocalizableString(TUIKitImageSizeCheckLimited)];
  290. }
  291. return;
  292. }
  293. if (image.sd_imageFormat != SDImageFormatGIF) {
  294. UIImage *newImage = image;
  295. UIImageOrientation imageOrientation = image.imageOrientation;
  296. CGFloat aspectRatio = MIN(1920 / image.size.width, 1920 / image.size.height);
  297. CGFloat aspectWidth = image.size.width * aspectRatio;
  298. CGFloat aspectHeight = image.size.height * aspectRatio;
  299. UIGraphicsBeginImageContext(CGSizeMake(aspectWidth, aspectHeight));
  300. [image drawInRect:CGRectMake(0, 0, aspectWidth, aspectHeight)];
  301. newImage = UIGraphicsGetImageFromCurrentImageContext();
  302. UIGraphicsEndImageContext();
  303. data = UIImageJPEGRepresentation(newImage, 0.75);
  304. }
  305. [[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil];
  306. if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
  307. [self.listener onProvideImage:path];
  308. }
  309. });
  310. }
  311. - (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url {
  312. if (succ == NO || url == nil) {
  313. [self handleVideoPick:NO message:message videoUrl:nil];
  314. return;
  315. }
  316. if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
  317. [self handleVideoPick:succ message:message videoUrl:url];
  318. return;
  319. }
  320. NSString *tempPath = NSTemporaryDirectory();
  321. NSURL *urlName = [url URLByDeletingPathExtension];
  322. NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
  323. NSFileManager *fileManager = [NSFileManager defaultManager];
  324. if ([fileManager fileExistsAtPath:newUrl.path]) {
  325. NSError *error;
  326. BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
  327. if (!success || error) {
  328. NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
  329. return;
  330. }
  331. }
  332. // mov to mp4
  333. AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
  334. AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
  335. exportSession.outputURL = newUrl;
  336. exportSession.outputFileType = AVFileTypeMPEG4;
  337. exportSession.shouldOptimizeForNetworkUse = YES;
  338. // intercept FirstTime VideoPicture
  339. NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
  340. AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
  341. NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
  342. AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
  343. gen.appliesPreferredTrackTransform = YES;
  344. gen.maximumSize = CGSizeMake(192, 192);
  345. NSError *error = nil;
  346. CMTime actualTime;
  347. CMTime time = CMTimeMakeWithSeconds(0.5, 30);
  348. CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
  349. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
  350. CGImageRelease(imageRef);
  351. dispatch_async(dispatch_get_main_queue(), ^{
  352. if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
  353. [self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:image Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
  354. [exportSession exportAsynchronouslyWithCompletionHandler:^{
  355. switch ([exportSession status]) {
  356. case AVAssetExportSessionStatusFailed:
  357. NSLog(@"Export session failed");
  358. break;
  359. case AVAssetExportSessionStatusCancelled:
  360. NSLog(@"Export canceled");
  361. break;
  362. case AVAssetExportSessionStatusCompleted: {
  363. // Video conversion finished
  364. NSLog(@"Successful!");
  365. [self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
  366. }
  367. break;
  368. default:
  369. break;
  370. }
  371. }];
  372. [NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
  373. if (exportSession.status == AVAssetExportSessionStatusExporting) {
  374. NSLog(@"exportSession.progress:%f",exportSession.progress);
  375. placeHolderCellData.videoTranscodingProgress = exportSession.progress;
  376. }
  377. }];
  378. }];
  379. }
  380. else {
  381. [exportSession exportAsynchronouslyWithCompletionHandler:^{
  382. switch ([exportSession status]) {
  383. case AVAssetExportSessionStatusCompleted: {
  384. // Video conversion finished
  385. NSLog(@"Successful!");
  386. [self handleVideoPick:succ message:message videoUrl:newUrl];
  387. } break;
  388. default:
  389. break;
  390. }
  391. }];
  392. }
  393. });
  394. }
  395. - (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url placeHolderCellData:(TUIMessageCellData*)placeHolderCellData {
  396. if (succ == NO || url == nil) {
  397. [self handleVideoPick:NO message:message videoUrl:nil];
  398. return;
  399. }
  400. if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
  401. [self handleVideoPick:succ message:message videoUrl:url];
  402. return;
  403. }
  404. NSString *tempPath = NSTemporaryDirectory();
  405. NSURL *urlName = [url URLByDeletingPathExtension];
  406. NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
  407. NSFileManager *fileManager = [NSFileManager defaultManager];
  408. if ([fileManager fileExistsAtPath:newUrl.path]) {
  409. NSError *error;
  410. BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
  411. if (!success || error) {
  412. NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
  413. return;
  414. }
  415. }
  416. // mov to mp4
  417. AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
  418. AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
  419. exportSession.outputURL = newUrl;
  420. exportSession.outputFileType = AVFileTypeMPEG4;
  421. exportSession.shouldOptimizeForNetworkUse = YES;
  422. // intercept FirstTime VideoPicture
  423. NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
  424. AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
  425. NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
  426. AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
  427. gen.appliesPreferredTrackTransform = YES;
  428. gen.maximumSize = CGSizeMake(192, 192);
  429. NSError *error = nil;
  430. CMTime actualTime;
  431. CMTime time = CMTimeMakeWithSeconds(0.5, 30);
  432. CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
  433. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
  434. CGImageRelease(imageRef);
  435. dispatch_async(dispatch_get_main_queue(), ^{
  436. if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
  437. [exportSession exportAsynchronouslyWithCompletionHandler:^{
  438. switch ([exportSession status]) {
  439. case AVAssetExportSessionStatusFailed:
  440. NSLog(@"Export session failed");
  441. break;
  442. case AVAssetExportSessionStatusCancelled:
  443. NSLog(@"Export canceled");
  444. break;
  445. case AVAssetExportSessionStatusCompleted: {
  446. // Video conversion finished
  447. NSLog(@"Successful!");
  448. [self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
  449. }
  450. break;
  451. default:
  452. break;
  453. }
  454. }];
  455. [NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
  456. if (exportSession.status == AVAssetExportSessionStatusExporting) {
  457. NSLog(@"exportSession.progress:%f",exportSession.progress);
  458. placeHolderCellData.videoTranscodingProgress = exportSession.progress;
  459. }
  460. }];
  461. }
  462. else {
  463. [exportSession exportAsynchronouslyWithCompletionHandler:^{
  464. switch ([exportSession status]) {
  465. case AVAssetExportSessionStatusCompleted: {
  466. // Video conversion finished
  467. NSLog(@"Successful!");
  468. [self handleVideoPick:succ message:message videoUrl:newUrl];
  469. } break;
  470. default:
  471. break;
  472. }
  473. }];
  474. }
  475. });
  476. }
  477. - (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl {
  478. [self handleVideoPick:succ message:message videoUrl:videoUrl placeHolderCellData:nil];
  479. }
  480. - (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl placeHolderCellData:(TUIMessageCellData*)placeHolderCellData{
  481. if (succ == NO || videoUrl == nil) {
  482. if ([self.listener respondsToSelector:@selector(onProvideVideoError:)]) {
  483. [self.listener onProvideVideoError:message];
  484. }
  485. return;
  486. }
  487. NSData *videoData = [NSData dataWithContentsOfURL:videoUrl];
  488. NSString *videoPath = [NSString stringWithFormat:@"%@%@_%u.mp4", TUIKit_Video_Path, [TUITool genVideoName:nil],arc4random()];
  489. [[NSFileManager defaultManager] createFileAtPath:videoPath contents:videoData attributes:nil];
  490. NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
  491. AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:videoUrl options:opts];
  492. NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
  493. AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
  494. gen.appliesPreferredTrackTransform = YES;
  495. gen.maximumSize = CGSizeMake(192, 192);
  496. NSError *error = nil;
  497. CMTime actualTime;
  498. CMTime time = CMTimeMakeWithSeconds(0.5, 30);
  499. CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
  500. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
  501. CGImageRelease(imageRef);
  502. NSData *imageData = UIImagePNGRepresentation(image);
  503. NSString *imagePath = [TUIKit_Video_Path stringByAppendingFormat:@"%@_%u",[TUITool genSnapshotName:nil],arc4random()];
  504. [[NSFileManager defaultManager] createFileAtPath:imagePath contents:imageData attributes:nil];
  505. if ([self.listener respondsToSelector:@selector(onProvideVideo:snapshot:duration:placeHolderCellData:)]) {
  506. [self.listener onProvideVideo:videoPath snapshot:imagePath duration:duration placeHolderCellData:placeHolderCellData];
  507. }
  508. }
  509. #pragma mark - PHPickerViewControllerDelegate
  510. - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
  511. dispatch_async(dispatch_get_main_queue(), ^{
  512. [picker dismissViewControllerAnimated:YES completion:nil];
  513. [[[UIApplication sharedApplication] keyWindow] endEditing:YES];
  514. });
  515. if (!results || results.count == 0) {
  516. return;
  517. }
  518. PHPickerResult *result = [results firstObject];
  519. for (PHPickerResult *result in results) {
  520. [self _dealPHPickerResultFinishPicking:result];
  521. }
  522. }
  523. - (void)_dealPHPickerResultFinishPicking:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
  524. NSItemProvider *itemProvoider = result.itemProvider;
  525. __weak typeof(self) weakSelf = self;
  526. if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
  527. [itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeImage
  528. completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
  529. dispatch_async(dispatch_get_main_queue(), ^{
  530. BOOL succ = YES;
  531. NSString *message = nil;
  532. if (error) {
  533. succ = NO;
  534. message = error.localizedDescription;
  535. }
  536. [weakSelf handleImagePick:succ message:message imageData:data];
  537. });
  538. }];
  539. } else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMPEG4]) {
  540. [itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
  541. completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
  542. dispatch_async(dispatch_get_main_queue(), ^{
  543. NSString *fileName = @"temp.mp4";
  544. NSString *tempPath = NSTemporaryDirectory();
  545. NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
  546. if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
  547. [NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
  548. }
  549. NSURL *newUrl = [NSURL fileURLWithPath:filePath];
  550. BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
  551. [weakSelf transcodeIfNeed:flag message:flag ? nil : @"video not found" videoUrl:newUrl];
  552. });
  553. }];
  554. } else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) {
  555. // Mov type: screen first
  556. if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
  557. [self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:nil Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
  558. [itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
  559. completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
  560. dispatch_async(dispatch_get_main_queue(), ^{
  561. // Non-mp4 format video, temporarily use mov suffix, will be converted to mp4 format later
  562. NSDate *datenow = [NSDate date];
  563. NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)([datenow timeIntervalSince1970]*1000)];
  564. NSString *fileName = [NSString stringWithFormat:@"%@_temp.mov",timeSp];
  565. NSString *tempPath = NSTemporaryDirectory();
  566. NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
  567. if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
  568. [NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
  569. }
  570. NSURL *newUrl = [NSURL fileURLWithPath:filePath];
  571. BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
  572. [weakSelf transcodeIfNeed:flag message:flag ? nil : @"movie not found" videoUrl:newUrl placeHolderCellData:placeHolderCellData];
  573. });
  574. }];
  575. }];
  576. }
  577. } else {
  578. NSString *typeIdentifier = result.itemProvider.registeredTypeIdentifiers.firstObject;
  579. [itemProvoider loadFileRepresentationForTypeIdentifier:typeIdentifier
  580. completionHandler:^(NSURL *_Nullable url, NSError *_Nullable error) {
  581. dispatch_async(dispatch_get_main_queue(), ^{
  582. UIImage *result;
  583. NSData *data = [NSData dataWithContentsOfURL:url];
  584. result = [UIImage imageWithData:data];
  585. /**
  586. * Can't get url when typeIdentifier is public.jepg on emulator:
  587. * There is a separate JEPG transcoding issue that only affects the simulator (63426347), please refer to
  588. * https://developer.apple.com/forums/thread/658135 for more information.
  589. */
  590. });
  591. }];
  592. }
  593. }
  594. #pragma mark - UIImagePickerController
  595. - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
  596. __weak typeof(self) weakSelf = self;
  597. picker.delegate = nil;
  598. [picker dismissViewControllerAnimated:YES
  599. completion:^{
  600. NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
  601. if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
  602. NSURL *url = nil;
  603. if (@available(iOS 11.0, *)) {
  604. url = [info objectForKey:UIImagePickerControllerImageURL];
  605. } else {
  606. url = [info objectForKey:UIImagePickerControllerReferenceURL];
  607. }
  608. BOOL succ = YES;
  609. NSData *imageData = nil;
  610. NSString *errorMessage = nil;
  611. if (url) {
  612. succ = YES;
  613. imageData = [NSData dataWithContentsOfURL:url];
  614. } else {
  615. succ = NO;
  616. errorMessage = @"image not found";
  617. }
  618. [weakSelf handleImagePick:succ message:errorMessage imageData:imageData];
  619. } else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
  620. NSURL *url = [info objectForKey:UIImagePickerControllerMediaURL];
  621. if (url) {
  622. [weakSelf transcodeIfNeed:YES message:nil videoUrl:url];
  623. return;
  624. }
  625. /**
  626. * In some cases UIImagePickerControllerMediaURL may be empty, use UIImagePickerControllerPHAsset
  627. */
  628. PHAsset *asset = nil;
  629. if (@available(iOS 11.0, *)) {
  630. asset = [info objectForKey:UIImagePickerControllerPHAsset];
  631. }
  632. if (asset) {
  633. [self originURLWithAsset:asset
  634. completion:^(BOOL success, NSURL *URL) {
  635. [weakSelf transcodeIfNeed:success
  636. message:success ? nil : @"origin url with asset not found"
  637. videoUrl:URL];
  638. }];
  639. return;
  640. }
  641. /**
  642. * UIImagePickerControllerPHAsset may be empty, and other methods need to be used to obtain the original path of the video
  643. * file
  644. */
  645. url = [info objectForKey:UIImagePickerControllerReferenceURL];
  646. if (url) {
  647. [weakSelf originURLWithRefrenceURL:url
  648. completion:^(BOOL success, NSURL *URL) {
  649. [weakSelf transcodeIfNeed:success
  650. message:success ? nil : @"origin url with asset not found"
  651. videoUrl:URL];
  652. }];
  653. return;
  654. }
  655. // not support the video
  656. [weakSelf transcodeIfNeed:NO message:@"not support the video" videoUrl:nil];
  657. }
  658. }];
  659. }
  660. - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
  661. [picker dismissViewControllerAnimated:YES completion:nil];
  662. }
  663. /**
  664. * Get the original file path based on UIImagePickerControllerReferenceURL
  665. */
  666. - (void)originURLWithRefrenceURL:(NSURL *)URL completion:(void (^)(BOOL success, NSURL *URL))completion {
  667. if (completion == nil) {
  668. return;
  669. }
  670. NSDictionary *queryInfo = [self dictionaryWithURLQuery:URL.query];
  671. NSString *fileName = @"temp.mp4";
  672. if ([queryInfo.allKeys containsObject:@"id"] && [queryInfo.allKeys containsObject:@"ext"]) {
  673. fileName = [NSString stringWithFormat:@"%@.%@", queryInfo[@"id"], [queryInfo[@"ext"] lowercaseString]];
  674. }
  675. NSString *tempPath = NSTemporaryDirectory();
  676. NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
  677. if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
  678. [NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
  679. }
  680. NSURL *newUrl = [NSURL fileURLWithPath:filePath];
  681. ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
  682. [assetLibrary assetForURL:URL
  683. resultBlock:^(ALAsset *asset) {
  684. if (asset == nil) {
  685. completion(NO, nil);
  686. return;
  687. }
  688. ALAssetRepresentation *rep = [asset defaultRepresentation];
  689. Byte *buffer = (Byte *)malloc(rep.size);
  690. NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
  691. NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; // this is NSData may be what you want
  692. BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
  693. completion(flag, newUrl);
  694. }
  695. failureBlock:^(NSError *err) {
  696. completion(NO, nil);
  697. }];
  698. }
  699. - (void)originURLWithAsset:(PHAsset *)asset completion:(void (^)(BOOL success, NSURL *URL))completion {
  700. if (completion == nil) {
  701. return;
  702. }
  703. NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
  704. if (resources.count == 0) {
  705. completion(NO, nil);
  706. return;
  707. }
  708. PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
  709. options.networkAccessAllowed = NO;
  710. __block BOOL invoked = NO;
  711. [PHAssetResourceManager.defaultManager requestDataForAssetResource:resources.firstObject
  712. options:options
  713. dataReceivedHandler:^(NSData *_Nonnull data) {
  714. /**
  715. *
  716. * There will be a problem of repeated callbacks here
  717. */
  718. if (invoked) {
  719. return;
  720. }
  721. invoked = YES;
  722. if (data == nil) {
  723. completion(NO, nil);
  724. return;
  725. }
  726. NSString *fileName = @"temp.mp4";
  727. NSString *tempPath = NSTemporaryDirectory();
  728. NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
  729. if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
  730. [NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
  731. }
  732. NSURL *newUrl = [NSURL fileURLWithPath:filePath];
  733. BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
  734. completion(flag, newUrl);
  735. }
  736. completionHandler:^(NSError *_Nullable error) {
  737. completion(NO, nil);
  738. }];
  739. }
  740. - (NSDictionary *)dictionaryWithURLQuery:(NSString *)query {
  741. NSArray *components = [query componentsSeparatedByString:@"&"];
  742. NSMutableDictionary *dict = [NSMutableDictionary dictionary];
  743. for (NSString *item in components) {
  744. NSArray *subs = [item componentsSeparatedByString:@"="];
  745. if (subs.count == 2) {
  746. [dict setObject:subs.lastObject forKey:subs.firstObject];
  747. }
  748. }
  749. return [NSDictionary dictionaryWithDictionary:dict];
  750. ;
  751. }
  752. #pragma mark - TUICameraViewControllerDelegate
  753. - (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url {
  754. [self transcodeIfNeed:YES message:nil videoUrl:url];
  755. }
  756. - (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data {
  757. [self handleImagePick:YES message:nil imageData:data];
  758. }
  759. - (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller {
  760. }
  761. - (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback {
  762. [self selectPhoto];
  763. if (callback) {
  764. callback();
  765. }
  766. }
  767. #pragma mark - UIDocumentPickerDelegate
  768. - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
  769. [url startAccessingSecurityScopedResource];
  770. NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
  771. NSError *error;
  772. @weakify(self);
  773. [coordinator
  774. coordinateReadingItemAtURL:url
  775. options:0
  776. error:&error
  777. byAccessor:^(NSURL *newURL) {
  778. @strongify(self);
  779. NSData *fileData = [NSData dataWithContentsOfURL:newURL options:NSDataReadingMappedIfSafe error:nil];
  780. NSString *fileName = [url lastPathComponent];
  781. NSString *filePath = [TUIKit_File_Path stringByAppendingString:fileName];
  782. if (fileData.length > 1e9 || fileData.length == 0) { // 1e9 bytes = 1GB
  783. UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitFileSizeCheckLimited) message:nil preferredStyle:UIAlertControllerStyleAlert];
  784. [ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
  785. [self.presentViewController presentViewController:ac animated:YES completion:nil];
  786. return;
  787. }
  788. if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) {
  789. /**
  790. * If a file with the same name exists, increment the file name
  791. */
  792. int i = 0;
  793. NSArray *arrayM = [NSFileManager.defaultManager subpathsAtPath:TUIKit_File_Path];
  794. for (NSString *sub in arrayM) {
  795. if ([sub.pathExtension isEqualToString:fileName.pathExtension] &&
  796. [sub.stringByDeletingPathExtension tui_containsString:fileName.stringByDeletingPathExtension]) {
  797. i++;
  798. }
  799. }
  800. if (i) {
  801. fileName = [fileName
  802. stringByReplacingOccurrencesOfString:fileName.stringByDeletingPathExtension
  803. withString:[NSString stringWithFormat:@"%@(%d)", fileName.stringByDeletingPathExtension, i]];
  804. filePath = [TUIKit_File_Path stringByAppendingString:fileName];
  805. }
  806. }
  807. [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
  808. if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
  809. unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
  810. if ([self.listener respondsToSelector:@selector(onProvideFile:filename:fileSize:)]) {
  811. [self.listener onProvideFile:filePath filename:fileName fileSize:fileSize];
  812. }
  813. } else {
  814. if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
  815. [self.listener onProvideFileError:@"file not found"];
  816. }
  817. }
  818. }];
  819. [url stopAccessingSecurityScopedResource];
  820. [controller dismissViewControllerAnimated:YES completion:nil];
  821. }
  822. - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
  823. [controller dismissViewControllerAnimated:YES completion:nil];
  824. }
  825. @end