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

Community
  • 1
  • 1
ProblemSlover
  • 2,538
  • 2
  • 23
  • 33
  • Are you uploading in main thread or created a separated thread? – Muzammil Oct 26 '15 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 :) – ProblemSlover Oct 27 '15 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, – Utsav Dusad Sep 07 '16 at 12:02

3 Answers3

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.

ProblemSlover
  • 2,538
  • 2
  • 23
  • 33
  • @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 '17 at 09:02
  • @sarita You are supposed first write(copy) the Phasset to your internal app storage and then use the local file path to upload. – ProblemSlover Apr 21 '17 at 10:23
  • This is good. do you have suggestions on how to retrieve metadata for the video before uploading ? – Curious101 Nov 30 '17 at 03:15
6

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];
ogres
  • 3,660
  • 1
  • 17
  • 16
  • 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! – ProblemSlover Oct 27 '15 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 '15 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!) – ProblemSlover Oct 27 '15 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 '15 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 – ProblemSlover Oct 27 '15 at 19:35
  • make sure you have access to the URL of the asset ( since you can open it , you should have it ) , but if the access is temp. you might try first copying it (while still have access) in documents or somewhere else and then upload. P.S. accepting answer really does not matter, but I also want to get the idea whats the reason behind this – ogres Oct 27 '15 at 19:49
  • Since no one else has given an answer.. this reward lands to you. Tested my approach(uploading by URL and it was suggested by you) and on an old iPhone 5S using third party service .. It works.. but not on a newer devices as was told by clients. So.. keep looking workaround – ProblemSlover Nov 02 '15 at 02:26
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.

Community
  • 1
  • 1
Alfie Hanssen
  • 16,964
  • 12
  • 68
  • 74
  • 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. – ProblemSlover Mar 17 '16 at 09: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 '16 at 15:21