Phasset + AfNetworking 上传多个视频
Phasset + AfNetworking Upload Multiple Videos
目前我正在使用以下代码上传视频:
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[UploadModel getAssetData:entity.asset resultHandler:^(NSData *filedata) {
NSString *mimeType =[FileHelper mimeTypeForFileAtUrl:entity.fileUrl];
// NSError *fileappenderror;
[formData appendPartWithFileData:filedata name:@"data" fileName: entity.filename mimeType:mimeType];
}];
} error:&urlRequestError];
GetAssetData 方法
+(void)getAssetData: (PHAsset*)mPhasset resultHandler:(void(^)(NSData *imageData))dataResponse{
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.version = PHVideoRequestOptionsVersionOriginal;
[[PHImageManager defaultManager] requestAVAssetForVideo:mPhasset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
NSURL *localVideoUrl = [(AVURLAsset *)asset URL];
NSData *videoData= [NSData dataWithContentsOfURL:localVideoUrl];
dataResponse(videoData);
}
}];
}
这种方法的问题是,每当上传 large/multiple 视频文件时,应用程序就会耗尽内存。我想这是由于请求 NSDATA(又名 filedata
)上传文件(见上面的方法)。我尝试使用方法请求文件路径
appendPartWithFileURL
而不是 appendPartWithFileData
它适用于模拟器。并在真实设备上失败,并出现无法通过指定路径读取文件的错误。我在这里描述了这个问题
=======================================
更新: 我修改了我的代码,以便测试通过本地路径在新的 iPhone 6s+ 上上传文件的方法,如下所示
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
NSString *mimeType =[FileHelper mimeTypeForFileAtUrl:entity.fileUrl];
NSError *fileappenderror;
[formData appendPartWithFileURL:entity.fileUrl name:@"data" fileName:entity.filename mimeType:mimeType error:&fileappenderror];
if (fileappenderror) {
[Sys MyLog: [NSString stringWithFormat:@"Failed to append: %@", [fileappenderror localizedDescription] ] ];
}
} error:&urlRequestError];
在 iPhone 6s+ 上测试给出了更清晰的日志警告它是调用方法 appendPartWithFileURL
的结果
<Warning>: my_log: Failed to append file: The operation couldn’t be completed. File URL not reachable.
deny(1) file-read-metadata /private/var/mobile/Media/DCIM/100APPLE/IMG_0008.MOV
15:41:25 iPhone-6s kernel[0] <Notice>: Sandbox: My_App(396) deny(1) file-read-metadata /private/var/mobile/Media/DCIM/100APPLE/IMG_0008.MOV
15:41:25 iPhone-6s My_App[396] <Warning>: my_log: Failed to append file: The file “IMG_0008.MOV” couldn’t be opened because you don’t have permission to view it.
这是用于从 PHAsset 中获取本地文件路径的代码
if (mPhasset.mediaType == PHAssetMediaTypeImage) {
PHContentEditingInputRequestOptions * options = [[PHContentEditingInputRequestOptions alloc]init];
options.canHandleAdjustmentData = ^BOOL(PHAdjustmentData *adjustmeta){
return YES;
};
[mPhasset requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) {
dataResponse(contentEditingInput.fullSizeImageURL);
}];
}else if(mPhasset.mediaType == PHAssetMediaTypeVideo){
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.version = PHVideoRequestOptionsVersionOriginal;
[[PHImageManager defaultManager] requestAVAssetForVideo:mPhasset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
NSURL *localVideoUrl = [(AVURLAsset *)asset URL];
dataResponse(localVideoUrl);
}
}];
}
所以问题还是一样 - 上传到服务器的文件是空的
为每个视频创建 NSData 可能不好,因为视频(或任何其他文件)可能比设备的 RAM 大得多,
我建议上传为 "file" 而不是 "data",如果你追加文件,它会从磁盘逐块发送数据,不会尝试一次读取整个文件, 尝试使用
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * __autoreleasing *)error
也看看https://github.com/AFNetworking/AFNetworking/issues/828
在你的情况下,像这样使用它
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:entity.fileUrl name:entity.filename error:&fileappenderror];
if(fileappenderror) {
NSLog(@"%@",fileappenderror);
}
} error:&urlRequestError];
上面提出的解决方案只是部分正确(之前是我自己找到的)。由于系统不允许读取沙箱外的文件,因此无法通过文件路径访问 (read/write) 文件,只能复制文件。在iOS 9及以上版本Photos Framework提供API(不能通过NSFileManager,只能使用Photos framework api)复制文件到你App的沙箱目录。这是我在挖掘文档和头文件后使用的代码。
首先复制一个文件到app沙盒目录下
// Assuming PHAsset has only one resource file.
PHAssetResource * resource = [[PHAssetResource assetResourcesForAsset:myPhasset] firstObject];
+(void)writeResourceToTmp: (PHAssetResource*)resource pathCallback: (void(^)(NSURL*localUrl))pathCallback {
// Get Asset Resource. Take first resource object. since it's only the one image.
NSString *filename = resource.originalFilename;
NSString *pathToWrite = [NSTemporaryDirectory() stringByAppendingString:filename];
NSURL *localpath = [NSURL fileURLWithPath:pathToWrite];
PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new];
options.networkAccessAllowed = YES;
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:resource toFile:localpath options:options completionHandler:^(NSError * _Nullable error) {
if (error) {
[Sys MyLog: [NSString stringWithFormat:@"Failed to write a resource: %@",[error localizedDescription]]];
}
pathCallback(localpath);
}];
} // Write Resource into Tmp
上传任务本身
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST"
URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
// Assuming PHAsset has only one resource file.
PHAssetResource * resource = [[PHAssetResource assetResourcesForAsset:myPhasset] firstObject];
[FileHelper writeResourceToTmp:resource pathCallback:^(NSURL *localUrl)
{
[formData appendPartWithFileURL: localUrl name:@"data" fileName:entity.filename mimeType:mimeType error:&fileappenderror];
}]; // writeResourceToTmp
}// End Url Request
AFHTTPRequestOperation * operation = [[AFHTTPRequestOperation alloc ] initWithRequest:urlRequest];
//.....
// Further steps are described in the AFNetworking Docs
这种上传方法有一个很大的缺点..如果设备进入"sleep mode",你就完蛋了。因此这里推荐的上传方法是使用AFURLSessionManager.
中的方法.uploadTaskWithRequest:fromFile:progress:completionHandler
对于 iOS 以下的版本 9.. 如果是图片,您可以从 PHAsset
中获取 NSDATA
,如我问题的代码所示......并上传。或者在上传之前先将其写入您的应用程序沙箱存储。这种方法在大文件的情况下不可用。或者,您可能希望使用 Image/video 选择器将文件导出为 ALAsset
。 ALAsset
提供 api 允许您从 storage.but 读取文件,您必须在上传前将其写入沙箱存储。
请参阅我对您的其他问题的回答 。似乎与这里相关,因为主题是相关联的。
在那个回答中,我描述了根据我的经验,Apple 不会让我们直接访问与 PHAsset 关联的视频源文件。或与此相关的 ALAsset。
为了访问视频文件并上传它们,您必须先使用 AVAssetExportSession
. Or using the iOS9+ PHAssetResourceManager API 创建一个副本。
您不应使用任何将数据加载到内存中的方法,因为您会很快 运行 遇到 OOM 异常。如上所述使用 requestAVAssetForVideo(_:options:resultHandler:)
方法可能不是一个好主意,因为您有时会得到一个 AVComposition 而不是 AVAsset(并且您不能直接从 AVComposition 获得 NSURL)。
您也可能不想使用任何利用 AFHTTPRequestOperation
或相关 API 的上传方法,因为它们基于已弃用的 NSURLConnection API 而不是更多现代 NSURLSession API 年代。 NSURLSession 将允许您在后台进程中进行长时间的 运行ning 视频上传,让您的用户离开您的应用程序并确信无论如何上传都会完成。
在我原来的回答中我提到了 VimeoUpload。它是一个处理视频文件上传到 Vimeo 的库,但它的核心可以重新调整用途以处理并发的后台视频文件上传到任何目的地。完全披露:我是图书馆的作者之一。
目前我正在使用以下代码上传视频:
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[UploadModel getAssetData:entity.asset resultHandler:^(NSData *filedata) {
NSString *mimeType =[FileHelper mimeTypeForFileAtUrl:entity.fileUrl];
// NSError *fileappenderror;
[formData appendPartWithFileData:filedata name:@"data" fileName: entity.filename mimeType:mimeType];
}];
} error:&urlRequestError];
GetAssetData 方法
+(void)getAssetData: (PHAsset*)mPhasset resultHandler:(void(^)(NSData *imageData))dataResponse{
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.version = PHVideoRequestOptionsVersionOriginal;
[[PHImageManager defaultManager] requestAVAssetForVideo:mPhasset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
NSURL *localVideoUrl = [(AVURLAsset *)asset URL];
NSData *videoData= [NSData dataWithContentsOfURL:localVideoUrl];
dataResponse(videoData);
}
}];
}
这种方法的问题是,每当上传 large/multiple 视频文件时,应用程序就会耗尽内存。我想这是由于请求 NSDATA(又名 filedata
)上传文件(见上面的方法)。我尝试使用方法请求文件路径
appendPartWithFileURL
而不是 appendPartWithFileData
它适用于模拟器。并在真实设备上失败,并出现无法通过指定路径读取文件的错误。我在这里描述了这个问题
=======================================
更新: 我修改了我的代码,以便测试通过本地路径在新的 iPhone 6s+ 上上传文件的方法,如下所示
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
NSString *mimeType =[FileHelper mimeTypeForFileAtUrl:entity.fileUrl];
NSError *fileappenderror;
[formData appendPartWithFileURL:entity.fileUrl name:@"data" fileName:entity.filename mimeType:mimeType error:&fileappenderror];
if (fileappenderror) {
[Sys MyLog: [NSString stringWithFormat:@"Failed to append: %@", [fileappenderror localizedDescription] ] ];
}
} error:&urlRequestError];
在 iPhone 6s+ 上测试给出了更清晰的日志警告它是调用方法 appendPartWithFileURL
<Warning>: my_log: Failed to append file: The operation couldn’t be completed. File URL not reachable.
deny(1) file-read-metadata /private/var/mobile/Media/DCIM/100APPLE/IMG_0008.MOV
15:41:25 iPhone-6s kernel[0] <Notice>: Sandbox: My_App(396) deny(1) file-read-metadata /private/var/mobile/Media/DCIM/100APPLE/IMG_0008.MOV
15:41:25 iPhone-6s My_App[396] <Warning>: my_log: Failed to append file: The file “IMG_0008.MOV” couldn’t be opened because you don’t have permission to view it.
这是用于从 PHAsset 中获取本地文件路径的代码
if (mPhasset.mediaType == PHAssetMediaTypeImage) {
PHContentEditingInputRequestOptions * options = [[PHContentEditingInputRequestOptions alloc]init];
options.canHandleAdjustmentData = ^BOOL(PHAdjustmentData *adjustmeta){
return YES;
};
[mPhasset requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) {
dataResponse(contentEditingInput.fullSizeImageURL);
}];
}else if(mPhasset.mediaType == PHAssetMediaTypeVideo){
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.version = PHVideoRequestOptionsVersionOriginal;
[[PHImageManager defaultManager] requestAVAssetForVideo:mPhasset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
NSURL *localVideoUrl = [(AVURLAsset *)asset URL];
dataResponse(localVideoUrl);
}
}];
}
所以问题还是一样 - 上传到服务器的文件是空的
为每个视频创建 NSData 可能不好,因为视频(或任何其他文件)可能比设备的 RAM 大得多, 我建议上传为 "file" 而不是 "data",如果你追加文件,它会从磁盘逐块发送数据,不会尝试一次读取整个文件, 尝试使用
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * __autoreleasing *)error
也看看https://github.com/AFNetworking/AFNetworking/issues/828
在你的情况下,像这样使用它
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:entity.fileUrl name:entity.filename error:&fileappenderror];
if(fileappenderror) {
NSLog(@"%@",fileappenderror);
}
} error:&urlRequestError];
上面提出的解决方案只是部分正确(之前是我自己找到的)。由于系统不允许读取沙箱外的文件,因此无法通过文件路径访问 (read/write) 文件,只能复制文件。在iOS 9及以上版本Photos Framework提供API(不能通过NSFileManager,只能使用Photos framework api)复制文件到你App的沙箱目录。这是我在挖掘文档和头文件后使用的代码。
首先复制一个文件到app沙盒目录下
// Assuming PHAsset has only one resource file.
PHAssetResource * resource = [[PHAssetResource assetResourcesForAsset:myPhasset] firstObject];
+(void)writeResourceToTmp: (PHAssetResource*)resource pathCallback: (void(^)(NSURL*localUrl))pathCallback {
// Get Asset Resource. Take first resource object. since it's only the one image.
NSString *filename = resource.originalFilename;
NSString *pathToWrite = [NSTemporaryDirectory() stringByAppendingString:filename];
NSURL *localpath = [NSURL fileURLWithPath:pathToWrite];
PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new];
options.networkAccessAllowed = YES;
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:resource toFile:localpath options:options completionHandler:^(NSError * _Nullable error) {
if (error) {
[Sys MyLog: [NSString stringWithFormat:@"Failed to write a resource: %@",[error localizedDescription]]];
}
pathCallback(localpath);
}];
} // Write Resource into Tmp
上传任务本身
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST"
URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
// Assuming PHAsset has only one resource file.
PHAssetResource * resource = [[PHAssetResource assetResourcesForAsset:myPhasset] firstObject];
[FileHelper writeResourceToTmp:resource pathCallback:^(NSURL *localUrl)
{
[formData appendPartWithFileURL: localUrl name:@"data" fileName:entity.filename mimeType:mimeType error:&fileappenderror];
}]; // writeResourceToTmp
}// End Url Request
AFHTTPRequestOperation * operation = [[AFHTTPRequestOperation alloc ] initWithRequest:urlRequest];
//.....
// Further steps are described in the AFNetworking Docs
这种上传方法有一个很大的缺点..如果设备进入"sleep mode",你就完蛋了。因此这里推荐的上传方法是使用AFURLSessionManager.
中的方法.uploadTaskWithRequest:fromFile:progress:completionHandler
对于 iOS 以下的版本 9.. 如果是图片,您可以从 PHAsset
中获取 NSDATA
,如我问题的代码所示......并上传。或者在上传之前先将其写入您的应用程序沙箱存储。这种方法在大文件的情况下不可用。或者,您可能希望使用 Image/video 选择器将文件导出为 ALAsset
。 ALAsset
提供 api 允许您从 storage.but 读取文件,您必须在上传前将其写入沙箱存储。
请参阅我对您的其他问题的回答
在那个回答中,我描述了根据我的经验,Apple 不会让我们直接访问与 PHAsset 关联的视频源文件。或与此相关的 ALAsset。
为了访问视频文件并上传它们,您必须先使用 AVAssetExportSession
. Or using the iOS9+ PHAssetResourceManager API 创建一个副本。
您不应使用任何将数据加载到内存中的方法,因为您会很快 运行 遇到 OOM 异常。如上所述使用 requestAVAssetForVideo(_:options:resultHandler:)
方法可能不是一个好主意,因为您有时会得到一个 AVComposition 而不是 AVAsset(并且您不能直接从 AVComposition 获得 NSURL)。
您也可能不想使用任何利用 AFHTTPRequestOperation
或相关 API 的上传方法,因为它们基于已弃用的 NSURLConnection API 而不是更多现代 NSURLSession API 年代。 NSURLSession 将允许您在后台进程中进行长时间的 运行ning 视频上传,让您的用户离开您的应用程序并确信无论如何上传都会完成。
在我原来的回答中我提到了 VimeoUpload。它是一个处理视频文件上传到 Vimeo 的库,但它的核心可以重新调整用途以处理并发的后台视频文件上传到任何目的地。完全披露:我是图书馆的作者之一。