Skip to content

Commit 4dbb644

Browse files
authored
Animated WebP support (TextureGroup#605)
* 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
1 parent 1705ec0 commit 4dbb644

9 files changed

Lines changed: 95 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [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)
99
- [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465)
1010
- [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy)
11+
- [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)
1112

1213
## 2.5
1314

Cartfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
github "pinterest/PINRemoteImage" "3.0.0-beta.12"
2-
github "pinterest/PINCache" "3.0.1-beta.5"
1+
github "pinterest/PINRemoteImage" "3.0.0-beta.13"
2+
github "pinterest/PINCache"

Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ target :'AsyncDisplayKitTests' do
88
pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master'
99

1010
# Only for buck build
11-
pod 'PINRemoteImage', '3.0.0-beta.10'
11+
pod 'PINRemoteImage', '3.0.0-beta.13'
1212
end
1313

1414
#TODO CocoaPods plugin instead?

Source/ASImageNode+AnimatedImage.mm

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,17 @@ - (void)_locked_setAnimatedImage:(id <ASAnimatedImageProtocol>)animatedImage
6666
};
6767
}
6868

69+
animatedImage.playbackReadyCallback = ^{
70+
// In this case the lock is already gone we have to call the unlocked version therefore
71+
[weakSelf setShouldAnimate:YES];
72+
};
6973
if (animatedImage.playbackReady) {
7074
[self _locked_setShouldAnimate:YES];
71-
} else {
72-
animatedImage.playbackReadyCallback = ^{
73-
// In this case the lock is already gone we have to call the unlocked version therefore
74-
[weakSelf setShouldAnimate:YES];
75-
};
7675
}
76+
} else {
77+
// Clean up after ourselves.
78+
self.contents = nil;
79+
[self setCoverImage:nil];
7780
}
7881

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

108111
- (void)setCoverImageCompleted:(UIImage *)coverImage
109112
{
110-
ASDN::MutexLocker l(__instanceLock__);
111-
[self _locked_setCoverImageCompleted:coverImage];
113+
if (ASInterfaceStateIncludesDisplay(self.interfaceState)) {
114+
ASDN::MutexLocker l(__instanceLock__);
115+
[self _locked_setCoverImageCompleted:coverImage];
116+
}
112117
}
113118

114119
- (void)_locked_setCoverImageCompleted:(UIImage *)coverImage
@@ -132,9 +137,12 @@ - (void)_locked_setCoverImage:(UIImage *)coverImage
132137
{
133138
//If we're a network image node, we want to set the default image so
134139
//that it will correctly be restored if it exits the range.
140+
#if ASAnimatedImageDebug
141+
NSLog(@"setting cover image: %p", self);
142+
#endif
135143
if ([self isKindOfClass:[ASNetworkImageNode class]]) {
136144
[(ASNetworkImageNode *)self _locked_setDefaultImage:coverImage];
137-
} else {
145+
} else if (_displayLink == nil || _displayLink.paused == YES) {
138146
[self _locked_setImage:coverImage];
139147
}
140148
}
@@ -218,11 +226,14 @@ - (void)_locked_startAnimating
218226
NSLog(@"starting animation: %p", self);
219227
#endif
220228

229+
// Get frame interval before holding display link lock to avoid deadlock
230+
NSUInteger frameInterval = self.animatedImage.frameInterval;
221231
ASDN::MutexLocker l(_displayLinkLock);
222232
if (_displayLink == nil) {
223233
_playHead = 0;
224234
_displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)];
225-
_displayLink.frameInterval = self.animatedImage.frameInterval;
235+
_displayLink.frameInterval = frameInterval;
236+
_lastSuccessfulFrameIndex = NSUIntegerMax;
226237

227238
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode];
228239
} else {
@@ -263,7 +274,9 @@ - (void)didEnterVisibleState
263274
if (self.animatedImage.coverImageReady) {
264275
[self setCoverImage:self.animatedImage.coverImage];
265276
}
266-
[self startAnimating];
277+
if (self.animatedImage.playbackReady) {
278+
[self startAnimating];
279+
}
267280
}
268281

269282
- (void)didExitVisibleState
@@ -274,6 +287,26 @@ - (void)didExitVisibleState
274287
[self stopAnimating];
275288
}
276289

290+
- (void)didExitDisplayState
291+
{
292+
ASDisplayNodeAssertMainThread();
293+
#if ASAnimatedImageDebug
294+
NSLog(@"exiting display state: %p", self);
295+
#endif
296+
297+
// Check to see if we're an animated image before calling super in case someone
298+
// decides they want to clear out the animatedImage itself on exiting the display
299+
// state
300+
BOOL isAnimatedImage = self.animatedImage != nil;
301+
[super didExitDisplayState];
302+
303+
// Also clear out the contents we've set to be good citizens, we'll put it back in when we become visible.
304+
if (isAnimatedImage) {
305+
self.contents = nil;
306+
[self setCoverImage:nil];
307+
}
308+
}
309+
277310
#pragma mark - Display Link Callbacks
278311

279312
- (void)displayLinkFired:(CADisplayLink *)displayLink
@@ -283,6 +316,8 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink
283316
CFTimeInterval timeBetweenLastFire;
284317
if (self.lastDisplayLinkFire == 0) {
285318
timeBetweenLastFire = 0;
319+
} else if (AS_AT_LEAST_IOS10){
320+
timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp;
286321
} else {
287322
timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire;
288323
}
@@ -291,7 +326,8 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink
291326
_playHead += timeBetweenLastFire;
292327

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

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

303339
NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead];
340+
if (frameIndex == _lastSuccessfulFrameIndex) {
341+
return;
342+
}
304343
CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex];
305344

306345
if (frameImage == nil) {
307-
_playHead -= timeBetweenLastFire;
308346
//Pause the display link until we get a file ready notification
309347
displayLink.paused = YES;
310348
self.lastDisplayLinkFire = 0;
311349
} else {
312350
self.contents = (__bridge id)frameImage;
351+
_lastSuccessfulFrameIndex = frameIndex;
313352
[self displayDidFinish];
314353
}
315354
}

Source/Details/ASPINRemoteImageDownloader.m

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,20 @@
2424
#import <AsyncDisplayKit/ASThread.h>
2525
#import <AsyncDisplayKit/ASImageContainerProtocolCategories.h>
2626

27-
#if __has_include (<PINRemoteImage/PINAnimatedImage.h>)
27+
#if __has_include (<PINRemoteImage/PINGIFAnimatedImage.h>)
2828
#define PIN_ANIMATED_AVAILABLE 1
29-
#import <PINRemoteImage/PINAnimatedImage.h>
29+
#import <PINRemoteImage/PINCachedAnimatedImage.h>
3030
#import <PINRemoteImage/PINAlternateRepresentationProvider.h>
3131
#else
3232
#define PIN_ANIMATED_AVAILABLE 0
3333
#endif
3434

35+
#if __has_include(<webp/decode.h>)
36+
#define PIN_WEBP_AVAILABLE 1
37+
#else
38+
#define PIN_WEBP_AVAILABLE 0
39+
#endif
40+
3541
#import <PINRemoteImage/PINRemoteImageManager.h>
3642
#import <PINRemoteImage/NSData+ImageDetectors.h>
3743
#import <PINRemoteImage/PINRemoteImageCaching.h>
@@ -42,35 +48,23 @@ @interface ASPINRemoteImageDownloader () <PINRemoteImageManagerAlternateRepresen
4248

4349
@end
4450

45-
@interface PINAnimatedImage (ASPINRemoteImageDownloader) <ASAnimatedImageProtocol>
51+
@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) <ASAnimatedImageProtocol>
4652

4753
@end
4854

49-
@implementation PINAnimatedImage (ASPINRemoteImageDownloader)
50-
51-
- (void)setCoverImageReadyCallback:(void (^)(UIImage * _Nonnull))coverImageReadyCallback
52-
{
53-
self.infoCompletion = coverImageReadyCallback;
54-
}
55-
56-
- (void (^)(UIImage * _Nonnull))coverImageReadyCallback
57-
{
58-
return self.infoCompletion;
59-
}
60-
61-
- (void)setPlaybackReadyCallback:(dispatch_block_t)playbackReadyCallback
62-
{
63-
self.fileReady = playbackReadyCallback;
64-
}
65-
66-
- (dispatch_block_t)playbackReadyCallback
67-
{
68-
return self.fileReady;
69-
}
55+
@implementation PINCachedAnimatedImage (ASPINRemoteImageDownloader)
7056

7157
- (BOOL)isDataSupported:(NSData *)data
7258
{
73-
return [data pin_isGIF];
59+
if ([data pin_isGIF]) {
60+
return YES;
61+
}
62+
#if PIN_WEBP_AVAILABLE
63+
else if ([data pin_isAnimatedWebP]) {
64+
return YES;
65+
}
66+
#endif
67+
return NO;
7468
}
7569

7670
@end
@@ -187,7 +181,7 @@ - (BOOL)sharedImageManagerSupportsMemoryRemoval
187181
#if PIN_ANIMATED_AVAILABLE
188182
- (nullable id <ASAnimatedImageProtocol>)animatedImageWithData:(NSData *)animatedImageData
189183
{
190-
return [[PINAnimatedImage alloc] initWithAnimatedImageData:animatedImageData];
184+
return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData];
191185
}
192186
#endif
193187

@@ -365,6 +359,12 @@ - (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageMana
365359
if ([data pin_isGIF]) {
366360
return data;
367361
}
362+
#if PIN_WEBP_AVAILABLE
363+
else if ([data pin_isAnimatedWebP]) {
364+
return data;
365+
}
366+
#endif
367+
368368
#endif
369369
return nil;
370370
}

Source/Private/ASImageNode+AnimatedImagePrivate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode;
2626
BOOL _animatedImagePaused;
2727
NSString *_animatedImageRunLoopMode;
2828
CADisplayLink *_displayLink;
29+
NSUInteger _lastSuccessfulFrameIndex;
2930

3031
//accessed on main thread only
3132
CFTimeInterval _playHead;

Texture.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Pod::Spec.new do |spec|
4545
end
4646

4747
spec.subspec 'PINRemoteImage' do |pin|
48-
pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.12'
48+
pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.13'
4949
pin.dependency 'PINRemoteImage/PINCache'
5050
pin.dependency 'Texture/Core'
5151
end

examples/AnimatedGIF/ASAnimatedImage/ViewController.m

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
//
22
// ViewController.m
3-
// Sample
4-
//
5-
// Created by Garrett Moon on 3/22/16.
3+
// Texture
64
//
75
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
86
// This source code is licensed under the BSD-style license found in the
9-
// LICENSE file in the root directory of this source tree. An additional grant
10-
// of patent rights can be found in the PATENTS file in the same directory.
7+
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
8+
// grant of patent rights can be found in the PATENTS file in the same directory.
9+
//
10+
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
11+
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
12+
// you may not use this file except in compliance with the License.
13+
// You may obtain a copy of the License at
1114
//
12-
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13-
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14-
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
15-
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
16-
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
17-
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+
// http://www.apache.org/licenses/LICENSE-2.0
1816
//
1917

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

3432
ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init];
3533
imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"];
34+
// Uncomment to see animated webp support
35+
// imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"];
3636
imageNode.frame = self.view.bounds;
3737
imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
3838
imageNode.contentMode = UIViewContentModeScaleAspectFit;

examples/AnimatedGIF/Podfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ source 'https://github.com/CocoaPods/Specs.git'
22
platform :ios, '8.0'
33
target 'Sample' do
44
pod 'Texture', :path => '../..'
5+
pod 'PINRemoteImage/WebP'
56
end
67

0 commit comments

Comments
 (0)