jt_dylan
jt_dylan

Reputation: 71

NSURLSession - use uploadTaskWithStreamedRequest with AWS IOS SDK

I've searched for days for a way to upload an IOS asset without creating a copy of the file in temp directory without luck. I got the code working with a temp copy but copying a video file that could be anywhere from 10MB to 4GB is not realistic.
The closest I have come to reading the asset in read-only mode is the code below. Per the apple documentation this should work - see the following links:

https://developer.apple.com/library/ios/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html

I have enabled these keys:

<key>com.apple.security.assets.movies.read-write</key>
<string>YES</string>
<key>com.apple.security.assets.music.read-write</key>
<string>YES</string>
<key>com.apple.security.assets.pictures.read-write</key>
<string>YES</string>
<key>com.apple.security.files.downloads.read-write</key>
<string>YES</string>

Here is the code:

//  QueueController.h
#import <AVFoundation/AVFoundation.h>
#import <AWSS3.h>
#import <Foundation/Foundation.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import "Reachability1.h"
#import "TransferType.h"
#import "TransferModel.h"
#import "Util.h"

@interface QueueController : NSObject<NSURLSessionDelegate>

@property(atomic, strong) NSURLSession* session;
@property(atomic, strong) NSNumber* sessionCount;
@property(atomic, strong) NSURLSessionConfiguration* configuration;

+ (QueueController*)sharedInstance;
- (void)transferMediaViaQueue:(MediaItem*)mediaItem
             withTransferType:(TransferType)transferType;
@end

@implementation QueueController {
  NSOperationQueue* copyQueue;
  NSOperationQueue* transferQueue;
  NSMutableArray* inProcessTransferArray;
  NSMutableArray* pendingTransferArray;
  bool isTransferring;
}

static QueueController* sharedInstance = nil;

// Get the shared instance and create it if necessary.
+ (QueueController*)sharedInstance {
  @synchronized(self) {
    if (sharedInstance == nil) {
      sharedInstance = [[QueueController alloc] init];
    }
  }
  return sharedInstance;
}

- (id)init {
  if (self = [super init]) {
    appDelegate =
        (RootViewControllerAppDelegate*)[UIApplication sharedApplication]
            .delegate;
    copyQueue = [[NSOperationQueue alloc] init];
    transferQueue = [[NSOperationQueue alloc] init];
    transferQueue.maxConcurrentOperationCount = MAX_CONCURRENT_TRANSFERS;
    inProcessTransferArray = [[NSMutableArray alloc] init];
    pendingTransferArray = [[NSMutableArray alloc] init];
    isTransferring = false;
    if (self.session == nil) {
      self.configuration = [NSURLSessionConfiguration
          backgroundSessionConfigurationWithIdentifier:@"transferQueue"];
      self.session = [NSURLSession sessionWithConfiguration:self.configuration
                                                   delegate:self
                                              delegateQueue:transferQueue];
    }
  }
  return self;
}

- (void)transferMediaViaQueue:(MediaItem*)mediaItem
             withTransferType:(TransferType)transferType {
  // Create a transfer model
  NSUserDefaults* defaultUser = [NSUserDefaults standardUserDefaults];
  NSString* user_id = [defaultUser valueForKey:@"UserId"];
  TransferModel* transferModel = [[TransferModel alloc] init];
  transferModel.mediaItem = mediaItem;
  transferModel.transferType = transferType;
  transferModel.s3Path = user_id;
  transferModel.s3file_name = mediaItem.mediaName;
  transferModel.assetURL =
      [[mediaItem.mediaLocalAsset defaultRepresentation] url];

  ALAssetRepresentation* mediaRep =
      [mediaItem.mediaLocalAsset defaultRepresentation];
  transferModel.content_type =
      (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass(
          (__bridge CFStringRef)[mediaRep UTI], kUTTagClassMIMEType);

  @synchronized(pendingTransferArray) {
    if ((!isTransferring) &&
        (transferQueue.operationCount < MAX_CONCURRENT_TRANSFERS)) {
      isTransferring = true;
      if (transferModel.transferType == UPLOAD) {
        /**
         * Read ALAsset from NSURLRequestStream
         */
        NSInvocationOperation* uploadOP = [[NSInvocationOperation alloc]
            initWithTarget:self
                  selector:@selector(uploadMediaViaLocalPath:)
                    object:transferModel];
        [transferQueue addOperation:uploadOP];
        [inProcessTransferArray addObject:transferModel];
      }

    } else {
      // Add to pending
      [pendingTransferArray addObject:transferModel];
    }
  }
}

- (void)uploadMediaViaLocalPath:(TransferModel*)transferModel {
  @try {
    /**
     * Fetch readable asset 
     */
    NSURL* assetURL =
        [[transferModel.mediaItem.mediaLocalAsset defaultRepresentation] url];
    NSData* fileToUpload = [[NSData alloc] initWithContentsOfURL:assetURL];
    NSURLRequest* assetAsRequest =
        [NSURLRequest requestWithURL:assetURL
                      cachePolicy:NSURLRequestUseProtocolCachePolicy
                      timeoutInterval:60.0];
    /**
     * Fetch signed URL
     */
    AWSS3GetPreSignedURLRequest* getPreSignedURLRequest =
        [AWSS3GetPreSignedURLRequest new];
    getPreSignedURLRequest.bucket = BUCKET_NAME;
    NSString* s3Key = [NSString stringWithFormat:@"%@/%@", transferModel.s3Path, transferModel.s3file_name];
    getPreSignedURLRequest.key = s3Key;
    getPreSignedURLRequest.HTTPMethod = AWSHTTPMethodPUT;
    getPreSignedURLRequest.expires = [NSDate dateWithTimeIntervalSinceNow:3600];

    // Important: must set contentType for PUT request
    // getPreSignedURLRequest.contentType = transferModel.mediaItem.mimeType;
    getPreSignedURLRequest.contentType = transferModel.content_type;
    NSLog(@"mimeType: %@", transferModel.content_type);

    /**
     * Upload the file
     */
    [[[AWSS3PreSignedURLBuilder defaultS3PreSignedURLBuilder]
        getPreSignedURL:getPreSignedURLRequest]
        continueWithBlock:^id(BFTask* task) {
          NSURLSessionUploadTask* uploadTask;
          transferModel.sessionTask = uploadTask;
          if (task.error) {
            NSLog(@"Error: %@", task.error);
          } else {
            NSURL* presignedURL = task.result;
            NSLog(@"upload presignedURL is: \n%@", presignedURL);

            NSMutableURLRequest* request =
                [NSMutableURLRequest requestWithURL:presignedURL];
            request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
            [request setHTTPMethod:@"PUT"];
            [request setValue:transferModel.content_type
                forHTTPHeaderField:@"Content-Type"];

            uploadTask =
                [self.session uploadTaskWithStreamedRequest:assetAsRequest];
            [uploadTask resume];
          }
          return nil;
        }];
  } @catch (NSException* exception) {
    NSLog(@"exception: %@", exception);
  } @finally {
  }
}

- (void)URLSession:(NSURLSession*)session
                        task:(NSURLSessionTask*)task
             didSendBodyData:(int64_t)bytesSent
              totalBytesSent:(int64_t)totalBytesSent
    totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
  // Calculate progress
  double progress = (double)totalBytesSent / (double)totalBytesExpectedToSend;
  NSLog(@"UploadTask progress: %lf", progress);
}

- (void)URLSession:(NSURLSession*)session
                    task:(NSURLSessionTask*)task
    didCompleteWithError:(NSError*)error {
  NSLog(@"(void)URLSession:session task:(NSURLSessionTask*)task "
        @"didCompleteWithError:error called...%@",
        error);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:
    (NSURLSession*)session {
  NSLog(@"URLSessionDidFinishEventsForBackgroundURLSession called...");
}

// NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession*)session
          dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
 completionHandler:
     (void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
  //completionHandler(NSURLSessionResponseAllow);
}

@end

But I'm receiving this error:

(void)URLSession:session task:(NSURLSessionTask*)task didCompleteWithError:error called...Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo=0x17166f840 {NSErrorFailingURLStringKey=assets-library://asset/asset.MOV?id=94F90EEB-BB6A-4E9D-B77E-CDD60173B60C&ext=MOV, NSLocalizedDescription=cancelled, NSErrorFailingURLKey=assets-library://asset/asset.MOV?id=94F90EEB-BB6A-4E9D-B77E-CDD60173B60C&ext=MOV}
 userInfo: {
    NSErrorFailingURLKey = "assets-library://asset/asset.MOV?id=94F90EEB-BB6A-4E9D-B77E-CDD60173B60C&ext=MOV";
    NSErrorFailingURLStringKey = "assets-library://asset/asset.MOV?id=94F90EEB-BB6A-4E9D-B77E-CDD60173B60C&ext=MOV";
    NSLocalizedDescription = cancelled;
}

Thanks in advance for your help.

Regards, -J

Upvotes: 1

Views: 2036

Answers (2)

Xcoder
Xcoder

Reputation: 1807

The ALAssetPropertyURL is a purely URL identifier for the asset i.e to identify assets and asset groups and I dont think you can use it directly to upload to a service.

You could use AVAssetExportSession to export the asset to temp url if other methods are teditious. i.e

  [AVAssetExportSession exportSessionWithAsset:[AVURLAsset URLAssetWithURL:assetURL options:nil] presetName:AVAssetExportPresetPassthrough];

Upvotes: 0

Rob
Rob

Reputation: 438232

A couple of comments regarding using NSURLSessionUploadTask:

  1. If you implement didReceiveResponse, you must call the completionHandler.

  2. If you call uploadTaskWithStreamedRequest, the documentation for the request parameter warns us that:

    The body stream and body data in this request object are ignored, and NSURLSession calls its delegate’s URLSession:task:needNewBodyStream: method to provide the body data.

    So you must implement needNewBodyStream if implementing a NSInputStream based request.

  3. Be forewarned, but using a stream-base request like this creates a request with a "chunked" transfer encoding and not all servers can handle that.

  4. At one point in the code, you appear to try to load the contents of the asset into a NSData. If you have assets that are that large, you cannot reasonably load that into a NSData object. Besides, that's inconsistent with using uploadTaskWithStreamedRequest.

    You either need to create NSInputStream or upload it from a file.

  5. You appear to be using the asset URL for the NSURLRequest. That URL should be the URL for your web service.

  6. When using image picker, you have access to two URL keys: the media URL (a file:// URL for movies, but not pictures) and the assets library reference URL (an assets-library:// URL). If you're using the media URL, you can use that for uploading movies. But you cannot use the assets library reference URL for uploading purposes. You can only use that in conjunction with ALAssetsLibrary.

Upvotes: 1

Related Questions