Currently I'm using the following code to upload 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 method
+(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);
}
}];
}
The problem with this approach that an app simply runs out of memory whenever large/multiple video files are being uploaded. I suppose it's due to requesting the NSDATA (aka filedata
) for uploading of a file(see in the method above). I've tried to request the file path using method
appendPartWithFileURL
intead of appendPartWithFileData
it works on an emulator. and fails on a real device with an error that it can't read the file by the path specified. I've described this issue here
PHAsset + AFNetworking. Unable to upload files to the server on a real device
=======================================
Update: I've modified my code in order to test approach of uploading file by the local path on a new iPhone 6s+ as follows
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];
Testing on iPhone 6s+ gives a more clear log warning It occurs as the result of invoking method 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.
Here is The code used to fetch the local file path from 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);
}
}];
}
So the issue remains the same - files uploaded to the server are empty
The proposed solution above is correct only partially(and it was found by myself before). Since The system doesn't permit to read files outside of sandbox therefore the files cannot be accessed(read/write) by the file path and just copied. In the version iOS 9 and above Photos Framework provides API(it cannot be done through the NSFileManager , but only using Photos framework api) to copy the file into your App's sandbox directory. Here is the code which I used after digging in docs and head files.
First of all copy a file to the app sandbox directory.
// 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
Upload Task Itself
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
This upload Method has a significant drawback .. you are screwed if device goes into "sleep mode".. Therefore the recommended upload approach here is to use method .uploadTaskWithRequest:fromFile:progress:completionHandler
in AFURLSessionManager.
For versions below iOS 9.. In Case of images You can fetch the NSDATA
from PHAsset
as shown in the code of my question.. and upload it. or write it first into your app sandbox storage before uploading. This approach is not usable in case of large files. Alternatively you may want to use Image/video picker exports files as ALAsset
. ALAsset
provides api which allows you to read file from the storage.but you have to write it to the sandbox storage before uploading.
Creating NSData for every video might be bad, because videos ( or any other files ) can be much bigger than RAM of the device,
I'd suggest to upload as "file" and not as "data", if you append file, it will send the data from the disk chunk-by-chunk and won`t try to read whole file at once, try using
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * __autoreleasing *)error
also have a look at https://github.com/AFNetworking/AFNetworking/issues/828
In your case , use it with like this
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];
See my answer here to your other question. Seems relevant here as the subject matter is linked.
In that answer I describe that in my experience Apple does not give us direct access to video source files associated with a PHAsset. Or an ALAsset for that matter.
In order to access the video files and upload them, you must first create a copy using an AVAssetExportSession
. Or using the iOS9+ PHAssetResourceManager API.
You should not use any methods that load data into memory as you'll quickly run up against OOM exceptions. And it's probably not a good idea to use the requestAVAssetForVideo(_:options:resultHandler:)
method as stated above because you will at times get an AVComposition as opposed to an AVAsset (and you can't get an NSURL from an AVComposition directly).
You also probably don't want to use any upload method that leverages AFHTTPRequestOperation
or related APIs because they are based on the deprecated NSURLConnection API as opposed to the more modern NSURLSession APIs. NSURLSession will allow you to conduct long running video uploads in a background process, allowing your users to leave your app and be confident that the upload will complete regardless.
In my original answer I mention VimeoUpload. It's a library that handles video file uploads to Vimeo, but its core can be repurposed to handle concurrent background video file uploads to any destination. Full disclosure: I'm one of the library's authors.