SDAnimatedImagePlayer.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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. #import "SDAnimatedImagePlayer.h"
  9. #import "NSImage+Compatibility.h"
  10. #import "SDDisplayLink.h"
  11. #import "SDDeviceHelper.h"
  12. #import "SDImageFramePool.h"
  13. #import "SDInternalMacros.h"
  14. @interface SDAnimatedImagePlayer () {
  15. NSRunLoopMode _runLoopMode;
  16. }
  17. @property (nonatomic, strong) SDImageFramePool *framePool;
  18. @property (nonatomic, strong, readwrite) UIImage *currentFrame;
  19. @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
  20. @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
  21. @property (nonatomic, strong) id<SDAnimatedImageProvider> animatedProvider;
  22. @property (nonatomic, assign) NSUInteger currentFrameBytes;
  23. @property (nonatomic, assign) NSTimeInterval currentTime;
  24. @property (nonatomic, assign) BOOL bufferMiss;
  25. @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
  26. @property (nonatomic, assign) BOOL shouldReverse;
  27. @property (nonatomic, strong) SDDisplayLink *displayLink;
  28. @end
  29. @implementation SDAnimatedImagePlayer
  30. - (instancetype)initWithProvider:(id<SDAnimatedImageProvider>)provider {
  31. self = [super init];
  32. if (self) {
  33. NSUInteger animatedImageFrameCount = provider.animatedImageFrameCount;
  34. // Check the frame count
  35. if (animatedImageFrameCount <= 1) {
  36. return nil;
  37. }
  38. self.totalFrameCount = animatedImageFrameCount;
  39. // Get the current frame and loop count.
  40. self.totalLoopCount = provider.animatedImageLoopCount;
  41. self.animatedProvider = provider;
  42. self.playbackRate = 1.0;
  43. self.framePool = [SDImageFramePool registerProvider:provider];
  44. }
  45. return self;
  46. }
  47. + (instancetype)playerWithProvider:(id<SDAnimatedImageProvider>)provider {
  48. SDAnimatedImagePlayer *player = [[SDAnimatedImagePlayer alloc] initWithProvider:provider];
  49. return player;
  50. }
  51. - (void)dealloc {
  52. // Dereference the frame pool, when zero the frame pool for provider will dealloc
  53. [SDImageFramePool unregisterProvider:self.animatedProvider];
  54. }
  55. #pragma mark - Private
  56. - (SDDisplayLink *)displayLink {
  57. if (!_displayLink) {
  58. _displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)];
  59. [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
  60. [_displayLink stop];
  61. }
  62. return _displayLink;
  63. }
  64. - (void)setRunLoopMode:(NSRunLoopMode)runLoopMode {
  65. if ([_runLoopMode isEqual:runLoopMode]) {
  66. return;
  67. }
  68. if (_displayLink) {
  69. if (_runLoopMode) {
  70. [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runLoopMode];
  71. }
  72. if (runLoopMode.length > 0) {
  73. [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode];
  74. }
  75. }
  76. _runLoopMode = [runLoopMode copy];
  77. }
  78. - (NSRunLoopMode)runLoopMode {
  79. if (!_runLoopMode) {
  80. _runLoopMode = [[self class] defaultRunLoopMode];
  81. }
  82. return _runLoopMode;
  83. }
  84. #pragma mark - State Control
  85. - (void)setupCurrentFrame {
  86. if (self.currentFrameIndex != 0) {
  87. return;
  88. }
  89. if (self.playbackMode == SDAnimatedImagePlaybackModeReverse ||
  90. self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
  91. self.currentFrameIndex = self.totalFrameCount - 1;
  92. }
  93. if (!self.currentFrame && [self.animatedProvider isKindOfClass:[UIImage class]]) {
  94. UIImage *image = (UIImage *)self.animatedProvider;
  95. // Cache the poster image if available, but should not callback to avoid caller thread issues
  96. #if SD_MAC
  97. UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
  98. #else
  99. UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
  100. #endif
  101. if (posterFrame) {
  102. // Calculate max buffer size
  103. [self calculateMaxBufferCountWithFrame:posterFrame];
  104. // HACK: The first frame should not check duration and immediately display
  105. self.needsDisplayWhenImageBecomesAvailable = YES;
  106. [self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex];
  107. }
  108. }
  109. }
  110. - (void)resetCurrentFrameStatus {
  111. // These should not trigger KVO, user don't need to receive an `index == 0, image == nil` callback.
  112. _currentFrame = nil;
  113. _currentFrameIndex = 0;
  114. _currentLoopCount = 0;
  115. _currentTime = 0;
  116. _bufferMiss = NO;
  117. _needsDisplayWhenImageBecomesAvailable = NO;
  118. }
  119. - (void)clearFrameBuffer {
  120. [self.framePool removeAllFrames];
  121. }
  122. #pragma mark - Animation Control
  123. - (void)startPlaying {
  124. [self.displayLink start];
  125. // Setup frame
  126. [self setupCurrentFrame];
  127. }
  128. - (void)stopPlaying {
  129. // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method.
  130. [_displayLink stop];
  131. // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct.
  132. [self resetCurrentFrameStatus];
  133. }
  134. - (void)pausePlaying {
  135. [_displayLink stop];
  136. }
  137. - (BOOL)isPlaying {
  138. return _displayLink.isRunning;
  139. }
  140. - (void)seekToFrameAtIndex:(NSUInteger)index loopCount:(NSUInteger)loopCount {
  141. if (index >= self.totalFrameCount) {
  142. return;
  143. }
  144. self.currentFrameIndex = index;
  145. self.currentLoopCount = loopCount;
  146. self.currentFrame = [self.animatedProvider animatedImageFrameAtIndex:index];
  147. [self handleFrameChange];
  148. }
  149. #pragma mark - Core Render
  150. - (void)displayDidRefresh:(SDDisplayLink *)displayLink {
  151. // If for some reason a wild call makes it through when we shouldn't be animating, bail.
  152. // Early return!
  153. if (!self.isPlaying) {
  154. return;
  155. }
  156. NSUInteger totalFrameCount = self.totalFrameCount;
  157. if (totalFrameCount <= 1) {
  158. // Total frame count less than 1, wrong configuration and stop animating
  159. [self stopPlaying];
  160. return;
  161. }
  162. NSTimeInterval playbackRate = self.playbackRate;
  163. if (playbackRate <= 0) {
  164. // Does not support <= 0 play rate
  165. [self stopPlaying];
  166. return;
  167. }
  168. // Calculate refresh duration
  169. NSTimeInterval duration = self.displayLink.duration;
  170. NSUInteger currentFrameIndex = self.currentFrameIndex;
  171. NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
  172. if (self.playbackMode == SDAnimatedImagePlaybackModeReverse) {
  173. nextFrameIndex = currentFrameIndex == 0 ? (totalFrameCount - 1) : (currentFrameIndex - 1) % totalFrameCount;
  174. } else if (self.playbackMode == SDAnimatedImagePlaybackModeBounce ||
  175. self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
  176. if (currentFrameIndex == 0) {
  177. self.shouldReverse = NO;
  178. } else if (currentFrameIndex == totalFrameCount - 1) {
  179. self.shouldReverse = YES;
  180. }
  181. nextFrameIndex = self.shouldReverse ? (currentFrameIndex - 1) : (currentFrameIndex + 1);
  182. nextFrameIndex %= totalFrameCount;
  183. }
  184. // Check if we need to display new frame firstly
  185. if (self.needsDisplayWhenImageBecomesAvailable) {
  186. UIImage *currentFrame = [self.framePool frameAtIndex:currentFrameIndex];
  187. // Update the current frame
  188. if (currentFrame) {
  189. // Update the current frame immediately
  190. self.currentFrame = currentFrame;
  191. [self handleFrameChange];
  192. self.bufferMiss = NO;
  193. self.needsDisplayWhenImageBecomesAvailable = NO;
  194. }
  195. else {
  196. self.bufferMiss = YES;
  197. }
  198. }
  199. // Check if we have the frame buffer
  200. if (!self.bufferMiss) {
  201. // Then check if timestamp is reached
  202. self.currentTime += duration;
  203. NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex];
  204. currentDuration = currentDuration / playbackRate;
  205. if (self.currentTime < currentDuration) {
  206. // Current frame timestamp not reached, prefetch frame in advance.
  207. [self prefetchFrameAtIndex:currentFrameIndex
  208. nextIndex:nextFrameIndex];
  209. return;
  210. }
  211. // Otherwise, we should be ready to display next frame
  212. self.needsDisplayWhenImageBecomesAvailable = YES;
  213. self.currentFrameIndex = nextFrameIndex;
  214. self.currentTime -= currentDuration;
  215. NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex];
  216. nextDuration = nextDuration / playbackRate;
  217. if (self.currentTime > nextDuration) {
  218. // Do not skip frame
  219. self.currentTime = nextDuration;
  220. }
  221. // Update the loop count when last frame rendered
  222. if (nextFrameIndex == 0) {
  223. // Update the loop count
  224. self.currentLoopCount++;
  225. [self handleLoopChange];
  226. // if reached the max loop count, stop animating, 0 means loop indefinitely
  227. NSUInteger maxLoopCount = self.totalLoopCount;
  228. if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) {
  229. [self stopPlaying];
  230. return;
  231. }
  232. }
  233. }
  234. // Since we support handler, check animating state again
  235. if (!self.isPlaying) {
  236. return;
  237. }
  238. [self prefetchFrameAtIndex:currentFrameIndex
  239. nextIndex:nextFrameIndex];
  240. }
  241. // Check if we should prefetch next frame or current frame
  242. // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
  243. // Or, most cases, the decode speed is faster than render speed, we fetch next frame
  244. - (void)prefetchFrameAtIndex:(NSUInteger)currentIndex
  245. nextIndex:(NSUInteger)nextIndex {
  246. NSUInteger fetchFrameIndex = currentIndex;
  247. UIImage *fetchFrame = nil;
  248. if (!self.bufferMiss) {
  249. fetchFrameIndex = nextIndex;
  250. fetchFrame = [self.framePool frameAtIndex:nextIndex];
  251. }
  252. BOOL bufferFull = NO;
  253. if (self.framePool.currentFrameCount == self.totalFrameCount) {
  254. bufferFull = YES;
  255. }
  256. if (!fetchFrame && !bufferFull) {
  257. // Calculate max buffer size
  258. [self calculateMaxBufferCountWithFrame:self.currentFrame];
  259. // Prefetch next frame
  260. [self.framePool prefetchFrameAtIndex:fetchFrameIndex];
  261. }
  262. }
  263. - (void)handleFrameChange {
  264. if (self.animationFrameHandler) {
  265. self.animationFrameHandler(self.currentFrameIndex, self.currentFrame);
  266. }
  267. }
  268. - (void)handleLoopChange {
  269. if (self.animationLoopHandler) {
  270. self.animationLoopHandler(self.currentLoopCount);
  271. }
  272. }
  273. #pragma mark - Util
  274. - (void)calculateMaxBufferCountWithFrame:(nonnull UIImage *)frame {
  275. NSUInteger bytes = self.currentFrameBytes;
  276. if (bytes == 0) {
  277. bytes = CGImageGetBytesPerRow(frame.CGImage) * CGImageGetHeight(frame.CGImage);
  278. if (bytes == 0) {
  279. bytes = 1024;
  280. } else {
  281. // Cache since most animated image each frame bytes is the same
  282. self.currentFrameBytes = bytes;
  283. }
  284. }
  285. NSUInteger max = 0;
  286. if (self.maxBufferSize > 0) {
  287. max = self.maxBufferSize;
  288. } else {
  289. // Calculate based on current memory, these factors are by experience
  290. NSUInteger total = [SDDeviceHelper totalMemory];
  291. NSUInteger free = [SDDeviceHelper freeMemory];
  292. max = MIN(total * 0.2, free * 0.6);
  293. }
  294. NSUInteger maxBufferCount = (double)max / (double)bytes;
  295. if (!maxBufferCount) {
  296. // At least 1 frame
  297. maxBufferCount = 1;
  298. }
  299. self.framePool.maxBufferCount = maxBufferCount;
  300. }
  301. + (NSString *)defaultRunLoopMode {
  302. // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
  303. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
  304. }
  305. @end