提交
This commit is contained in:
898
TUIKit/TUIChat/CommonUI/Camera/TUIChatMediaDataProvider.m
Normal file
898
TUIKit/TUIChat/CommonUI/Camera/TUIChatMediaDataProvider.m
Normal file
@@ -0,0 +1,898 @@
|
||||
//
|
||||
// TUIChatMediaDataProvider.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2022/12/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatMediaDataProvider.h"
|
||||
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import <PhotosUI/PhotosUI.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TUIUserAuthorizationCenter.h>
|
||||
#import <TIMCommon/NSTimer+TUISafe.h>
|
||||
#import <TUICore/TUITool.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import "TUICameraViewController.h"
|
||||
#import "TUIChatConfig.h"
|
||||
#import "AlbumPicker.h"
|
||||
#import "MultimediaRecorder.h"
|
||||
#define kTUIChatMediaSelectImageMax 9
|
||||
@interface TUIChatMediaDataProvider () <PHPickerViewControllerDelegate,
|
||||
UINavigationControllerDelegate,
|
||||
UIImagePickerControllerDelegate,
|
||||
UIDocumentPickerDelegate,
|
||||
TUICameraViewControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatMediaDataProvider
|
||||
|
||||
#pragma mark - Public API
|
||||
- (void)selectPhoto {
|
||||
if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
__strong typeof(weakSelf.listener) strongListener = weakSelf.listener;
|
||||
[[AlbumPicker sharedInstance].advancedAlbumPicker pickMediaWithCaller:self.presentViewController originalMediaPicked:^(NSDictionary *param) {
|
||||
if (param) {
|
||||
NSString * type = param[@"type"];
|
||||
if ([type isEqualToString:@"image"]) {
|
||||
// image do nothing
|
||||
}
|
||||
else if ([type isEqualToString:@"video"]) {
|
||||
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
|
||||
if ([strongListener respondsToSelector:@selector(sendPlaceHolderUIMessage:)]) {
|
||||
[strongListener sendPlaceHolderUIMessage:placeHolderCellData];
|
||||
}
|
||||
TUIChatMediaTask * task = [[TUIChatMediaTask alloc] init];
|
||||
task.placeHolderCellData = placeHolderCellData;
|
||||
task.msgID = placeHolderCellData.msgID;
|
||||
task.conversationID = weakSelf.conversationID;
|
||||
if (placeHolderCellData.msgID.length > 0) {
|
||||
[TUIChatMediaSendingManager.sharedInstance addMediaTask: task forKey:placeHolderCellData.msgID];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
} progressCallback:^(NSDictionary *param) {
|
||||
NSLog(@"%@,strongListener:%@",param,strongListener);
|
||||
} finishedCallback:^(NSDictionary *param) {
|
||||
if (param) {
|
||||
V2TIMMessage * message = param[@"message"];
|
||||
NSString * type = param[@"type"];
|
||||
if ([type isEqualToString:@"image"]) {
|
||||
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
|
||||
[strongListener sendMessage:message placeHolderCellData:nil];
|
||||
}
|
||||
}
|
||||
else if ([type isEqualToString:@"video"]) {
|
||||
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
|
||||
if (placeHolderCellData.msgID.length > 0) {
|
||||
[TUIChatMediaSendingManager.sharedInstance removeMediaTaskForKey:placeHolderCellData.msgID];
|
||||
}
|
||||
BOOL canSendByCurrentPage = NO;
|
||||
for (id<TUIChatMediaDataListener> currentVC in TUIChatMediaSendingManager.sharedInstance.mediaSendingControllers) {
|
||||
if ([currentVC.currentConversationID isEqualToString:self.conversationID]&&
|
||||
[currentVC respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
|
||||
if (currentVC.isPageAppears) {
|
||||
[currentVC sendMessage:message placeHolderCellData:placeHolderCellData];
|
||||
canSendByCurrentPage = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!canSendByCurrentPage) {
|
||||
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
|
||||
[strongListener sendMessage:message placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
//defalut AlbumPicker
|
||||
[self _selectPhoto];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_selectPhoto {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (@available(iOS 14.0, *)) {
|
||||
PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init];
|
||||
configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] ]];
|
||||
configuration.selectionLimit = kTUIChatMediaSelectImageMax;
|
||||
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:configuration];
|
||||
picker.delegate = self;
|
||||
picker.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
picker.view.backgroundColor = [UIColor whiteColor];
|
||||
[self.presentViewController presentViewController:picker animated:YES completion:nil];
|
||||
} else {
|
||||
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
|
||||
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
||||
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
||||
picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
|
||||
picker.delegate = self;
|
||||
[self.presentViewController presentViewController:picker animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)takePicture {
|
||||
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
|
||||
[[MultimediaRecorder sharedInstance].advancedVideoRecorder takePhoneWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
|
||||
NSData *imageData = [NSData dataWithContentsOfURL:uri];
|
||||
UIImage *photo = [UIImage imageWithData:imageData];
|
||||
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
|
||||
[[NSFileManager defaultManager] createFileAtPath:path
|
||||
contents:UIImagePNGRepresentation(photo) attributes:nil];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
|
||||
[self.listener onProvideImage:path];
|
||||
}
|
||||
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
|
||||
|
||||
}];
|
||||
}
|
||||
else {
|
||||
//defalut PhotoCamera
|
||||
[self _takePicture];
|
||||
}
|
||||
}
|
||||
- (void)_takePicture {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
void (^actionBlock)(void) = ^(void) {
|
||||
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
|
||||
vc.type = TUICameraMediaTypePhoto;
|
||||
vc.delegate = weakSelf;
|
||||
if (weakSelf.presentViewController.navigationController) {
|
||||
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
|
||||
} else {
|
||||
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
|
||||
}
|
||||
};
|
||||
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
actionBlock();
|
||||
});
|
||||
} else {
|
||||
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
actionBlock();
|
||||
});
|
||||
}];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
- (void)executeBlockWithMicroAndCameraAuth:(void(^)(void))block{
|
||||
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization] && [TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), block);
|
||||
} else {
|
||||
if (![TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
|
||||
[TUIUserAuthorizationCenter microStateActionWithPopCompletion:^{
|
||||
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}];
|
||||
}
|
||||
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
|
||||
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)takeVideo {
|
||||
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
|
||||
[[MultimediaRecorder sharedInstance].advancedVideoRecorder recordVideoWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
|
||||
if (uri) {
|
||||
if ([uri.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
|
||||
[self handleVideoPick:YES message:nil videoUrl:uri];
|
||||
return;
|
||||
}
|
||||
else if ([self isImageURL:uri]){
|
||||
NSData *imageData = [NSData dataWithContentsOfURL:uri];
|
||||
UIImage *photo = [UIImage imageWithData:imageData];
|
||||
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
|
||||
[[NSFileManager defaultManager] createFileAtPath:path
|
||||
contents:UIImagePNGRepresentation(photo) attributes:nil];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
|
||||
[self.listener onProvideImage:path];
|
||||
}
|
||||
}
|
||||
else {
|
||||
[self transcodeIfNeed:YES message:nil videoUrl:uri];
|
||||
}
|
||||
}
|
||||
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
|
||||
|
||||
}];
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
//defalut VideoRecorder
|
||||
[self _takeVideo];
|
||||
}
|
||||
}
|
||||
- (void)_takeVideo {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
void (^actionBlock)(void) = ^(void) {
|
||||
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
|
||||
vc.type = TUICameraMediaTypeVideo;
|
||||
vc.videoMinimumDuration = 1.5;
|
||||
vc.delegate = weakSelf;
|
||||
if ([TUIChatConfig defaultConfig].maxVideoRecordDuration > 0) {
|
||||
vc.videoMaximumDuration = [TUIChatConfig defaultConfig].maxVideoRecordDuration;
|
||||
}
|
||||
if (weakSelf.presentViewController.navigationController) {
|
||||
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
|
||||
} else {
|
||||
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
|
||||
}
|
||||
};
|
||||
|
||||
[self executeBlockWithMicroAndCameraAuth:actionBlock];
|
||||
}
|
||||
|
||||
- (void)selectFile {
|
||||
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ (NSString *)kUTTypeData ]
|
||||
inMode:UIDocumentPickerModeOpen];
|
||||
picker.delegate = self;
|
||||
[self.presentViewController presentViewController:picker animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)isImageURL:(NSURL *)url {
|
||||
NSArray *imageExtensions = @[@"jpg", @"jpeg", @"png", @"gif", @"bmp", @"tiff", @"webp", @"heic"];
|
||||
NSString *pathExtension = url.pathExtension.lowercaseString;
|
||||
return [imageExtensions containsObject:pathExtension];
|
||||
}
|
||||
#pragma mark - Private Do task
|
||||
- (void)handleImagePick:(BOOL)succ message:(NSString *)message imageData:(NSData *)imageData {
|
||||
static NSDictionary *imageFormatExtensionMap = nil;
|
||||
if (imageFormatExtensionMap == nil) {
|
||||
imageFormatExtensionMap = @{
|
||||
@(SDImageFormatUndefined) : @"",
|
||||
@(SDImageFormatJPEG) : @"jpeg",
|
||||
@(SDImageFormatPNG) : @"png",
|
||||
@(SDImageFormatGIF) : @"gif",
|
||||
@(SDImageFormatTIFF) : @"tiff",
|
||||
@(SDImageFormatWebP) : @"webp",
|
||||
@(SDImageFormatHEIC) : @"heic",
|
||||
@(SDImageFormatHEIF) : @"heif",
|
||||
@(SDImageFormatPDF) : @"pdf",
|
||||
@(SDImageFormatSVG) : @"svg",
|
||||
@(SDImageFormatBMP) : @"bmp",
|
||||
@(SDImageFormatRAW) : @"raw"
|
||||
};
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (succ == NO || imageData == nil) {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImageError:)]) {
|
||||
[self.listener onProvideImageError:message];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:imageData];
|
||||
NSData *data = UIImageJPEGRepresentation(image, 1);
|
||||
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
|
||||
NSString *extenionName = [imageFormatExtensionMap objectForKey:@(image.sd_imageFormat)];
|
||||
if (extenionName.length > 0) {
|
||||
path = [path stringByAppendingPathExtension:extenionName];
|
||||
}
|
||||
|
||||
|
||||
int32_t imageFormatSizeMax = 28 * 1024 * 1024;
|
||||
|
||||
if (image.sd_imageFormat == SDImageFormatGIF) {
|
||||
imageFormatSizeMax = 10 * 1024 * 1024;
|
||||
}
|
||||
|
||||
if (imageData.length > imageFormatSizeMax) {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
|
||||
[self.listener onProvideFileError:TIMCommonLocalizableString(TUIKitImageSizeCheckLimited)];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (image.sd_imageFormat != SDImageFormatGIF) {
|
||||
UIImage *newImage = image;
|
||||
UIImageOrientation imageOrientation = image.imageOrientation;
|
||||
CGFloat aspectRatio = MIN(1920 / image.size.width, 1920 / image.size.height);
|
||||
CGFloat aspectWidth = image.size.width * aspectRatio;
|
||||
CGFloat aspectHeight = image.size.height * aspectRatio;
|
||||
UIGraphicsBeginImageContext(CGSizeMake(aspectWidth, aspectHeight));
|
||||
[image drawInRect:CGRectMake(0, 0, aspectWidth, aspectHeight)];
|
||||
newImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
data = UIImageJPEGRepresentation(newImage, 0.75);
|
||||
}
|
||||
|
||||
[[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
|
||||
[self.listener onProvideImage:path];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url {
|
||||
if (succ == NO || url == nil) {
|
||||
[self handleVideoPick:NO message:message videoUrl:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
|
||||
[self handleVideoPick:succ message:message videoUrl:url];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSURL *urlName = [url URLByDeletingPathExtension];
|
||||
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if ([fileManager fileExistsAtPath:newUrl.path]) {
|
||||
NSError *error;
|
||||
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
|
||||
if (!success || error) {
|
||||
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// mov to mp4
|
||||
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
|
||||
exportSession.outputURL = newUrl;
|
||||
exportSession.outputFileType = AVFileTypeMPEG4;
|
||||
exportSession.shouldOptimizeForNetworkUse = YES;
|
||||
|
||||
// intercept FirstTime VideoPicture
|
||||
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
|
||||
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
|
||||
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
|
||||
gen.appliesPreferredTrackTransform = YES;
|
||||
gen.maximumSize = CGSizeMake(192, 192);
|
||||
NSError *error = nil;
|
||||
CMTime actualTime;
|
||||
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
|
||||
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
|
||||
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:image Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusFailed:
|
||||
NSLog(@"Export session failed");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCancelled:
|
||||
NSLog(@"Export canceled");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
|
||||
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
if (exportSession.status == AVAssetExportSessionStatusExporting) {
|
||||
NSLog(@"exportSession.progress:%f",exportSession.progress);
|
||||
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
|
||||
}
|
||||
}];
|
||||
|
||||
}];
|
||||
}
|
||||
else {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url placeHolderCellData:(TUIMessageCellData*)placeHolderCellData {
|
||||
if (succ == NO || url == nil) {
|
||||
[self handleVideoPick:NO message:message videoUrl:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
|
||||
[self handleVideoPick:succ message:message videoUrl:url];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSURL *urlName = [url URLByDeletingPathExtension];
|
||||
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if ([fileManager fileExistsAtPath:newUrl.path]) {
|
||||
NSError *error;
|
||||
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
|
||||
if (!success || error) {
|
||||
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// mov to mp4
|
||||
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
|
||||
exportSession.outputURL = newUrl;
|
||||
exportSession.outputFileType = AVFileTypeMPEG4;
|
||||
exportSession.shouldOptimizeForNetworkUse = YES;
|
||||
|
||||
// intercept FirstTime VideoPicture
|
||||
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
|
||||
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
|
||||
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
|
||||
gen.appliesPreferredTrackTransform = YES;
|
||||
gen.maximumSize = CGSizeMake(192, 192);
|
||||
NSError *error = nil;
|
||||
CMTime actualTime;
|
||||
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
|
||||
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusFailed:
|
||||
NSLog(@"Export session failed");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCancelled:
|
||||
NSLog(@"Export canceled");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
|
||||
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
if (exportSession.status == AVAssetExportSessionStatusExporting) {
|
||||
NSLog(@"exportSession.progress:%f",exportSession.progress);
|
||||
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl {
|
||||
[self handleVideoPick:succ message:message videoUrl:videoUrl placeHolderCellData:nil];
|
||||
}
|
||||
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl placeHolderCellData:(TUIMessageCellData*)placeHolderCellData{
|
||||
if (succ == NO || videoUrl == nil) {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideVideoError:)]) {
|
||||
[self.listener onProvideVideoError:message];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *videoData = [NSData dataWithContentsOfURL:videoUrl];
|
||||
NSString *videoPath = [NSString stringWithFormat:@"%@%@_%u.mp4", TUIKit_Video_Path, [TUITool genVideoName:nil],arc4random()];
|
||||
[[NSFileManager defaultManager] createFileAtPath:videoPath contents:videoData attributes:nil];
|
||||
|
||||
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:videoUrl options:opts];
|
||||
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
|
||||
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
|
||||
gen.appliesPreferredTrackTransform = YES;
|
||||
gen.maximumSize = CGSizeMake(192, 192);
|
||||
NSError *error = nil;
|
||||
CMTime actualTime;
|
||||
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
|
||||
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
NSData *imageData = UIImagePNGRepresentation(image);
|
||||
NSString *imagePath = [TUIKit_Video_Path stringByAppendingFormat:@"%@_%u",[TUITool genSnapshotName:nil],arc4random()];
|
||||
[[NSFileManager defaultManager] createFileAtPath:imagePath contents:imageData attributes:nil];
|
||||
|
||||
if ([self.listener respondsToSelector:@selector(onProvideVideo:snapshot:duration:placeHolderCellData:)]) {
|
||||
[self.listener onProvideVideo:videoPath snapshot:imagePath duration:duration placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - PHPickerViewControllerDelegate
|
||||
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
[[[UIApplication sharedApplication] keyWindow] endEditing:YES];
|
||||
});
|
||||
|
||||
if (!results || results.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PHPickerResult *result = [results firstObject];
|
||||
for (PHPickerResult *result in results) {
|
||||
[self _dealPHPickerResultFinishPicking:result];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_dealPHPickerResultFinishPicking:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
|
||||
NSItemProvider *itemProvoider = result.itemProvider;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
|
||||
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeImage
|
||||
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
BOOL succ = YES;
|
||||
NSString *message = nil;
|
||||
if (error) {
|
||||
succ = NO;
|
||||
message = error.localizedDescription;
|
||||
}
|
||||
[weakSelf handleImagePick:succ message:message imageData:data];
|
||||
});
|
||||
}];
|
||||
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMPEG4]) {
|
||||
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
|
||||
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString *fileName = @"temp.mp4";
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"video not found" videoUrl:newUrl];
|
||||
});
|
||||
}];
|
||||
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) {
|
||||
// Mov type: screen first
|
||||
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
|
||||
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:nil Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
|
||||
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
|
||||
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Non-mp4 format video, temporarily use mov suffix, will be converted to mp4 format later
|
||||
NSDate *datenow = [NSDate date];
|
||||
NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)([datenow timeIntervalSince1970]*1000)];
|
||||
NSString *fileName = [NSString stringWithFormat:@"%@_temp.mov",timeSp];
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"movie not found" videoUrl:newUrl placeHolderCellData:placeHolderCellData];
|
||||
});
|
||||
}];
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
NSString *typeIdentifier = result.itemProvider.registeredTypeIdentifiers.firstObject;
|
||||
[itemProvoider loadFileRepresentationForTypeIdentifier:typeIdentifier
|
||||
completionHandler:^(NSURL *_Nullable url, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIImage *result;
|
||||
NSData *data = [NSData dataWithContentsOfURL:url];
|
||||
result = [UIImage imageWithData:data];
|
||||
|
||||
/**
|
||||
* Can't get url when typeIdentifier is public.jepg on emulator:
|
||||
* There is a separate JEPG transcoding issue that only affects the simulator (63426347), please refer to
|
||||
* https://developer.apple.com/forums/thread/658135 for more information.
|
||||
*/
|
||||
});
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIImagePickerController
|
||||
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
picker.delegate = nil;
|
||||
[picker dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
|
||||
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
|
||||
NSURL *url = nil;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
url = [info objectForKey:UIImagePickerControllerImageURL];
|
||||
} else {
|
||||
url = [info objectForKey:UIImagePickerControllerReferenceURL];
|
||||
}
|
||||
|
||||
BOOL succ = YES;
|
||||
NSData *imageData = nil;
|
||||
NSString *errorMessage = nil;
|
||||
if (url) {
|
||||
succ = YES;
|
||||
imageData = [NSData dataWithContentsOfURL:url];
|
||||
} else {
|
||||
succ = NO;
|
||||
errorMessage = @"image not found";
|
||||
}
|
||||
[weakSelf handleImagePick:succ message:errorMessage imageData:imageData];
|
||||
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
|
||||
NSURL *url = [info objectForKey:UIImagePickerControllerMediaURL];
|
||||
if (url) {
|
||||
[weakSelf transcodeIfNeed:YES message:nil videoUrl:url];
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases UIImagePickerControllerMediaURL may be empty, use UIImagePickerControllerPHAsset
|
||||
*/
|
||||
PHAsset *asset = nil;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
asset = [info objectForKey:UIImagePickerControllerPHAsset];
|
||||
}
|
||||
if (asset) {
|
||||
[self originURLWithAsset:asset
|
||||
completion:^(BOOL success, NSURL *URL) {
|
||||
[weakSelf transcodeIfNeed:success
|
||||
message:success ? nil : @"origin url with asset not found"
|
||||
videoUrl:URL];
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* UIImagePickerControllerPHAsset may be empty, and other methods need to be used to obtain the original path of the video
|
||||
* file
|
||||
*/
|
||||
url = [info objectForKey:UIImagePickerControllerReferenceURL];
|
||||
if (url) {
|
||||
[weakSelf originURLWithRefrenceURL:url
|
||||
completion:^(BOOL success, NSURL *URL) {
|
||||
[weakSelf transcodeIfNeed:success
|
||||
message:success ? nil : @"origin url with asset not found"
|
||||
videoUrl:URL];
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
// not support the video
|
||||
[weakSelf transcodeIfNeed:NO message:@"not support the video" videoUrl:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original file path based on UIImagePickerControllerReferenceURL
|
||||
*/
|
||||
- (void)originURLWithRefrenceURL:(NSURL *)URL completion:(void (^)(BOOL success, NSURL *URL))completion {
|
||||
if (completion == nil) {
|
||||
return;
|
||||
}
|
||||
NSDictionary *queryInfo = [self dictionaryWithURLQuery:URL.query];
|
||||
NSString *fileName = @"temp.mp4";
|
||||
if ([queryInfo.allKeys containsObject:@"id"] && [queryInfo.allKeys containsObject:@"ext"]) {
|
||||
fileName = [NSString stringWithFormat:@"%@.%@", queryInfo[@"id"], [queryInfo[@"ext"] lowercaseString]];
|
||||
}
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
|
||||
[assetLibrary assetForURL:URL
|
||||
resultBlock:^(ALAsset *asset) {
|
||||
if (asset == nil) {
|
||||
completion(NO, nil);
|
||||
return;
|
||||
}
|
||||
ALAssetRepresentation *rep = [asset defaultRepresentation];
|
||||
Byte *buffer = (Byte *)malloc(rep.size);
|
||||
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
|
||||
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; // this is NSData may be what you want
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
completion(flag, newUrl);
|
||||
}
|
||||
failureBlock:^(NSError *err) {
|
||||
completion(NO, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)originURLWithAsset:(PHAsset *)asset completion:(void (^)(BOOL success, NSURL *URL))completion {
|
||||
if (completion == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
if (resources.count == 0) {
|
||||
completion(NO, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
|
||||
options.networkAccessAllowed = NO;
|
||||
__block BOOL invoked = NO;
|
||||
[PHAssetResourceManager.defaultManager requestDataForAssetResource:resources.firstObject
|
||||
options:options
|
||||
dataReceivedHandler:^(NSData *_Nonnull data) {
|
||||
/**
|
||||
*
|
||||
* There will be a problem of repeated callbacks here
|
||||
*/
|
||||
if (invoked) {
|
||||
return;
|
||||
}
|
||||
invoked = YES;
|
||||
if (data == nil) {
|
||||
completion(NO, nil);
|
||||
return;
|
||||
}
|
||||
NSString *fileName = @"temp.mp4";
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
completion(flag, newUrl);
|
||||
}
|
||||
completionHandler:^(NSError *_Nullable error) {
|
||||
completion(NO, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary *)dictionaryWithURLQuery:(NSString *)query {
|
||||
NSArray *components = [query componentsSeparatedByString:@"&"];
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
for (NSString *item in components) {
|
||||
NSArray *subs = [item componentsSeparatedByString:@"="];
|
||||
if (subs.count == 2) {
|
||||
[dict setObject:subs.lastObject forKey:subs.firstObject];
|
||||
}
|
||||
}
|
||||
return [NSDictionary dictionaryWithDictionary:dict];
|
||||
;
|
||||
}
|
||||
|
||||
#pragma mark - TUICameraViewControllerDelegate
|
||||
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url {
|
||||
[self transcodeIfNeed:YES message:nil videoUrl:url];
|
||||
}
|
||||
|
||||
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data {
|
||||
[self handleImagePick:YES message:nil imageData:data];
|
||||
}
|
||||
|
||||
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller {
|
||||
}
|
||||
|
||||
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback {
|
||||
[self selectPhoto];
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIDocumentPickerDelegate
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
|
||||
[url startAccessingSecurityScopedResource];
|
||||
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
|
||||
NSError *error;
|
||||
@weakify(self);
|
||||
[coordinator
|
||||
coordinateReadingItemAtURL:url
|
||||
options:0
|
||||
error:&error
|
||||
byAccessor:^(NSURL *newURL) {
|
||||
@strongify(self);
|
||||
NSData *fileData = [NSData dataWithContentsOfURL:newURL options:NSDataReadingMappedIfSafe error:nil];
|
||||
NSString *fileName = [url lastPathComponent];
|
||||
NSString *filePath = [TUIKit_File_Path stringByAppendingString:fileName];
|
||||
if (fileData.length > 1e9 || fileData.length == 0) { // 1e9 bytes = 1GB
|
||||
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitFileSizeCheckLimited) message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||||
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self.presentViewController presentViewController:ac animated:YES completion:nil];
|
||||
return;
|
||||
}
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) {
|
||||
/**
|
||||
* If a file with the same name exists, increment the file name
|
||||
*/
|
||||
int i = 0;
|
||||
NSArray *arrayM = [NSFileManager.defaultManager subpathsAtPath:TUIKit_File_Path];
|
||||
for (NSString *sub in arrayM) {
|
||||
if ([sub.pathExtension isEqualToString:fileName.pathExtension] &&
|
||||
[sub.stringByDeletingPathExtension tui_containsString:fileName.stringByDeletingPathExtension]) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
fileName = [fileName
|
||||
stringByReplacingOccurrencesOfString:fileName.stringByDeletingPathExtension
|
||||
withString:[NSString stringWithFormat:@"%@(%d)", fileName.stringByDeletingPathExtension, i]];
|
||||
filePath = [TUIKit_File_Path stringByAppendingString:fileName];
|
||||
}
|
||||
}
|
||||
|
||||
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
||||
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideFile:filename:fileSize:)]) {
|
||||
[self.listener onProvideFile:filePath filename:fileName fileSize:fileSize];
|
||||
}
|
||||
} else {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
|
||||
[self.listener onProvideFileError:@"file not found"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
[controller dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
|
||||
[controller dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user