Skip to content

Commit

Permalink
Animated WebP support (TextureGroup#605)
Browse files Browse the repository at this point in the history
* Updating to support animated WebP

* Fix a deadlock with display link

* Fix playhead issue.

* Fix up timing on iOS 10 and above

* Don't redraw the same frame over and over

* Clear out layer contents if we're an animated GIF on exit range

* Clear out cover image on exit of visible range

* Don't set cover image if we're no longer in display range.

* Don't clear out image if we're not an animated image

* Only set image if we're not already animating

* Get rid of changes to podfile

* Add CHANGELOG entry

* Update license

* Update PINRemoteImage

* Remove commented out lines in example
  • Loading branch information
garrettmoon authored and bernieperez committed Apr 25, 2018
1 parent 6bb278c commit 0cdb46d
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder)
- [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465)
- [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy)
- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon)

## 2.5

Expand Down
4 changes: 2 additions & 2 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "pinterest/PINRemoteImage" "3.0.0-beta.12"
github "pinterest/PINCache" "3.0.1-beta.5"
github "pinterest/PINRemoteImage" "3.0.0-beta.13"
github "pinterest/PINCache"
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ target :'AsyncDisplayKitTests' do
pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master'

# Only for buck build
pod 'PINRemoteImage', '3.0.0-beta.10'
pod 'PINRemoteImage', '3.0.0-beta.13'
end

#TODO CocoaPods plugin instead?
Expand Down
63 changes: 51 additions & 12 deletions Source/ASImageNode+AnimatedImage.mm
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@ - (void)_locked_setAnimatedImage:(id <ASAnimatedImageProtocol>)animatedImage
};
}

animatedImage.playbackReadyCallback = ^{
// In this case the lock is already gone we have to call the unlocked version therefore
[weakSelf setShouldAnimate:YES];
};
if (animatedImage.playbackReady) {
[self _locked_setShouldAnimate:YES];
} else {
animatedImage.playbackReadyCallback = ^{
// In this case the lock is already gone we have to call the unlocked version therefore
[weakSelf setShouldAnimate:YES];
};
}
} else {
// Clean up after ourselves.
self.contents = nil;
[self setCoverImage:nil];
}

[self animatedImageSet:_animatedImage previousAnimatedImage:previousAnimatedImage];
Expand Down Expand Up @@ -107,8 +110,10 @@ - (BOOL)animatedImagePaused

- (void)setCoverImageCompleted:(UIImage *)coverImage
{
ASDN::MutexLocker l(__instanceLock__);
[self _locked_setCoverImageCompleted:coverImage];
if (ASInterfaceStateIncludesDisplay(self.interfaceState)) {
ASDN::MutexLocker l(__instanceLock__);
[self _locked_setCoverImageCompleted:coverImage];
}
}

- (void)_locked_setCoverImageCompleted:(UIImage *)coverImage
Expand All @@ -132,9 +137,12 @@ - (void)_locked_setCoverImage:(UIImage *)coverImage
{
//If we're a network image node, we want to set the default image so
//that it will correctly be restored if it exits the range.
#if ASAnimatedImageDebug
NSLog(@"setting cover image: %p", self);
#endif
if ([self isKindOfClass:[ASNetworkImageNode class]]) {
[(ASNetworkImageNode *)self _locked_setDefaultImage:coverImage];
} else {
} else if (_displayLink == nil || _displayLink.paused == YES) {
[self _locked_setImage:coverImage];
}
}
Expand Down Expand Up @@ -218,11 +226,14 @@ - (void)_locked_startAnimating
NSLog(@"starting animation: %p", self);
#endif

// Get frame interval before holding display link lock to avoid deadlock
NSUInteger frameInterval = self.animatedImage.frameInterval;
ASDN::MutexLocker l(_displayLinkLock);
if (_displayLink == nil) {
_playHead = 0;
_displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)];
_displayLink.frameInterval = self.animatedImage.frameInterval;
_displayLink.frameInterval = frameInterval;
_lastSuccessfulFrameIndex = NSUIntegerMax;

[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode];
} else {
Expand Down Expand Up @@ -263,7 +274,9 @@ - (void)didEnterVisibleState
if (self.animatedImage.coverImageReady) {
[self setCoverImage:self.animatedImage.coverImage];
}
[self startAnimating];
if (self.animatedImage.playbackReady) {
[self startAnimating];
}
}

- (void)didExitVisibleState
Expand All @@ -274,6 +287,26 @@ - (void)didExitVisibleState
[self stopAnimating];
}

- (void)didExitDisplayState
{
ASDisplayNodeAssertMainThread();
#if ASAnimatedImageDebug
NSLog(@"exiting display state: %p", self);
#endif

// Check to see if we're an animated image before calling super in case someone
// decides they want to clear out the animatedImage itself on exiting the display
// state
BOOL isAnimatedImage = self.animatedImage != nil;
[super didExitDisplayState];

// Also clear out the contents we've set to be good citizens, we'll put it back in when we become visible.
if (isAnimatedImage) {
self.contents = nil;
[self setCoverImage:nil];
}
}

#pragma mark - Display Link Callbacks

- (void)displayLinkFired:(CADisplayLink *)displayLink
Expand All @@ -283,6 +316,8 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink
CFTimeInterval timeBetweenLastFire;
if (self.lastDisplayLinkFire == 0) {
timeBetweenLastFire = 0;
} else if (AS_AT_LEAST_IOS10){
timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp;
} else {
timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire;
}
Expand All @@ -291,7 +326,8 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink
_playHead += timeBetweenLastFire;

while (_playHead > self.animatedImage.totalDuration) {
_playHead -= self.animatedImage.totalDuration;
// Set playhead to zero to keep from showing different frames on different playthroughs
_playHead = 0;
_playedLoops++;
}

Expand All @@ -301,15 +337,18 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink
}

NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead];
if (frameIndex == _lastSuccessfulFrameIndex) {
return;
}
CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex];

if (frameImage == nil) {
_playHead -= timeBetweenLastFire;
//Pause the display link until we get a file ready notification
displayLink.paused = YES;
self.lastDisplayLinkFire = 0;
} else {
self.contents = (__bridge id)frameImage;
_lastSuccessfulFrameIndex = frameIndex;
[self displayDidFinish];
}
}
Expand Down
52 changes: 26 additions & 26 deletions Source/Details/ASPINRemoteImageDownloader.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASImageContainerProtocolCategories.h>

#if __has_include (<PINRemoteImage/PINAnimatedImage.h>)
#if __has_include (<PINRemoteImage/PINGIFAnimatedImage.h>)
#define PIN_ANIMATED_AVAILABLE 1
#import <PINRemoteImage/PINAnimatedImage.h>
#import <PINRemoteImage/PINCachedAnimatedImage.h>
#import <PINRemoteImage/PINAlternateRepresentationProvider.h>
#else
#define PIN_ANIMATED_AVAILABLE 0
#endif

#if __has_include(<webp/decode.h>)
#define PIN_WEBP_AVAILABLE 1
#else
#define PIN_WEBP_AVAILABLE 0
#endif

#import <PINRemoteImage/PINRemoteImageManager.h>
#import <PINRemoteImage/NSData+ImageDetectors.h>
#import <PINRemoteImage/PINRemoteImageCaching.h>
Expand All @@ -42,35 +48,23 @@ @interface ASPINRemoteImageDownloader () <PINRemoteImageManagerAlternateRepresen

@end

@interface PINAnimatedImage (ASPINRemoteImageDownloader) <ASAnimatedImageProtocol>
@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) <ASAnimatedImageProtocol>

@end

@implementation PINAnimatedImage (ASPINRemoteImageDownloader)

- (void)setCoverImageReadyCallback:(void (^)(UIImage * _Nonnull))coverImageReadyCallback
{
self.infoCompletion = coverImageReadyCallback;
}

- (void (^)(UIImage * _Nonnull))coverImageReadyCallback
{
return self.infoCompletion;
}

- (void)setPlaybackReadyCallback:(dispatch_block_t)playbackReadyCallback
{
self.fileReady = playbackReadyCallback;
}

- (dispatch_block_t)playbackReadyCallback
{
return self.fileReady;
}
@implementation PINCachedAnimatedImage (ASPINRemoteImageDownloader)

- (BOOL)isDataSupported:(NSData *)data
{
return [data pin_isGIF];
if ([data pin_isGIF]) {
return YES;
}
#if PIN_WEBP_AVAILABLE
else if ([data pin_isAnimatedWebP]) {
return YES;
}
#endif
return NO;
}

@end
Expand Down Expand Up @@ -187,7 +181,7 @@ - (BOOL)sharedImageManagerSupportsMemoryRemoval
#if PIN_ANIMATED_AVAILABLE
- (nullable id <ASAnimatedImageProtocol>)animatedImageWithData:(NSData *)animatedImageData
{
return [[PINAnimatedImage alloc] initWithAnimatedImageData:animatedImageData];
return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData];
}
#endif

Expand Down Expand Up @@ -365,6 +359,12 @@ - (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageMana
if ([data pin_isGIF]) {
return data;
}
#if PIN_WEBP_AVAILABLE
else if ([data pin_isAnimatedWebP]) {
return data;
}
#endif

#endif
return nil;
}
Expand Down
1 change: 1 addition & 0 deletions Source/Private/ASImageNode+AnimatedImagePrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode;
BOOL _animatedImagePaused;
NSString *_animatedImageRunLoopMode;
CADisplayLink *_displayLink;
NSUInteger _lastSuccessfulFrameIndex;

//accessed on main thread only
CFTimeInterval _playHead;
Expand Down
2 changes: 1 addition & 1 deletion Texture.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Pod::Spec.new do |spec|
end

spec.subspec 'PINRemoteImage' do |pin|
pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.12'
pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.13'
pin.dependency 'PINRemoteImage/PINCache'
pin.dependency 'Texture/Core'
end
Expand Down
22 changes: 11 additions & 11 deletions examples/AnimatedGIF/ASAnimatedImage/ViewController.m
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
//
// ViewController.m
// Sample
//
// Created by Garrett Moon on 3/22/16.
// Texture
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// http://www.apache.org/licenses/LICENSE-2.0
//

#import "ViewController.h"
Expand All @@ -33,6 +31,8 @@ - (void)viewDidLoad {

ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init];
imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"];
// Uncomment to see animated webp support
// imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"];
imageNode.frame = self.view.bounds;
imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
imageNode.contentMode = UIViewContentModeScaleAspectFit;
Expand Down
1 change: 1 addition & 0 deletions examples/AnimatedGIF/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
target 'Sample' do
pod 'Texture', :path => '../..'
pod 'PINRemoteImage/WebP'
end

0 comments on commit 0cdb46d

Please sign in to comment.