From 8dd15f2c87fbebbd6e1ce802b8a8362d4f5d7bf8 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Tue, 13 Jun 2017 08:02:13 -0700 Subject: [PATCH] Allow video uploads Summary: Allows video uploads from native iOS by using the proper network handler, `RCTAssetsLibrary` rather than `RCTImageLoader`. Currently, uploading a camera roll video via its `assets-library://` URI would upload the first image frame. This fixes it by checking if the `assets-library://` URI is of the extension `MOV`, and if so, declines to process it from `RCTImageLoader`. Reviewed By: javache Differential Revision: D5223351 fbshipit-source-id: 8de92c5bb72acb02ed4d9fb92d69f68c8b5d1b36 --- Libraries/Image/RCTImageLoader.m | 1206 +++++++++++++++--------------- 1 file changed, 615 insertions(+), 591 deletions(-) diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index b8edc9148..a9843548e 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -26,28 +26,28 @@ - (CAKeyframeAnimation *)reactKeyframeAnimation { - return objc_getAssociatedObject(self, _cmd); + return objc_getAssociatedObject(self, _cmd); } - (void)setReactKeyframeAnimation:(CAKeyframeAnimation *)reactKeyframeAnimation { - objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC); + objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC); } @end @implementation RCTImageLoader { - NSArray> *_loaders; - NSArray> *_decoders; - NSOperationQueue *_imageDecodeQueue; - dispatch_queue_t _URLRequestQueue; - id _imageCache; - NSMutableArray *_pendingTasks; - NSInteger _activeTasks; - NSMutableArray *_pendingDecodes; - NSInteger _scheduledDecodes; - NSUInteger _activeBytes; + NSArray> *_loaders; + NSArray> *_decoders; + NSOperationQueue *_imageDecodeQueue; + dispatch_queue_t _URLRequestQueue; + id _imageCache; + NSMutableArray *_pendingTasks; + NSInteger _activeTasks; + NSMutableArray *_pendingDecodes; + NSInteger _scheduledDecodes; + NSUInteger _activeBytes; } @synthesize bridge = _bridge; @@ -56,149 +56,149 @@ RCT_EXPORT_MODULE() - (void)setUp { - // Set defaults - _maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4; - _maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2; - _maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 * 1024; // 30MB + // Set defaults + _maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4; + _maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2; + _maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 * 1024; // 30MB - _URLRequestQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLRequestQueue", DISPATCH_QUEUE_SERIAL); + _URLRequestQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLRequestQueue", DISPATCH_QUEUE_SERIAL); } - (float)handlerPriority { - return 1; + return 1; } - (id)imageCache { - if (!_imageCache) { - //set up with default cache - _imageCache = [RCTImageCache new]; - } - return _imageCache; + if (!_imageCache) { + //set up with default cache + _imageCache = [RCTImageCache new]; + } + return _imageCache; } - (void)setImageCache:(id)cache { - if (_imageCache) { - RCTLogWarn(@"RCTImageCache was already set and has now been overriden."); - } - _imageCache = cache; + if (_imageCache) { + RCTLogWarn(@"RCTImageCache was already set and has now been overriden."); + } + _imageCache = cache; } - (id)imageURLLoaderForURL:(NSURL *)URL { - if (!_maxConcurrentLoadingTasks) { - [self setUp]; - } + if (!_maxConcurrentLoadingTasks) { + [self setUp]; + } - if (!_loaders) { - // Get loaders, sorted in reverse priority order (highest priority first) - RCTAssert(_bridge, @"Bridge not set"); - _loaders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { - float priorityA = [a respondsToSelector:@selector(loaderPriority)] ? [a loaderPriority] : 0; - float priorityB = [b respondsToSelector:@selector(loaderPriority)] ? [b loaderPriority] : 0; - if (priorityA > priorityB) { - return NSOrderedAscending; - } else if (priorityA < priorityB) { - return NSOrderedDescending; - } else { - return NSOrderedSame; - } - }]; - } + if (!_loaders) { + // Get loaders, sorted in reverse priority order (highest priority first) + RCTAssert(_bridge, @"Bridge not set"); + _loaders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + float priorityA = [a respondsToSelector:@selector(loaderPriority)] ? [a loaderPriority] : 0; + float priorityB = [b respondsToSelector:@selector(loaderPriority)] ? [b loaderPriority] : 0; + if (priorityA > priorityB) { + return NSOrderedAscending; + } else if (priorityA < priorityB) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } + }]; + } - if (RCT_DEBUG) { - // Check for handler conflicts - float previousPriority = 0; - id previousLoader = nil; - for (id loader in _loaders) { - float priority = [loader respondsToSelector:@selector(loaderPriority)] ? [loader loaderPriority] : 0; - if (previousLoader && priority < previousPriority) { - return previousLoader; - } - if ([loader canLoadImageURL:URL]) { - if (previousLoader) { - if (priority == previousPriority) { - RCTLogError(@"The RCTImageURLLoaders %@ and %@ both reported that" - " they can load the URL %@, and have equal priority" - " (%g). This could result in non-deterministic behavior.", - loader, previousLoader, URL, priority); - } - } else { - previousLoader = loader; - previousPriority = priority; + if (RCT_DEBUG) { + // Check for handler conflicts + float previousPriority = 0; + id previousLoader = nil; + for (id loader in _loaders) { + float priority = [loader respondsToSelector:@selector(loaderPriority)] ? [loader loaderPriority] : 0; + if (previousLoader && priority < previousPriority) { + return previousLoader; + } + if ([loader canLoadImageURL:URL]) { + if (previousLoader) { + if (priority == previousPriority) { + RCTLogError(@"The RCTImageURLLoaders %@ and %@ both reported that" + " they can load the URL %@, and have equal priority" + " (%g). This could result in non-deterministic behavior.", + loader, previousLoader, URL, priority); + } + } else { + previousLoader = loader; + previousPriority = priority; + } + } } - } + return previousLoader; } - return previousLoader; - } - // Normal code path - for (id loader in _loaders) { - if ([loader canLoadImageURL:URL]) { - return loader; + // Normal code path + for (id loader in _loaders) { + if ([loader canLoadImageURL:URL]) { + return loader; + } } - } - return nil; + return nil; } - (id)imageDataDecoderForData:(NSData *)data { - if (!_maxConcurrentLoadingTasks) { - [self setUp]; - } + if (!_maxConcurrentLoadingTasks) { + [self setUp]; + } - if (!_decoders) { - // Get decoders, sorted in reverse priority order (highest priority first) - RCTAssert(_bridge, @"Bridge not set"); - _decoders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageDataDecoder)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { - float priorityA = [a respondsToSelector:@selector(decoderPriority)] ? [a decoderPriority] : 0; - float priorityB = [b respondsToSelector:@selector(decoderPriority)] ? [b decoderPriority] : 0; - if (priorityA > priorityB) { - return NSOrderedAscending; - } else if (priorityA < priorityB) { - return NSOrderedDescending; - } else { - return NSOrderedSame; - } - }]; - } + if (!_decoders) { + // Get decoders, sorted in reverse priority order (highest priority first) + RCTAssert(_bridge, @"Bridge not set"); + _decoders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageDataDecoder)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + float priorityA = [a respondsToSelector:@selector(decoderPriority)] ? [a decoderPriority] : 0; + float priorityB = [b respondsToSelector:@selector(decoderPriority)] ? [b decoderPriority] : 0; + if (priorityA > priorityB) { + return NSOrderedAscending; + } else if (priorityA < priorityB) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } + }]; + } - if (RCT_DEBUG) { - // Check for handler conflicts - float previousPriority = 0; - id previousDecoder = nil; - for (id decoder in _decoders) { - float priority = [decoder respondsToSelector:@selector(decoderPriority)] ? [decoder decoderPriority] : 0; - if (previousDecoder && priority < previousPriority) { - return previousDecoder; - } - if ([decoder canDecodeImageData:data]) { - if (previousDecoder) { - if (priority == previousPriority) { - RCTLogError(@"The RCTImageDataDecoders %@ and %@ both reported that" - " they can decode the data , and" - " have equal priority (%g). This could result in" - " non-deterministic behavior.", - decoder, previousDecoder, data, data.length, priority); - } - } else { - previousDecoder = decoder; - previousPriority = priority; + if (RCT_DEBUG) { + // Check for handler conflicts + float previousPriority = 0; + id previousDecoder = nil; + for (id decoder in _decoders) { + float priority = [decoder respondsToSelector:@selector(decoderPriority)] ? [decoder decoderPriority] : 0; + if (previousDecoder && priority < previousPriority) { + return previousDecoder; + } + if ([decoder canDecodeImageData:data]) { + if (previousDecoder) { + if (priority == previousPriority) { + RCTLogError(@"The RCTImageDataDecoders %@ and %@ both reported that" + " they can decode the data , and" + " have equal priority (%g). This could result in" + " non-deterministic behavior.", + decoder, previousDecoder, data, data.length, priority); + } + } else { + previousDecoder = decoder; + previousPriority = priority; + } + } } - } + return previousDecoder; } - return previousDecoder; - } - // Normal code path - for (id decoder in _decoders) { - if ([decoder canDecodeImageData:data]) { - return decoder; + // Normal code path + for (id decoder in _decoders) { + if ([decoder canDecodeImageData:data]) { + return decoder; + } } - } - return nil; + return nil; } static UIImage *RCTResizeImageIfNeeded(UIImage *image, @@ -206,91 +206,91 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image, CGFloat scale, RCTResizeMode resizeMode) { - if (CGSizeEqualToSize(size, CGSizeZero) || - CGSizeEqualToSize(image.size, CGSizeZero) || - CGSizeEqualToSize(image.size, size)) { + if (CGSizeEqualToSize(size, CGSizeZero) || + CGSizeEqualToSize(image.size, CGSizeZero) || + CGSizeEqualToSize(image.size, size)) { + return image; + } + CAKeyframeAnimation *animation = image.reactKeyframeAnimation; + CGRect targetSize = RCTTargetRect(image.size, size, scale, resizeMode); + CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetSize); + image = RCTTransformImage(image, size, scale, transform); + image.reactKeyframeAnimation = animation; return image; - } - CAKeyframeAnimation *animation = image.reactKeyframeAnimation; - CGRect targetSize = RCTTargetRect(image.size, size, scale, resizeMode); - CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetSize); - image = RCTTransformImage(image, size, scale, transform); - image.reactKeyframeAnimation = animation; - return image; } - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest callback:(RCTImageLoaderCompletionBlock)callback { - return [self loadImageWithURLRequest:imageURLRequest - size:CGSizeZero - scale:1 - clipped:YES - resizeMode:RCTResizeModeStretch - progressBlock:nil - partialLoadBlock:nil - completionBlock:callback]; + return [self loadImageWithURLRequest:imageURLRequest + size:CGSizeZero + scale:1 + clipped:YES + resizeMode:RCTResizeModeStretch + progressBlock:nil + partialLoadBlock:nil + completionBlock:callback]; } - (void)dequeueTasks { - dispatch_async(_URLRequestQueue, ^{ - // Remove completed tasks - NSMutableArray *tasksToRemove = nil; - for (RCTNetworkTask *task in self->_pendingTasks.reverseObjectEnumerator) { - switch (task.status) { - case RCTNetworkTaskFinished: - if (!tasksToRemove) { - tasksToRemove = [NSMutableArray new]; - } - [tasksToRemove addObject:task]; - self->_activeTasks--; - break; - case RCTNetworkTaskPending: - break; - case RCTNetworkTaskInProgress: - // Check task isn't "stuck" - if (task.requestToken == nil) { - RCTLogWarn(@"Task orphaned for request %@", task.request); - if (!tasksToRemove) { - tasksToRemove = [NSMutableArray new]; + dispatch_async(_URLRequestQueue, ^{ + // Remove completed tasks + NSMutableArray *tasksToRemove = nil; + for (RCTNetworkTask *task in self->_pendingTasks.reverseObjectEnumerator) { + switch (task.status) { + case RCTNetworkTaskFinished: + if (!tasksToRemove) { + tasksToRemove = [NSMutableArray new]; + } + [tasksToRemove addObject:task]; + self->_activeTasks--; + break; + case RCTNetworkTaskPending: + break; + case RCTNetworkTaskInProgress: + // Check task isn't "stuck" + if (task.requestToken == nil) { + RCTLogWarn(@"Task orphaned for request %@", task.request); + if (!tasksToRemove) { + tasksToRemove = [NSMutableArray new]; + } + [tasksToRemove addObject:task]; + self->_activeTasks--; + [task cancel]; + } + break; } - [tasksToRemove addObject:task]; - self->_activeTasks--; - [task cancel]; - } - break; - } - } - - if (tasksToRemove) { - [self->_pendingTasks removeObjectsInArray:tasksToRemove]; - } + } - // Start queued decode - NSInteger activeDecodes = self->_scheduledDecodes - self->_pendingDecodes.count; - while (activeDecodes == 0 || (self->_activeBytes <= self->_maxConcurrentDecodingBytes && - activeDecodes <= self->_maxConcurrentDecodingTasks)) { - dispatch_block_t decodeBlock = self->_pendingDecodes.firstObject; - if (decodeBlock) { - [self->_pendingDecodes removeObjectAtIndex:0]; - decodeBlock(); - } else { - break; - } - } + if (tasksToRemove) { + [self->_pendingTasks removeObjectsInArray:tasksToRemove]; + } - // Start queued tasks - for (RCTNetworkTask *task in self->_pendingTasks) { - if (MAX(self->_activeTasks, self->_scheduledDecodes) >= self->_maxConcurrentLoadingTasks) { - break; - } - if (task.status == RCTNetworkTaskPending) { - [task start]; - self->_activeTasks++; - } - } - }); + // Start queued decode + NSInteger activeDecodes = self->_scheduledDecodes - self->_pendingDecodes.count; + while (activeDecodes == 0 || (self->_activeBytes <= self->_maxConcurrentDecodingBytes && + activeDecodes <= self->_maxConcurrentDecodingTasks)) { + dispatch_block_t decodeBlock = self->_pendingDecodes.firstObject; + if (decodeBlock) { + [self->_pendingDecodes removeObjectAtIndex:0]; + decodeBlock(); + } else { + break; + } + } + + // Start queued tasks + for (RCTNetworkTask *task in self->_pendingTasks) { + if (MAX(self->_activeTasks, self->_scheduledDecodes) >= self->_maxConcurrentLoadingTasks) { + break; + } + if (task.status == RCTNetworkTaskPending) { + [task start]; + self->_activeTasks++; + } + } + }); } /** @@ -306,213 +306,213 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image, partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate))completionBlock { - { - NSMutableURLRequest *mutableRequest = [request mutableCopy]; - [NSURLProtocol setProperty:@"RCTImageLoader" - forKey:@"trackingName" - inRequest:mutableRequest]; + { + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + [NSURLProtocol setProperty:@"RCTImageLoader" + forKey:@"trackingName" + inRequest:mutableRequest]; - // Add missing png extension - if (request.URL.fileURL && request.URL.pathExtension.length == 0) { - mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]]; - } - request = mutableRequest; - } - - // Find suitable image URL loader - id loadHandler = [self imageURLLoaderForURL:request.URL]; - BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ? - [loadHandler requiresScheduling] : YES; - - __block volatile uint32_t cancelled = 0; - __block dispatch_block_t cancelLoad = nil; - void (^completionHandler)(NSError *, id, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate) { - cancelLoad = nil; - - BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ? - [loadHandler shouldCacheLoadedImages] : YES; - - // If we've received an image, we should try to set it synchronously, - // if it's data, do decoding on a background thread. - if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) { - // Most loaders do not return on the main thread, so caller is probably not - // expecting it, and may do expensive post-processing in the callback - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (!cancelled) { - completionBlock(error, imageOrData, cacheResult, fetchDate); + // Add missing png extension + if (request.URL.fileURL && request.URL.pathExtension.length == 0) { + mutableRequest.URL = [NSURL fileURLWithPath:[request.URL.path stringByAppendingPathExtension:@"png"]]; } - }); - } else if (!cancelled) { - completionBlock(error, imageOrData, cacheResult, fetchDate); - } - }; - - // If the loader doesn't require scheduling we call it directly on - // the main queue. - if (loadHandler && !requiresScheduling) { - return [loadHandler loadImageForURL:request.URL - size:size - scale:scale - resizeMode:resizeMode - progressHandler:progressHandler - partialLoadHandler:partialLoadHandler - completionHandler:^(NSError *error, UIImage *image){ - completionHandler(error, image, nil); - }]; - } - - // All access to URL cache must be serialized - if (!_URLRequestQueue) { - [self setUp]; - } - - __weak RCTImageLoader *weakSelf = self; - dispatch_async(_URLRequestQueue, ^{ - __typeof(self) strongSelf = weakSelf; - if (cancelled || !strongSelf) { - return; + request = mutableRequest; } - if (loadHandler) { - cancelLoad = [loadHandler loadImageForURL:request.URL - size:size - scale:scale - resizeMode:resizeMode - progressHandler:progressHandler - partialLoadHandler:partialLoadHandler - completionHandler:^(NSError *error, UIImage *image) { - completionHandler(error, image, nil); - }]; - } else { - // Use networking module to load image - cancelLoad = [strongSelf _loadURLRequest:request - progressBlock:progressHandler - completionBlock:completionHandler]; - } - }); + // Find suitable image URL loader + id loadHandler = [self imageURLLoaderForURL:request.URL]; + BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ? + [loadHandler requiresScheduling] : YES; - return ^{ - dispatch_block_t cancelLoadLocal = cancelLoad; - cancelLoad = nil; - if (cancelLoadLocal && !cancelled) { - cancelLoadLocal(); + __block volatile uint32_t cancelled = 0; + __block dispatch_block_t cancelLoad = nil; + void (^completionHandler)(NSError *, id, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate) { + cancelLoad = nil; + + BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ? + [loadHandler shouldCacheLoadedImages] : YES; + + // If we've received an image, we should try to set it synchronously, + // if it's data, do decoding on a background thread. + if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) { + // Most loaders do not return on the main thread, so caller is probably not + // expecting it, and may do expensive post-processing in the callback + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (!cancelled) { + completionBlock(error, imageOrData, cacheResult, fetchDate); + } + }); + } else if (!cancelled) { + completionBlock(error, imageOrData, cacheResult, fetchDate); + } + }; + + // If the loader doesn't require scheduling we call it directly on + // the main queue. + if (loadHandler && !requiresScheduling) { + return [loadHandler loadImageForURL:request.URL + size:size + scale:scale + resizeMode:resizeMode + progressHandler:progressHandler + partialLoadHandler:partialLoadHandler + completionHandler:^(NSError *error, UIImage *image){ + completionHandler(error, image, nil); + }]; } - OSAtomicOr32Barrier(1, &cancelled); - }; + + // All access to URL cache must be serialized + if (!_URLRequestQueue) { + [self setUp]; + } + + __weak RCTImageLoader *weakSelf = self; + dispatch_async(_URLRequestQueue, ^{ + __typeof(self) strongSelf = weakSelf; + if (cancelled || !strongSelf) { + return; + } + + if (loadHandler) { + cancelLoad = [loadHandler loadImageForURL:request.URL + size:size + scale:scale + resizeMode:resizeMode + progressHandler:progressHandler + partialLoadHandler:partialLoadHandler + completionHandler:^(NSError *error, UIImage *image) { + completionHandler(error, image, nil); + }]; + } else { + // Use networking module to load image + cancelLoad = [strongSelf _loadURLRequest:request + progressBlock:progressHandler + completionBlock:completionHandler]; + } + }); + + return ^{ + dispatch_block_t cancelLoadLocal = cancelLoad; + cancelLoad = nil; + if (cancelLoadLocal && !cancelled) { + cancelLoadLocal(); + } + OSAtomicOr32Barrier(1, &cancelled); + }; } - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request progressBlock:(RCTImageLoaderProgressBlock)progressHandler completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate))completionHandler { - // Check if networking module is available - if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) { - RCTLogError(@"No suitable image URL loader found for %@. You may need to " - " import the RCTNetwork library in order to load images.", - request.URL.absoluteString); - return NULL; - } - - RCTNetworking *networking = [_bridge networking]; - - // Check if networking module can load image - if (RCT_DEBUG && ![networking canHandleRequest:request]) { - RCTLogError(@"No suitable image URL loader found for %@", request.URL.absoluteString); - return NULL; - } - - // Use networking module to load image - RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) { - // Check for system errors - if (error) { - completionHandler(error, nil, nil); - return; - } else if (!response) { - completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil); - return; - } else if (!data) { - completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil); - return; + // Check if networking module is available + if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) { + RCTLogError(@"No suitable image URL loader found for %@. You may need to " + " import the RCTNetwork library in order to load images.", + request.URL.absoluteString); + return NULL; } - // Check for http errors - NSString *responseDate; - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; - if (statusCode != 200) { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to load %@", response.URL]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage}; - completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain - code:statusCode - userInfo:userInfo], nil, nil); - return; - } + RCTNetworking *networking = [_bridge networking]; - responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"]; + // Check if networking module can load image + if (RCT_DEBUG && ![networking canHandleRequest:request]) { + RCTLogError(@"No suitable image URL loader found for %@", request.URL.absoluteString); + return NULL; } - // Call handler - completionHandler(nil, data, responseDate); - }; + // Use networking module to load image + RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) { + // Check for system errors + if (error) { + completionHandler(error, nil, nil); + return; + } else if (!response) { + completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil); + return; + } else if (!data) { + completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil); + return; + } - // Download image - __weak __typeof(self) weakSelf = self; - __block RCTNetworkTask *task = - [networking networkTaskWithRequest:request - completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) { - __typeof(self) strongSelf = weakSelf; - if (!strongSelf) { - return; + // Check for http errors + NSString *responseDate; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; + if (statusCode != 200) { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to load %@", response.URL]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage}; + completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain + code:statusCode + userInfo:userInfo], nil, nil); + return; + } + + responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"]; + } + + // Call handler + completionHandler(nil, data, responseDate); + }; + + // Download image + __weak __typeof(self) weakSelf = self; + __block RCTNetworkTask *task = + [networking networkTaskWithRequest:request + completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) { + __typeof(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (error || !response || !data) { + NSError *someError = nil; + if (error) { + someError = error; + } else if (!response) { + someError = RCTErrorWithMessage(@"Response metadata error"); + } else { + someError = RCTErrorWithMessage(@"Unknown image download error"); + } + completionHandler(someError, nil, nil); + [strongSelf dequeueTasks]; + return; + } + + dispatch_async(strongSelf->_URLRequestQueue, ^{ + // Process image data + processResponse(response, data, nil); + + // Prepare for next task + [strongSelf dequeueTasks]; + }); + }]; + + task.downloadProgressBlock = ^(int64_t progress, int64_t total) { + if (progressHandler) { + progressHandler(progress, total); + } + }; + + if (task) { + if (!_pendingTasks) { + _pendingTasks = [NSMutableArray new]; + } + [_pendingTasks addObject:task]; + [self dequeueTasks]; } - if (error || !response || !data) { - NSError *someError = nil; - if (error) { - someError = error; - } else if (!response) { - someError = RCTErrorWithMessage(@"Response metadata error"); - } else { - someError = RCTErrorWithMessage(@"Unknown image download error"); - } - completionHandler(someError, nil, nil); - [strongSelf dequeueTasks]; - return; - } - - dispatch_async(strongSelf->_URLRequestQueue, ^{ - // Process image data - processResponse(response, data, nil); - - // Prepare for next task - [strongSelf dequeueTasks]; - }); - }]; - - task.downloadProgressBlock = ^(int64_t progress, int64_t total) { - if (progressHandler) { - progressHandler(progress, total); - } - }; - - if (task) { - if (!_pendingTasks) { - _pendingTasks = [NSMutableArray new]; - } - [_pendingTasks addObject:task]; - [self dequeueTasks]; - } - - return ^{ - __typeof(self) strongSelf = weakSelf; - if (!strongSelf || !task) { - return; - } - dispatch_async(strongSelf->_URLRequestQueue, ^{ - [task cancel]; - task = nil; - }); - [strongSelf dequeueTasks]; - }; + return ^{ + __typeof(self) strongSelf = weakSelf; + if (!strongSelf || !task) { + return; + } + dispatch_async(strongSelf->_URLRequestQueue, ^{ + [task cancel]; + task = nil; + }); + [strongSelf dequeueTasks]; + }; } - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest @@ -524,74 +524,74 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image, partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { - __block volatile uint32_t cancelled = 0; - __block dispatch_block_t cancelLoad = nil; - dispatch_block_t cancellationBlock = ^{ - dispatch_block_t cancelLoadLocal = cancelLoad; - if (cancelLoadLocal && !cancelled) { - cancelLoadLocal(); - } - OSAtomicOr32Barrier(1, &cancelled); - }; - - __weak RCTImageLoader *weakSelf = self; - void (^completionHandler)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) { - __typeof(self) strongSelf = weakSelf; - if (cancelled || !strongSelf) { - return; - } - - if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) { - cancelLoad = nil; - completionBlock(error, imageOrData); - return; - } - - // Check decoded image cache - if (cacheResult) { - UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString - size:size - scale:scale - resizeMode:resizeMode - responseDate:fetchDate]; - if (image) { - cancelLoad = nil; - completionBlock(nil, image); - return; - } - } - - RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) { - if (cacheResult && image) { - // Store decoded image in cache - [[strongSelf imageCache] addImageToCache:image - URL:imageURLRequest.URL.absoluteString - size:size - scale:scale - resizeMode:resizeMode - responseDate:fetchDate]; - } - - cancelLoad = nil; - completionBlock(error_, image); + __block volatile uint32_t cancelled = 0; + __block dispatch_block_t cancelLoad = nil; + dispatch_block_t cancellationBlock = ^{ + dispatch_block_t cancelLoadLocal = cancelLoad; + if (cancelLoadLocal && !cancelled) { + cancelLoadLocal(); + } + OSAtomicOr32Barrier(1, &cancelled); }; - cancelLoad = [strongSelf decodeImageData:imageOrData - size:size - scale:scale - clipped:clipped - resizeMode:resizeMode - completionBlock:decodeCompletionHandler]; - }; + __weak RCTImageLoader *weakSelf = self; + void (^completionHandler)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) { + __typeof(self) strongSelf = weakSelf; + if (cancelled || !strongSelf) { + return; + } - cancelLoad = [self _loadImageOrDataWithURLRequest:imageURLRequest - size:size - scale:scale - resizeMode:resizeMode - progressBlock:progressBlock - partialLoadBlock:partialLoadBlock - completionBlock:completionHandler]; - return cancellationBlock; + if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) { + cancelLoad = nil; + completionBlock(error, imageOrData); + return; + } + + // Check decoded image cache + if (cacheResult) { + UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString + size:size + scale:scale + resizeMode:resizeMode + responseDate:fetchDate]; + if (image) { + cancelLoad = nil; + completionBlock(nil, image); + return; + } + } + + RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) { + if (cacheResult && image) { + // Store decoded image in cache + [[strongSelf imageCache] addImageToCache:image + URL:imageURLRequest.URL.absoluteString + size:size + scale:scale + resizeMode:resizeMode + responseDate:fetchDate]; + } + + cancelLoad = nil; + completionBlock(error_, image); + }; + + cancelLoad = [strongSelf decodeImageData:imageOrData + size:size + scale:scale + clipped:clipped + resizeMode:resizeMode + completionBlock:decodeCompletionHandler]; + }; + + cancelLoad = [self _loadImageOrDataWithURLRequest:imageURLRequest + size:size + scale:scale + resizeMode:resizeMode + progressBlock:progressBlock + partialLoadBlock:partialLoadBlock + completionBlock:completionHandler]; + return cancellationBlock; } - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data @@ -601,189 +601,213 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image, resizeMode:(RCTResizeMode)resizeMode completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { - if (data.length == 0) { - completionBlock(RCTErrorWithMessage(@"No image data"), nil); - return ^{}; - } - - __block volatile uint32_t cancelled = 0; - void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) { - if (RCTIsMainQueue()) { - // Most loaders do not return on the main thread, so caller is probably not - // expecting it, and may do expensive post-processing in the callback - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (!cancelled) { - completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image); - } - }); - } else if (!cancelled) { - completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image); + if (data.length == 0) { + completionBlock(RCTErrorWithMessage(@"No image data"), nil); + return ^{}; } - }; - id imageDecoder = [self imageDataDecoderForData:data]; - if (imageDecoder) { - return [imageDecoder decodeImageData:data - size:size - scale:scale - resizeMode:resizeMode - completionHandler:completionHandler] ?: ^{}; - } else { - dispatch_block_t decodeBlock = ^{ - // Calculate the size, in bytes, that the decompressed image will require - NSInteger decodedImageBytes = (size.width * scale) * (size.height * scale) * 4; + __block volatile uint32_t cancelled = 0; + void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) { + if (RCTIsMainQueue()) { + // Most loaders do not return on the main thread, so caller is probably not + // expecting it, and may do expensive post-processing in the callback + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (!cancelled) { + completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image); + } + }); + } else if (!cancelled) { + completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image); + } + }; - // Mark these bytes as in-use - self->_activeBytes += decodedImageBytes; + id imageDecoder = [self imageDataDecoderForData:data]; + if (imageDecoder) { + return [imageDecoder decodeImageData:data + size:size + scale:scale + resizeMode:resizeMode + completionHandler:completionHandler] ?: ^{}; + } else { + dispatch_block_t decodeBlock = ^{ + // Calculate the size, in bytes, that the decompressed image will require + NSInteger decodedImageBytes = (size.width * scale) * (size.height * scale) * 4; - // Do actual decompression on a concurrent background queue - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (!cancelled) { + // Mark these bytes as in-use + self->_activeBytes += decodedImageBytes; - // Decompress the image data (this may be CPU and memory intensive) - UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode); + // Do actual decompression on a concurrent background queue + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (!cancelled) { + + // Decompress the image data (this may be CPU and memory intensive) + UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode); #if RCT_DEV - CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale); - CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale()); - if (imagePixelSize.width * imagePixelSize.height > - screenPixelSize.width * screenPixelSize.height) { - RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger " - "than the screen size %@", NSStringFromCGSize(imagePixelSize), - NSStringFromCGSize(screenPixelSize)); - } + CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale); + CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale()); + if (imagePixelSize.width * imagePixelSize.height > + screenPixelSize.width * screenPixelSize.height) { + RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger " + "than the screen size %@", NSStringFromCGSize(imagePixelSize), + NSStringFromCGSize(screenPixelSize)); + } #endif - if (image) { - completionHandler(nil, image); - } else { - NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data ", data, data.length]; - NSError *finalError = RCTErrorWithMessage(errorMessage); - completionHandler(finalError, nil); - } + if (image) { + completionHandler(nil, image); + } else { + NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data ", data, data.length]; + NSError *finalError = RCTErrorWithMessage(errorMessage); + completionHandler(finalError, nil); + } + } + + // We're no longer retaining the uncompressed data, so now we'll mark + // the decoding as complete so that the loading task queue can resume. + dispatch_async(self->_URLRequestQueue, ^{ + self->_scheduledDecodes--; + self->_activeBytes -= decodedImageBytes; + [self dequeueTasks]; + }); + }); + }; + + if (!_URLRequestQueue) { + [self setUp]; } + dispatch_async(_URLRequestQueue, ^{ + // The decode operation retains the compressed image data until it's + // complete, so we'll mark it as having started, in order to block + // further image loads from happening until we're done with the data. + self->_scheduledDecodes++; - // We're no longer retaining the uncompressed data, so now we'll mark - // the decoding as complete so that the loading task queue can resume. - dispatch_async(self->_URLRequestQueue, ^{ - self->_scheduledDecodes--; - self->_activeBytes -= decodedImageBytes; - [self dequeueTasks]; + if (!self->_pendingDecodes) { + self->_pendingDecodes = [NSMutableArray new]; + } + NSInteger activeDecodes = self->_scheduledDecodes - self->_pendingDecodes.count - 1; + if (activeDecodes == 0 || (self->_activeBytes <= self->_maxConcurrentDecodingBytes && + activeDecodes <= self->_maxConcurrentDecodingTasks)) { + decodeBlock(); + } else { + [self->_pendingDecodes addObject:decodeBlock]; + } }); - }); - }; - if (!_URLRequestQueue) { - [self setUp]; + return ^{ + OSAtomicOr32Barrier(1, &cancelled); + }; } - dispatch_async(_URLRequestQueue, ^{ - // The decode operation retains the compressed image data until it's - // complete, so we'll mark it as having started, in order to block - // further image loads from happening until we're done with the data. - self->_scheduledDecodes++; - - if (!self->_pendingDecodes) { - self->_pendingDecodes = [NSMutableArray new]; - } - NSInteger activeDecodes = self->_scheduledDecodes - self->_pendingDecodes.count - 1; - if (activeDecodes == 0 || (self->_activeBytes <= self->_maxConcurrentDecodingBytes && - activeDecodes <= self->_maxConcurrentDecodingTasks)) { - decodeBlock(); - } else { - [self->_pendingDecodes addObject:decodeBlock]; - } - }); - - return ^{ - OSAtomicOr32Barrier(1, &cancelled); - }; - } } - (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest block:(void(^)(NSError *error, CGSize size))callback { - void (^completion)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) { - CGSize size; - if ([imageOrData isKindOfClass:[NSData class]]) { - NSDictionary *meta = RCTGetImageMetadata(imageOrData); - size = (CGSize){ - [meta[(id)kCGImagePropertyPixelWidth] doubleValue], - [meta[(id)kCGImagePropertyPixelHeight] doubleValue], - }; - } else { - UIImage *image = imageOrData; - size = (CGSize){ - image.size.width * image.scale, - image.size.height * image.scale, - }; - } - callback(error, size); - }; + void (^completion)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) { + CGSize size; + if ([imageOrData isKindOfClass:[NSData class]]) { + NSDictionary *meta = RCTGetImageMetadata(imageOrData); + size = (CGSize){ + [meta[(id)kCGImagePropertyPixelWidth] doubleValue], + [meta[(id)kCGImagePropertyPixelHeight] doubleValue], + }; + } else { + UIImage *image = imageOrData; + size = (CGSize){ + image.size.width * image.scale, + image.size.height * image.scale, + }; + } + callback(error, size); + }; - return [self _loadImageOrDataWithURLRequest:imageURLRequest - size:CGSizeZero - scale:1 - resizeMode:RCTResizeModeStretch - progressBlock:NULL - partialLoadBlock:NULL - completionBlock:completion]; + return [self _loadImageOrDataWithURLRequest:imageURLRequest + size:CGSizeZero + scale:1 + resizeMode:RCTResizeModeStretch + progressBlock:NULL + partialLoadBlock:NULL + completionBlock:completion]; } #pragma mark - RCTURLRequestHandler - (BOOL)canHandleRequest:(NSURLRequest *)request { - NSURL *requestURL = request.URL; - for (id loader in _loaders) { - // Don't use RCTImageURLLoader protocol for modules that already conform to - // RCTURLRequestHandler as it's inefficient to decode an image and then - // convert it back into data - if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] && - [loader canLoadImageURL:requestURL]) { - return YES; + NSURL *requestURL = request.URL; + + // If the data being loaded is a video, return NO + // Even better may be to implement this on the RCTImageURLLoader that would try to load it, + // but we'd have to run the logic both in RCTPhotoLibraryImageLoader and + // RCTAssetsLibraryRequestHandler. Once we drop iOS7 though, we'd drop + // RCTAssetsLibraryRequestHandler and can move it there. + static NSRegularExpression *videoRegex = nil; + if (!videoRegex) { + NSError *error = nil; + videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)" + options:NSRegularExpressionCaseInsensitive + error:&error]; + if (error) { + RCTLogError(@"%@", error); + } } - } - return NO; + + NSString *query = requestURL.query; + if (query != nil && [videoRegex firstMatchInString:query + options:0 + range:NSMakeRange(0, query.length)]) { + return NO; + } + + for (id loader in _loaders) { + // Don't use RCTImageURLLoader protocol for modules that already conform to + // RCTURLRequestHandler as it's inefficient to decode an image and then + // convert it back into data + if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] && + [loader canLoadImageURL:requestURL]) { + return YES; + } + } + return NO; } - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate { - __block RCTImageLoaderCancellationBlock requestToken; - requestToken = [self loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { - if (error) { - [delegate URLRequest:requestToken didCompleteWithError:error]; - return; - } + __block RCTImageLoaderCancellationBlock requestToken; + requestToken = [self loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { + if (error) { + [delegate URLRequest:requestToken didCompleteWithError:error]; + return; + } - NSString *mimeType = nil; - NSData *imageData = nil; - if (RCTImageHasAlpha(image.CGImage)) { - mimeType = @"image/png"; - imageData = UIImagePNGRepresentation(image); - } else { - mimeType = @"image/jpeg"; - imageData = UIImageJPEGRepresentation(image, 1.0); - } + NSString *mimeType = nil; + NSData *imageData = nil; + if (RCTImageHasAlpha(image.CGImage)) { + mimeType = @"image/png"; + imageData = UIImagePNGRepresentation(image); + } else { + mimeType = @"image/jpeg"; + imageData = UIImageJPEGRepresentation(image, 1.0); + } - NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL - MIMEType:mimeType - expectedContentLength:imageData.length - textEncodingName:nil]; + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL + MIMEType:mimeType + expectedContentLength:imageData.length + textEncodingName:nil]; - [delegate URLRequest:requestToken didReceiveResponse:response]; - [delegate URLRequest:requestToken didReceiveData:imageData]; - [delegate URLRequest:requestToken didCompleteWithError:nil]; - }]; + [delegate URLRequest:requestToken didReceiveResponse:response]; + [delegate URLRequest:requestToken didReceiveData:imageData]; + [delegate URLRequest:requestToken didCompleteWithError:nil]; + }]; - return requestToken; + return requestToken; } - (void)cancelRequest:(id)requestToken { - if (requestToken) { - ((RCTImageLoaderCancellationBlock)requestToken)(); - } + if (requestToken) { + ((RCTImageLoaderCancellationBlock)requestToken)(); + } } @end @@ -792,7 +816,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image, - (RCTImageLoader *)imageLoader { - return [self moduleForClass:[RCTImageLoader class]]; + return [self moduleForClass:[RCTImageLoader class]]; } @end