1
/*
2
* This file is part of the SDWebImage package.
3
* (c) Olivier Poitrey <rs@dailymotion.com>
4
*
5
* For the full copyright and license information, please view the LICENSE
6
* file that was distributed with this source code.
7
*/
8

9
#import "SDAnimatedImagePlayer.h"
10
#import "NSImage+Compatibility.h"
11
#import "SDDisplayLink.h"
12
#import "SDDeviceHelper.h"
13
#import "SDInternalMacros.h"
14

15
@interface SDAnimatedImagePlayer () {
16
    NSRunLoopMode _runLoopMode;
17
}
18

19
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
20
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
21
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
22
@property (nonatomic, strong) id<SDAnimatedImageProvider> animatedProvider;
23
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *frameBuffer;
24
@property (nonatomic, assign) NSTimeInterval currentTime;
25
@property (nonatomic, assign) BOOL bufferMiss;
26
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
27
@property (nonatomic, assign) NSUInteger maxBufferCount;
28
@property (nonatomic, strong) NSOperationQueue *fetchQueue;
29
@property (nonatomic, strong) dispatch_semaphore_t lock;
30
@property (nonatomic, strong) SDDisplayLink *displayLink;
31

32
@end
33

34
@implementation SDAnimatedImagePlayer
35

36 2
- (instancetype)initWithProvider:(id<SDAnimatedImageProvider>)provider {
37 2
    self = [super init];
38 2
    if (self) {
39 2
        NSUInteger animatedImageFrameCount = provider.animatedImageFrameCount;
40 2
        // Check the frame count
41 2
        if (animatedImageFrameCount <= 1) {
42 0
            return nil;
43
        }
44 2
        self.totalFrameCount = animatedImageFrameCount;
45 2
        // Get the current frame and loop count.
46 2
        self.totalLoopCount = provider.animatedImageLoopCount;
47 2
        self.animatedProvider = provider;
48 2
        self.playbackRate = 1.0;
49 1
#if SD_UIKIT
50 1
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
51 1
#endif
52
    }
53 2
    return self;
54
}
55

56 2
+ (instancetype)playerWithProvider:(id<SDAnimatedImageProvider>)provider {
57 2
    SDAnimatedImagePlayer *player = [[SDAnimatedImagePlayer alloc] initWithProvider:provider];
58 2
    return player;
59
}
60

61
#pragma mark - Life Cycle
62

63 2
- (void)dealloc {
64 1
#if SD_UIKIT
65 1
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
66 1
#endif
67
}
68

69 1
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
70 1
    [_fetchQueue cancelAllOperations];
71 1
    [_fetchQueue addOperationWithBlock:^{
72 1
        NSNumber *currentFrameIndex = @(self.currentFrameIndex);
73 1
        SD_LOCK(self.lock);
74 1
        NSArray *keys = self.frameBuffer.allKeys;
75 1
        // only keep the next frame for later rendering
76 1
        for (NSNumber * key in keys) {
77 1
            if (![key isEqualToNumber:currentFrameIndex]) {
78 1
                [self.frameBuffer removeObjectForKey:key];
79
            }
80
        }
81 1
        SD_UNLOCK(self.lock);
82 1
    }];
83
}
84

85
#pragma mark - Private
86 2
- (NSOperationQueue *)fetchQueue {
87 2
    if (!_fetchQueue) {
88 2
        _fetchQueue = [[NSOperationQueue alloc] init];
89 2
        _fetchQueue.maxConcurrentOperationCount = 1;
90
    }
91 2
    return _fetchQueue;
92
}
93

94 2
- (NSMutableDictionary<NSNumber *,UIImage *> *)frameBuffer {
95 2
    if (!_frameBuffer) {
96 2
        _frameBuffer = [NSMutableDictionary dictionary];
97
    }
98 2
    return _frameBuffer;
99
}
100

101 2
- (dispatch_semaphore_t)lock {
102 2
    if (!_lock) {
103 2
        _lock = dispatch_semaphore_create(1);
104
    }
105 2
    return _lock;
106
}
107

108 2
- (SDDisplayLink *)displayLink {
109 2
    if (!_displayLink) {
110 2
        _displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)];
111 2
        [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
112 2
        [_displayLink stop];
113
    }
114 2
    return _displayLink;
115
}
116

117 2
- (void)setRunLoopMode:(NSRunLoopMode)runLoopMode {
118 2
    if ([_runLoopMode isEqual:runLoopMode]) {
119 2
        return;
120
    }
121 2
    if (_displayLink) {
122 0
        if (_runLoopMode) {
123 0
            [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runLoopMode];
124
        }
125 0
        if (runLoopMode.length > 0) {
126 0
            [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode];
127
        }
128
    }
129 2
    _runLoopMode = [runLoopMode copy];
130
}
131

132 2
- (NSRunLoopMode)runLoopMode {
133 2
    if (!_runLoopMode) {
134 0
        _runLoopMode = [[self class] defaultRunLoopMode];
135
    }
136 2
    return _runLoopMode;
137
}
138

139
#pragma mark - State Control
140

141 2
- (void)setupCurrentFrame {
142 2
    if (self.currentFrameIndex != 0) {
143 0
        return;
144
    }
145 2
    if ([self.animatedProvider isKindOfClass:[UIImage class]]) {
146 2
        UIImage *image = (UIImage *)self.animatedProvider;
147 2
        // Use the poster image if available
148 1
        #if SD_MAC
149 1
        UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
150
        #else
151
        UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
152 1
        #endif
153 2
        if (posterFrame) {
154 2
            self.currentFrame = posterFrame;
155 2
            SD_LOCK(self.lock);
156 2
            self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame;
157 2
            SD_UNLOCK(self.lock);
158 2
            [self handleFrameChange];
159
        }
160
    }
161
}
162

163 2
- (void)resetCurrentFrameStatus {
164 2
    // These should not trigger KVO, user don't need to receive an `index == 0, image == nil` callback.
165 2
    _currentFrame = nil;
166 2
    _currentFrameIndex = 0;
167 2
    _currentLoopCount = 0;
168 2
    _currentTime = 0;
169 2
    _bufferMiss = NO;
170 2
    _needsDisplayWhenImageBecomesAvailable = NO;
171
}
172

173 2
- (void)clearFrameBuffer {
174 2
    SD_LOCK(self.lock);
175 2
    [_frameBuffer removeAllObjects];
176 2
    SD_UNLOCK(self.lock);
177
}
178

179
#pragma mark - Animation Control
180 2
- (void)startPlaying {
181 2
    [self.displayLink start];
182 2
    // Setup frame
183 2
    if (self.currentFrameIndex == 0 && !self.currentFrame) {
184 2
        [self setupCurrentFrame];
185
    }
186 2
    // Calculate max buffer size
187 2
    [self calculateMaxBufferCount];
188
}
189

190 2
- (void)stopPlaying {
191 2
    [_fetchQueue cancelAllOperations];
192 2
    // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method.
193 2
    [_displayLink stop];
194 2
    // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct.
195 2
    [self resetCurrentFrameStatus];
196
}
197

198 2
- (void)pausePlaying {
199 2
    [_fetchQueue cancelAllOperations];
200 2
    [_displayLink stop];
201
}
202

203 2
- (BOOL)isPlaying {
204 2
    return _displayLink.isRunning;
205
}
206

207 2
- (void)seekToFrameAtIndex:(NSUInteger)index loopCount:(NSUInteger)loopCount {
208 2
    if (index >= self.totalFrameCount) {
209 0
        return;
210
    }
211 2
    self.currentFrameIndex = index;
212 2
    self.currentLoopCount = loopCount;
213 2
    self.currentFrame = [self.animatedProvider animatedImageFrameAtIndex:index];
214 2
    [self handleFrameChange];
215
}
216

217
#pragma mark - Core Render
218 2
- (void)displayDidRefresh:(SDDisplayLink *)displayLink {
219 2
    // If for some reason a wild call makes it through when we shouldn't be animating, bail.
220 2
    // Early return!
221 2
    if (!self.isPlaying) {
222 1
        return;
223
    }
224
    
225 2
    NSUInteger totalFrameCount = self.totalFrameCount;
226 2
    if (totalFrameCount <= 1) {
227 0
        // Total frame count less than 1, wrong configuration and stop animating
228 0
        [self stopPlaying];
229 0
        return;
230
    }
231
    
232 2
    NSTimeInterval playbackRate = self.playbackRate;
233 2
    if (playbackRate <= 0) {
234 0
        // Does not support <= 0 play rate
235 0
        [self stopPlaying];
236 0
        return;
237
    }
238
    
239 2
    // Calculate refresh duration
240 2
    NSTimeInterval duration = self.displayLink.duration;
241
    
242 2
    NSUInteger currentFrameIndex = self.currentFrameIndex;
243 2
    NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
244
    
245 2
    // Check if we need to display new frame firstly
246 2
    BOOL bufferFull = NO;
247 2
    if (self.needsDisplayWhenImageBecomesAvailable) {
248 2
        UIImage *currentFrame;
249 2
        SD_LOCK(self.lock);
250 2
        currentFrame = self.frameBuffer[@(currentFrameIndex)];
251 2
        SD_UNLOCK(self.lock);
252
        
253 2
        // Update the current frame
254 2
        if (currentFrame) {
255 2
            SD_LOCK(self.lock);
256 2
            // Remove the frame buffer if need
257 2
            if (self.frameBuffer.count > self.maxBufferCount) {
258 0
                self.frameBuffer[@(currentFrameIndex)] = nil;
259
            }
260 2
            // Check whether we can stop fetch
261 2
            if (self.frameBuffer.count == totalFrameCount) {
262 2
                bufferFull = YES;
263
            }
264 2
            SD_UNLOCK(self.lock);
265
            
266 2
            // Update the current frame immediately
267 2
            self.currentFrame = currentFrame;
268 2
            [self handleFrameChange];
269
            
270 2
            self.bufferMiss = NO;
271 2
            self.needsDisplayWhenImageBecomesAvailable = NO;
272
        }
273 2
        else {
274 2
            self.bufferMiss = YES;
275
        }
276
    }
277
    
278 2
    // Check if we have the frame buffer
279 2
    if (!self.bufferMiss) {
280 2
        // Then check if timestamp is reached
281 2
        self.currentTime += duration;
282 2
        NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex];
283 2
        currentDuration = currentDuration / playbackRate;
284 2
        if (self.currentTime < currentDuration) {
285 2
            // Current frame timestamp not reached, return
286 2
            return;
287
        }
288
        
289 2
        // Otherwise, we should be ready to display next frame
290 2
        self.needsDisplayWhenImageBecomesAvailable = YES;
291 2
        self.currentFrameIndex = nextFrameIndex;
292 2
        self.currentTime -= currentDuration;
293 2
        NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex];
294 2
        nextDuration = nextDuration / playbackRate;
295 2
        if (self.currentTime > nextDuration) {
296 0
            // Do not skip frame
297 0
            self.currentTime = nextDuration;
298
        }
299
        
300 2
        // Update the loop count when last frame rendered
301 2
        if (nextFrameIndex == 0) {
302 2
            // Update the loop count
303 2
            self.currentLoopCount++;
304 2
            [self handleLoopChange];
305
            
306 2
            // if reached the max loop count, stop animating, 0 means loop indefinitely
307 2
            NSUInteger maxLoopCount = self.totalLoopCount;
308 2
            if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) {
309 0
                [self stopPlaying];
310 0
                return;
311
            }
312
        }
313
    }
314
    
315 2
    // Since we support handler, check animating state again
316 2
    if (!self.isPlaying) {
317 2
        return;
318
    }
319
    
320 2
    // Check if we should prefetch next frame or current frame
321 2
    // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
322 2
    // Or, most cases, the decode speed is faster than render speed, we fetch next frame
323 2
    NSUInteger fetchFrameIndex = self.bufferMiss? currentFrameIndex : nextFrameIndex;
324 2
    UIImage *fetchFrame;
325 2
    SD_LOCK(self.lock);
326 2
    fetchFrame = self.bufferMiss? nil : self.frameBuffer[@(nextFrameIndex)];
327 2
    SD_UNLOCK(self.lock);
328
    
329 2
    if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) {
330 2
        // Prefetch next frame in background queue
331 2
        id<SDAnimatedImageProvider> animatedProvider = self.animatedProvider;
332 2
        @weakify(self);
333 2
        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
334 2
            @strongify(self);
335 2
            if (!self) {
336 0
                return;
337
            }
338 2
            UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex];
339

340 2
            BOOL isAnimating = self.displayLink.isRunning;
341 2
            if (isAnimating) {
342 2
                SD_LOCK(self.lock);
343 2
                self.frameBuffer[@(fetchFrameIndex)] = frame;
344 2
                SD_UNLOCK(self.lock);
345
            }
346 2
        }];
347 2
        [self.fetchQueue addOperation:operation];
348
    }
349
}
350

351 2
- (void)handleFrameChange {
352 2
    if (self.animationFrameHandler) {
353 2
        self.animationFrameHandler(self.currentFrameIndex, self.currentFrame);
354
    }
355
}
356

357 2
- (void)handleLoopChange {
358 2
    if (self.animationLoopHandler) {
359 2
        self.animationLoopHandler(self.currentLoopCount);
360
    }
361
}
362

363
#pragma mark - Util
364 2
- (void)calculateMaxBufferCount {
365 2
    NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage);
366 2
    if (bytes == 0) bytes = 1024;
367
    
368 2
    NSUInteger max = 0;
369 2
    if (self.maxBufferSize > 0) {
370 0
        max = self.maxBufferSize;
371 2
    } else {
372 2
        // Calculate based on current memory, these factors are by experience
373 2
        NSUInteger total = [SDDeviceHelper totalMemory];
374 2
        NSUInteger free = [SDDeviceHelper freeMemory];
375 2
        max = MIN(total * 0.2, free * 0.6);
376
    }
377
    
378 2
    NSUInteger maxBufferCount = (double)max / (double)bytes;
379 2
    if (!maxBufferCount) {
380 0
        // At least 1 frame
381 0
        maxBufferCount = 1;
382
    }
383
    
384 2
    self.maxBufferCount = maxBufferCount;
385
}
386

387 0
+ (NSString *)defaultRunLoopMode {
388 0
    // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
389 0
    return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
390
}
391

392
@end

Read our documentation on viewing source code .

Loading