9

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

3
  • Are you uploading in main thread or created a separated thread?
    – Muzammil
    Oct 26, 2015 at 20:53
  • @Muzammil Hi. It's being uploaded in the main thread.. since it's done through the third party library. Im' not sure whether it matters. Thank you for your input :) Oct 27, 2015 at 16:01
  • How we can send the live photos and slo motion videos. These are combination of 2 or more files. So there are multiple file URLs for them, Sep 7, 2016 at 12:02

3 Answers 3

12

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.

3
  • @problemSolver, your answer works fine on the same screen but how to implement it when we have to do upload part on the next screen? I have shown all the videos fetched from gallery using photo framework in the collection view and on the selection of a cell, I have to upload the selected video in the next view controller. I'm passing the URL extracted from PHAssetResource to the next screen. But it doesn't upload the file showing response as (null).
    – sarita
    Apr 21, 2017 at 9:02
  • @sarita You are supposed first write(copy) the Phasset to your internal app storage and then use the local file path to upload. Apr 21, 2017 at 10:23
  • This is good. do you have suggestions on how to retrieve metadata for the video before uploading ?
    – Curious101
    Nov 30, 2017 at 3:15
6
+50

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];
7
  • Hi. as I've described in my question. I can't do it by the local file url(in Case of PHAsset) on a devise. but it works fine on an emulator. I've updated my question! Oct 27, 2015 at 16:41
  • Emulator works fine because your computer has bigger RAM than iPhone and it can copy file's data into Memory, I can see that you are getting GetAssetData , use Assets URL and do not create dataWithContentsOfURL with it, Upload directly with URL without creating the NSData
    – ogres
    Oct 27, 2015 at 16:42
  • Your Approach (It's mine too, I've tried it before) Doesn't work on a real device. As I 've pointed out it fails with an error that it can't read from file system whenever the file is being uploaded by the local path(Please note file path is requested through PHAsset!) Oct 27, 2015 at 17:25
  • if you are able to create NSData like this NSData *videoData= [NSData dataWithContentsOfURL:localVideoUrl];, then this means you are able to open the file and you should be able to reuse this URL to upload
    – ogres
    Oct 27, 2015 at 19:03
  • It's true. but when it comes to upload it doesn't work as it suppose to. Since I don't have a devise I gonna write a test.. and using third party testing service for that. If It works. I will accept your answer and tell my client that something wrong with their iPhones Oct 27, 2015 at 19:35
3

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.

2
  • Thank you for your input. That's exactly what I did. however. It took me a while to figure out how to get long uploads working with NSURLSession in the background: For some reason my parameters were missing in the background upload mode. I after a long research found somewhere that parameters must be written (following specific format) to the file rather than as parameters. Mar 17, 2016 at 9:00
  • @ProblemSlover Is it OK for you to share your NSURLSession code for the upload? I am also having trouble uploading videos in the background.
    – nyus2006
    Sep 15, 2016 at 15:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.