Bug 717872 - Move all image animation logic into a new class, FrameAnimator, and use it from RasterImage. r=seth
authorJoe Drew <joe@drew.ca>
Mon, 15 Jul 2013 14:38:59 -0400
changeset 146117 ea8d855c4edb57c2c45cdda9d67168701e804363
parent 146116 bf847b1a3776f8f057a6815ae8639216805cebd8
child 146118 638ccf9872d8aca1569fe56d5565906990b184f2
push idunknown
push userunknown
push dateunknown
reviewersseth
bugs717872
milestone25.0a1
Bug 717872 - Move all image animation logic into a new class, FrameAnimator, and use it from RasterImage. r=seth This patch moves the logic of moving from one frame to another (and tracking what frame is current, etc) to a separate class, FrameAnimator. Deciding *whether* to animate, and actually calling that animation code, is left to RasterImage, but the animation itself is driven by FrameAnimator.
image/src/FrameAnimator.cpp
image/src/FrameAnimator.h
image/src/RasterImage.cpp
image/src/RasterImage.h
image/src/imgFrame.cpp
image/src/imgFrame.h
image/src/moz.build
new file mode 100644
--- /dev/null
+++ b/image/src/FrameAnimator.cpp
@@ -0,0 +1,264 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FrameAnimator.h"
+
+#include "imgIContainer.h"
+
+using namespace mozilla::image;
+using namespace mozilla;
+
+FrameAnimator::FrameAnimator(FrameBlender& aFrameBlender)
+  : mCurrentAnimationFrameIndex(0)
+  , mLoopCount(-1)
+  , mFrameBlender(aFrameBlender)
+  , mAnimationMode(imgIContainer::kNormalAnimMode)
+  , mDoneDecoding(false)
+{
+}
+
+uint32_t
+FrameAnimator::GetSingleLoopTime() const
+{
+  // If we aren't done decoding, we don't know the image's full play time.
+  if (!mDoneDecoding) {
+    return 0;
+  }
+
+  // If we're not looping, a single loop time has no meaning
+  if (mAnimationMode != imgIContainer::kNormalAnimMode) {
+    return 0;
+  }
+
+  uint32_t looptime = 0;
+  for (uint32_t i = 0; i < mFrameBlender.GetNumFrames(); ++i) {
+    int32_t timeout = mFrameBlender.RawGetFrame(i)->GetTimeout();
+    if (timeout > 0) {
+      looptime += static_cast<uint32_t>(timeout);
+    } else {
+      // If we have a frame that never times out, we're probably in an error
+      // case, but let's handle it more gracefully.
+      NS_WARNING("Negative frame timeout - how did this happen?");
+      return 0;
+    }
+  }
+
+  return looptime;
+}
+
+TimeStamp
+FrameAnimator::GetCurrentImgFrameEndTime() const
+{
+  imgFrame* currentFrame = mFrameBlender.RawGetFrame(mCurrentAnimationFrameIndex);
+  TimeStamp currentFrameTime = mCurrentAnimationFrameTime;
+  int64_t timeout = currentFrame->GetTimeout();
+
+  if (timeout < 0) {
+    // We need to return a sentinel value in this case, because our logic
+    // doesn't work correctly if we have a negative timeout value. The reason
+    // this positive infinity was chosen was because it works with the loop in
+    // RequestRefresh() below.
+    return TimeStamp() +
+           TimeDuration::FromMilliseconds(static_cast<double>(UINT64_MAX));
+  }
+
+  TimeDuration durationOfTimeout =
+    TimeDuration::FromMilliseconds(static_cast<double>(timeout));
+  TimeStamp currentFrameEndTime = currentFrameTime + durationOfTimeout;
+
+  return currentFrameEndTime;
+}
+
+FrameAnimator::RefreshResult
+FrameAnimator::AdvanceFrame(TimeStamp aTime)
+{
+  NS_ASSERTION(aTime <= TimeStamp::Now(),
+               "Given time appears to be in the future");
+
+  uint32_t currentFrameIndex = mCurrentAnimationFrameIndex;
+  uint32_t nextFrameIndex = currentFrameIndex + 1;
+  uint32_t timeout = 0;
+
+  RefreshResult ret;
+
+  // If we're done decoding, we know we've got everything we're going to get.
+  // If we aren't, we only display fully-downloaded frames; everything else
+  // gets delayed.
+  bool needToWait = !mDoneDecoding &&
+                    mFrameBlender.RawGetFrame(nextFrameIndex) &&
+                    !mFrameBlender.RawGetFrame(nextFrameIndex)->ImageComplete();
+
+  if (needToWait) {
+    // Uh oh, the frame we want to show is currently being decoded (partial)
+    // Wait until the next refresh driver tick and try again
+    return ret;
+  } else {
+    // If we're done decoding the next frame, go ahead and display it now and
+    // reinit with the next frame's delay time.
+    if (mFrameBlender.GetNumFrames() == nextFrameIndex) {
+      // End of Animation, unless we are looping forever
+
+      // If animation mode is "loop once", it's time to stop animating
+      if (mAnimationMode == imgIContainer::kLoopOnceAnimMode || mLoopCount == 0) {
+        ret.animationFinished = true;
+      }
+
+      nextFrameIndex = 0;
+
+      if (mLoopCount > 0) {
+        mLoopCount--;
+      }
+
+      // If we're done, exit early.
+      if (ret.animationFinished) {
+        return ret;
+      }
+    }
+
+    timeout = mFrameBlender.GetFrame(nextFrameIndex)->GetTimeout();
+  }
+
+  // Bad data
+  if (!(timeout > 0)) {
+    ret.animationFinished = true;
+    ret.error = true;
+  }
+
+  if (nextFrameIndex == 0) {
+    ret.dirtyRect = mFirstFrameRefreshArea;
+  } else {
+    // Change frame
+    if (!mFrameBlender.DoBlend(&ret.dirtyRect, currentFrameIndex, nextFrameIndex)) {
+      // something went wrong, move on to next
+      NS_WARNING("FrameAnimator::AdvanceFrame(): Compositing of frame failed");
+      mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(true);
+      mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
+      mCurrentAnimationFrameIndex = nextFrameIndex;
+
+      ret.error = true;
+      return ret;
+    }
+
+    mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(false);
+  }
+
+  mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
+
+  // If we can get closer to the current time by a multiple of the image's loop
+  // time, we should.
+  uint32_t loopTime = GetSingleLoopTime();
+  if (loopTime > 0) {
+    TimeDuration delay = aTime - mCurrentAnimationFrameTime;
+    if (delay.ToMilliseconds() > loopTime) {
+      // Explicitly use integer division to get the floor of the number of
+      // loops.
+      uint32_t loops = static_cast<uint32_t>(delay.ToMilliseconds()) / loopTime;
+      mCurrentAnimationFrameTime += TimeDuration::FromMilliseconds(loops * loopTime);
+    }
+  }
+
+  // Set currentAnimationFrameIndex at the last possible moment
+  mCurrentAnimationFrameIndex = nextFrameIndex;
+
+  // If we're here, we successfully advanced the frame.
+  ret.frameAdvanced = true;
+
+  return ret;
+}
+
+FrameAnimator::RefreshResult
+FrameAnimator::RequestRefresh(const mozilla::TimeStamp& aTime)
+{
+  // only advance the frame if the current time is greater than or
+  // equal to the current frame's end time.
+  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime();
+
+  // By default, an empty RefreshResult.
+  RefreshResult ret;
+
+  while (currentFrameEndTime <= aTime) {
+    TimeStamp oldFrameEndTime = currentFrameEndTime;
+
+    RefreshResult frameRes = AdvanceFrame(aTime);
+
+    // Accumulate our result for returning to callers.
+    ret.Accumulate(frameRes);
+
+    currentFrameEndTime = GetCurrentImgFrameEndTime();
+
+    // if we didn't advance a frame, and our frame end time didn't change,
+    // then we need to break out of this loop & wait for the frame(s)
+    // to finish downloading
+    if (!frameRes.frameAdvanced && (currentFrameEndTime == oldFrameEndTime)) {
+      break;
+    }
+  }
+
+  return ret;
+}
+
+void
+FrameAnimator::ResetAnimation()
+{
+  mCurrentAnimationFrameIndex = 0;
+}
+
+void
+FrameAnimator::SetDoneDecoding(bool aDone)
+{
+  mDoneDecoding = aDone;
+}
+
+void
+FrameAnimator::SetAnimationMode(uint16_t aAnimationMode)
+{
+  mAnimationMode = aAnimationMode;
+}
+
+void
+FrameAnimator::InitAnimationFrameTimeIfNecessary()
+{
+  if (mCurrentAnimationFrameTime.IsNull()) {
+    mCurrentAnimationFrameTime = TimeStamp::Now();
+  }
+}
+
+void
+FrameAnimator::SetAnimationFrameTime(const TimeStamp& aTime)
+{
+  mCurrentAnimationFrameTime = aTime;
+}
+
+void
+FrameAnimator::SetFirstFrameRefreshArea(const nsIntRect& aRect)
+{
+  mFirstFrameRefreshArea = aRect;
+}
+
+void
+FrameAnimator::UnionFirstFrameRefreshArea(const nsIntRect& aRect)
+{
+  mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, aRect);
+}
+
+void
+FrameAnimator::SetLoopCount(int loopcount)
+{
+  mLoopCount = loopcount;
+}
+
+uint32_t
+FrameAnimator::GetCurrentAnimationFrameIndex() const
+{
+  return mCurrentAnimationFrameIndex;
+}
+
+nsIntRect
+FrameAnimator::GetFirstFrameRefreshArea() const
+{
+  return mFirstFrameRefreshArea;
+}
+
+
new file mode 100644
--- /dev/null
+++ b/image/src/FrameAnimator.h
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_imagelib_FrameAnimator_h_
+#define mozilla_imagelib_FrameAnimator_h_
+
+#include "mozilla/TimeStamp.h"
+#include "FrameBlender.h"
+#include "nsRect.h"
+
+namespace mozilla {
+namespace image {
+
+class FrameAnimator
+{
+public:
+  FrameAnimator(FrameBlender& aBlender);
+
+  /**
+   * Return value from RequestRefresh. Tells callers what happened in that call
+   * to RequestRefresh.
+   */
+  struct RefreshResult
+  {
+    // The dirty rectangle to be re-drawn after this RequestRefresh().
+    nsIntRect dirtyRect;
+
+    // Whether any frame changed, and hence the dirty rect was set.
+    bool frameAdvanced : 1;
+
+    // Whether the animation has finished playing.
+    bool animationFinished : 1;
+
+    // Whether an error has occurred when trying to advance a frame. Note that
+    // errors do not, on their own, end the animation.
+    bool error : 1;
+
+    RefreshResult()
+      : frameAdvanced(false)
+      , animationFinished(false)
+      , error(false)
+    {}
+
+    void Accumulate(const RefreshResult& other)
+    {
+      frameAdvanced = frameAdvanced || other.frameAdvanced;
+      animationFinished = animationFinished || other.animationFinished;
+      error = error || other.error;
+      dirtyRect = dirtyRect.Union(other.dirtyRect);
+    }
+  };
+
+  /**
+   * Re-evaluate what frame we're supposed to be on, and do whatever blending
+   * is necessary to get us to that frame.
+   *
+   * Returns the result of that blending, including whether the current frame
+   * changed and what the resulting dirty rectangle is.
+   */
+  RefreshResult RequestRefresh(const mozilla::TimeStamp& aTime);
+
+  /**
+   * Call when this image is finished decoding so we know that there aren't any
+   * more frames coming.
+   */
+  void SetDoneDecoding(bool aDone);
+
+  /**
+   * Call when you need to re-start animating. Ensures we start from the first
+   * frame.
+   */
+  void ResetAnimation();
+
+  /**
+   * Number of times to loop the image.
+   * @note -1 means forever.
+   */
+  void SetLoopCount(int32_t aLoopCount);
+
+  /**
+   * The animation mode of the image.
+   *
+   * Constants defined in imgIContainer.idl.
+   */
+  void SetAnimationMode(uint16_t aAnimationMode);
+
+  /**
+   * Set the area to refresh when we loop around to the first frame.
+   */
+  void SetFirstFrameRefreshArea(const nsIntRect& aRect);
+
+  /**
+   * Union the area to refresh when we loop around to the first frame with this
+   * rect.
+   */
+  void UnionFirstFrameRefreshArea(const nsIntRect& aRect);
+
+  /**
+   * If the animation frame time has not yet been set, set it to
+   * TimeStamp::Now().
+   */
+  void InitAnimationFrameTimeIfNecessary();
+
+  /**
+   * Set the animation frame time to @aTime.
+   */
+  void SetAnimationFrameTime(const TimeStamp& aTime);
+
+  /**
+   * The current frame we're on, from 0 to (numFrames - 1).
+   */
+  uint32_t GetCurrentAnimationFrameIndex() const;
+
+  /**
+   * Get the area we refresh when we loop around to the first frame.
+   */
+  nsIntRect GetFirstFrameRefreshArea() const;
+
+private: // methods
+  /**
+   * Gets the length of a single loop of this image, in milliseconds.
+   *
+   * If this image is not finished decoding, is not animated, or it is animated
+   * but does not loop, returns 0.
+   */
+  uint32_t GetSingleLoopTime() const;
+
+  /**
+   * Advances the animation. Typically, this will advance a single frame, but it
+   * may advance multiple frames. This may happen if we have infrequently
+   * "ticking" refresh drivers (e.g. in background tabs), or extremely short-
+   * lived animation frames.
+   *
+   * @param aTime the time that the animation should advance to. This will
+   *              typically be <= TimeStamp::Now().
+   *
+   * @returns a RefreshResult that shows whether the frame was successfully
+   *          advanced, and its resulting dirty rect.
+   */
+  RefreshResult AdvanceFrame(mozilla::TimeStamp aTime);
+
+  /**
+   * Get the time the frame we're currently displaying is supposed to end.
+   *
+   * In the error case, returns an "infinity" timestamp.
+   */
+  mozilla::TimeStamp GetCurrentImgFrameEndTime() const;
+
+private: // data
+  //! Area of the first frame that needs to be redrawn on subsequent loops.
+  nsIntRect mFirstFrameRefreshArea;
+
+  //! the time that the animation advanced to the current frame
+  TimeStamp mCurrentAnimationFrameTime;
+
+  //! The current frame index we're on. 0 to (numFrames - 1).
+  uint32_t mCurrentAnimationFrameIndex;
+
+  //! number of loops remaining before animation stops (-1 no stop)
+  int32_t mLoopCount;
+
+  //! All the frames of the image, shared with our owner
+  FrameBlender& mFrameBlender;
+
+  //! The animation mode of this image. Constants defined in imgIContainer.
+  uint16_t mAnimationMode;
+
+  //! Whether this image is done being decoded.
+  bool mDoneDecoding;
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif /* mozilla_imagelib_FrameAnimator_h_ */
--- a/image/src/RasterImage.cpp
+++ b/image/src/RasterImage.cpp
@@ -19,16 +19,17 @@
 #include "prsystem.h"
 #include "ImageContainer.h"
 #include "Layers.h"
 #include "nsPresContext.h"
 #include "nsThread.h"
 #include "nsIThreadPool.h"
 #include "nsXPCOMCIDInternal.h"
 #include "nsIObserverService.h"
+#include "FrameAnimator.h"
 
 #include "nsPNGDecoder.h"
 #include "nsGIFDecoder2.h"
 #include "nsJPEGDecoder.h"
 #include "nsBMPDecoder.h"
 #include "nsICODecoder.h"
 #include "nsIconDecoder.h"
 
@@ -385,17 +386,16 @@ NS_IMPL_THREADSAFE_ISUPPORTS3(RasterImag
 //******************************************************************************
 RasterImage::RasterImage(imgStatusTracker* aStatusTracker,
                          nsIURI* aURI /* = nullptr */) :
   ImageResource(aStatusTracker, aURI), // invoke superclass's constructor
   mSize(0,0),
   mFrameDecodeFlags(DECODE_FLAGS_DEFAULT),
   mMultipartDecodedFrame(nullptr),
   mAnim(nullptr),
-  mLoopCount(-1),
   mLockCount(0),
   mDecodeCount(0),
 #ifdef DEBUG
   mFramesNotified(0),
 #endif
   mDecodingMutex("RasterImage"),
   mDecoder(nullptr),
   mBytesDecoded(0),
@@ -523,197 +523,51 @@ RasterImage::Init(const char* aMimeType,
   }
 
   // Mark us as initialized
   mInitialized = true;
 
   return NS_OK;
 }
 
-uint32_t
-RasterImage::GetSingleLoopTime() const
-{
-  if (!mAnim) {
-    return 0;
-  }
-
-  // If we aren't done decoding, we don't know the image's full play time.
-  if (!mHasBeenDecoded) {
-    return 0;
-  }
-
-  // If we're not looping, a single loop time has no meaning
-  if (mLoopCount == 0) {
-    return 0;
-  }
-
-  uint32_t looptime = 0;
-  for (uint32_t i = 0; i < GetNumFrames(); ++i) {
-    int32_t timeout = mFrameBlender.RawGetFrame(i)->GetTimeout();
-    if (timeout > 0) {
-      looptime += static_cast<uint32_t>(timeout);
-    } else {
-      // If we have a frame that never times out, we're probably in an error
-      // case, but let's handle it more gracefully.
-      NS_WARNING("Negative frame timeout - how did this happen?");
-      return 0;
-    }
-  }
-
-  return looptime;
-}
-
-bool
-RasterImage::AdvanceFrame(TimeStamp aTime, nsIntRect* aDirtyRect)
-{
-  NS_ASSERTION(aTime <= TimeStamp::Now(),
-               "Given time appears to be in the future");
-
-  uint32_t currentFrameIndex = mAnim->currentAnimationFrameIndex;
-  uint32_t nextFrameIndex = mAnim->currentAnimationFrameIndex + 1;
-  uint32_t timeout = 0;
-
-  // Figure out if we have the next full frame. This is more complicated than
-  // just checking GetNumFrames() because decoders append their frames
-  // before they're filled in.
-  NS_ABORT_IF_FALSE(mDecoder || nextFrameIndex <= GetNumFrames(),
-                    "How did we get 2 indices too far by incrementing?");
-
-  // If we don't have a decoder, we know we've got everything we're going to
-  // get. If we do, we only display fully-downloaded frames; everything else
-  // gets delayed.
-  bool haveFullNextFrame = (mMultipart && mBytesDecoded == 0) || !mDecoder ||
-                            nextFrameIndex < mDecoder->GetCompleteFrameCount();
-
-  // If we're done decoding the next frame, go ahead and display it now and
-  // reinit with the next frame's delay time.
-  if (haveFullNextFrame) {
-    if (GetNumFrames() == nextFrameIndex) {
-      // End of Animation, unless we are looping forever
-
-      // If animation mode is "loop once", it's time to stop animating
-      if (mAnimationMode == kLoopOnceAnimMode || mLoopCount == 0) {
-        mAnimationFinished = true;
-        EvaluateAnimation();
-      }
-
-      nextFrameIndex = 0;
-
-      if (mLoopCount > 0) {
-        mLoopCount--;
-      }
-
-      if (!mAnimating) {
-        // break out early if we are actually done animating
-        return false;
-      }
-    }
-
-    timeout = mFrameBlender.GetFrame(nextFrameIndex)->GetTimeout();
-
-  } else {
-    // Uh oh, the frame we want to show is currently being decoded (partial)
-    // Wait until the next refresh driver tick and try again
-    return false;
-  }
-
-  if (!(timeout > 0)) {
-    mAnimationFinished = true;
-    EvaluateAnimation();
-  }
-
-  if (nextFrameIndex == 0) {
-    *aDirtyRect = mAnim->firstFrameRefreshArea;
-  } else {
-    // Change frame
-    if (!mFrameBlender.DoBlend(aDirtyRect, currentFrameIndex, nextFrameIndex)) {
-      // something went wrong, move on to next
-      NS_WARNING("RasterImage::AdvanceFrame(): Compositing of frame failed");
-      mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(true);
-      mAnim->currentAnimationFrameTime = GetCurrentImgFrameEndTime();
-      mAnim->currentAnimationFrameIndex = nextFrameIndex;
-      return false;
-    }
-
-    mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(false);
-  }
-
-  mAnim->currentAnimationFrameTime = GetCurrentImgFrameEndTime();
-
-  // If we can get closer to the current time by a multiple of the image's loop
-  // time, we should.
-  uint32_t loopTime = GetSingleLoopTime();
-  if (loopTime > 0) {
-    TimeDuration delay = aTime - mAnim->currentAnimationFrameTime;
-    if (delay.ToMilliseconds() > loopTime) {
-      // Explicitly use integer division to get the floor of the number of
-      // loops.
-      uint32_t loops = static_cast<uint32_t>(delay.ToMilliseconds()) / loopTime;
-      mAnim->currentAnimationFrameTime += TimeDuration::FromMilliseconds(loops * loopTime);
-    }
-  }
-
-  // Set currentAnimationFrameIndex at the last possible moment
-  mAnim->currentAnimationFrameIndex = nextFrameIndex;
-
-  return true;
-}
-
 //******************************************************************************
 // [notxpcom] void requestRefresh ([const] in TimeStamp aTime);
 NS_IMETHODIMP_(void)
 RasterImage::RequestRefresh(const mozilla::TimeStamp& aTime)
 {
   if (!ShouldAnimate()) {
     return;
   }
 
   EvaluateAnimation();
 
-  // only advance the frame if the current time is greater than or
-  // equal to the current frame's end time.
-  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime();
-  bool frameAdvanced = false;
-
-  // The dirtyRect variable will contain an accumulation of the sub-rectangles
-  // that are dirty for each frame we advance in AdvanceFrame().
-  nsIntRect dirtyRect;
-
-  while (currentFrameEndTime <= aTime) {
-    TimeStamp oldFrameEndTime = currentFrameEndTime;
-    nsIntRect frameDirtyRect;
-    bool didAdvance = AdvanceFrame(aTime, &frameDirtyRect);
-    frameAdvanced = frameAdvanced || didAdvance;
-    currentFrameEndTime = GetCurrentImgFrameEndTime();
-
-    // Accumulate the dirty area.
-    dirtyRect = dirtyRect.Union(frameDirtyRect);
-
-    // if we didn't advance a frame, and our frame end time didn't change,
-    // then we need to break out of this loop & wait for the frame(s)
-    // to finish downloading
-    if (!didAdvance && (currentFrameEndTime == oldFrameEndTime)) {
-      break;
-    }
+  FrameAnimator::RefreshResult res;
+  if (mAnim) {
+    res = mAnim->RequestRefresh(aTime);
   }
 
-  if (frameAdvanced) {
+  if (res.frameAdvanced) {
     // Notify listeners that our frame has actually changed, but do this only
     // once for all frames that we've now passed (if AdvanceFrame() was called
     // more than once).
     #ifdef DEBUG
       mFramesNotified++;
     #endif
 
     UpdateImageContainer();
 
     // Explicitly call this on mStatusTracker so we're sure to not interfere
     // with the decoding process
     if (mStatusTracker)
-      mStatusTracker->FrameChanged(&dirtyRect);
+      mStatusTracker->FrameChanged(&res.dirtyRect);
+  }
+
+  if (res.animationFinished) {
+    mAnimationFinished = true;
+    EvaluateAnimation();
   }
 }
 
 //******************************************************************************
 /* readonly attribute int32_t width; */
 NS_IMETHODIMP
 RasterImage::GetWidth(int32_t *aWidth)
 {
@@ -828,44 +682,21 @@ RasterImage::GetDrawableImgFrame(uint32_
     return nullptr;
   return frame;
 }
 
 uint32_t
 RasterImage::GetCurrentImgFrameIndex() const
 {
   if (mAnim)
-    return mAnim->currentAnimationFrameIndex;
+    return mAnim->GetCurrentAnimationFrameIndex();
 
   return 0;
 }
 
-TimeStamp
-RasterImage::GetCurrentImgFrameEndTime() const
-{
-  imgFrame* currentFrame = mFrameBlender.RawGetFrame(mAnim->currentAnimationFrameIndex);
-  TimeStamp currentFrameTime = mAnim->currentAnimationFrameTime;
-  int64_t timeout = currentFrame->GetTimeout();
-
-  if (timeout < 0) {
-    // We need to return a sentinel value in this case, because our logic
-    // doesn't work correctly if we have a negative timeout value. The reason
-    // this positive infinity was chosen was because it works with the loop in
-    // RequestRefresh() above.
-    return TimeStamp() +
-           TimeDuration::FromMilliseconds(static_cast<double>(UINT64_MAX));
-  }
-
-  TimeDuration durationOfTimeout =
-    TimeDuration::FromMilliseconds(static_cast<double>(timeout));
-  TimeStamp currentFrameEndTime = currentFrameTime + durationOfTimeout;
-
-  return currentFrameEndTime;
-}
-
 imgFrame*
 RasterImage::GetCurrentImgFrame()
 {
   return GetImgFrame(GetCurrentImgFrameIndex());
 }
 
 //******************************************************************************
 /* [notxpcom] boolean frameIsOpaque(in uint32_t aWhichFrame); */
@@ -1225,16 +1056,40 @@ RasterImage::NonHeapSizeOfDecoded() cons
 
 size_t
 RasterImage::OutOfProcessSizeOfDecoded() const
 {
   return SizeOfDecodedWithComputedFallbackIfHeap(gfxASurface::MEMORY_OUT_OF_PROCESS,
                                                  NULL);
 }
 
+void
+RasterImage::EnsureAnimExists()
+{
+  if (!mAnim) {
+
+    // Create the animation context
+    mAnim = new FrameAnimator(mFrameBlender);
+
+    // We don't support discarding animated images (See bug 414259).
+    // Lock the image and throw away the key.
+    //
+    // Note that this is inefficient, since we could get rid of the source
+    // data too. However, doing this is actually hard, because we're probably
+    // calling ensureAnimExists mid-decode, and thus we're decoding out of
+    // the source buffer. Since we're going to fix this anyway later, and
+    // since we didn't kill the source data in the old world either, locking
+    // is acceptable for the moment.
+    LockImage();
+
+    // Notify our observers that we are starting animation.
+    CurrentStatusTracker().RecordImageIsAnimated();
+  }
+}
+
 nsresult
 RasterImage::InternalAddFrameHelper(uint32_t framenum, imgFrame *aFrame,
                                     uint8_t **imageData, uint32_t *imageLength,
                                     uint32_t **paletteData, uint32_t *paletteLength,
                                     imgFrame** aRetFrame)
 {
   NS_ABORT_IF_FALSE(framenum <= GetNumFrames(), "Invalid frame index!");
   if (framenum > GetNumFrames())
@@ -1305,25 +1160,23 @@ RasterImage::InternalAddFrame(uint32_t f
     EnsureAnimExists();
 
     // If we dispose of the first frame by clearing it, then the
     // First Frame's refresh area is all of itself.
     // RESTORE_PREVIOUS is invalid (assumed to be DISPOSE_CLEAR)
     int32_t frameDisposalMethod = mFrameBlender.RawGetFrame(0)->GetFrameDisposalMethod();
     if (frameDisposalMethod == FrameBlender::kDisposeClear ||
         frameDisposalMethod == FrameBlender::kDisposeRestorePrevious)
-      mAnim->firstFrameRefreshArea = mFrameBlender.RawGetFrame(0)->GetRect();
+      mAnim->SetFirstFrameRefreshArea(mFrameBlender.RawGetFrame(0)->GetRect());
   }
 
   // Calculate firstFrameRefreshArea
   // Some gifs are huge but only have a small area that they animate
   // We only need to refresh that small area when Frame 0 comes around again
-  nsIntRect frameRect = frame->GetRect();
-  mAnim->firstFrameRefreshArea.UnionRect(mAnim->firstFrameRefreshArea,
-                                         frameRect);
+  mAnim->UnionFirstFrameRefreshArea(frame->GetRect());
 
   rv = InternalAddFrameHelper(framenum, frame.forget(), imageData, imageLength,
                               paletteData, paletteLength, aRetFrame);
 
   return rv;
 }
 
 bool
@@ -1536,19 +1389,32 @@ RasterImage::DecodingComplete()
       // complexity and it's not really needed since we already are smart about
       // not displaying the still-decoding frame of an animated image. We may
       // have already stored an extra frame, though, so we'll release it here.
       delete mMultipartDecodedFrame;
       mMultipartDecodedFrame = nullptr;
     }
   }
 
+  if (mAnim) {
+    mAnim->SetDoneDecoding(true);
+  }
+
   return NS_OK;
 }
 
+NS_IMETHODIMP
+RasterImage::SetAnimationMode(uint16_t aAnimationMode)
+{
+  if (mAnim) {
+    mAnim->SetAnimationMode(aAnimationMode);
+  }
+  return SetAnimationModeInternal(aAnimationMode);
+}
+
 //******************************************************************************
 /* void StartAnimation () */
 nsresult
 RasterImage::StartAnimation()
 {
   if (mError)
     return NS_ERROR_FAILURE;
 
@@ -1560,19 +1426,17 @@ RasterImage::StartAnimation()
   if (currentFrame) {
     if (currentFrame->GetTimeout() < 0) { // -1 means display this frame forever
       mAnimationFinished = true;
       return NS_ERROR_ABORT;
     }
 
     // We need to set the time that this initial frame was first displayed, as
     // this is used in AdvanceFrame().
-    if (mAnim->currentAnimationFrameTime.IsNull()) {
-      mAnim->currentAnimationFrameTime = TimeStamp::Now();
-    }
+    mAnim->InitAnimationFrameTimeIfNecessary();
   }
 
   return NS_OK;
 }
 
 //******************************************************************************
 /* void stopAnimation (); */
 nsresult
@@ -1590,35 +1454,38 @@ RasterImage::StopAnimation()
 /* void resetAnimation (); */
 NS_IMETHODIMP
 RasterImage::ResetAnimation()
 {
   if (mError)
     return NS_ERROR_FAILURE;
 
   if (mAnimationMode == kDontAnimMode ||
-      !mAnim || mAnim->currentAnimationFrameIndex == 0)
+      !mAnim || mAnim->GetCurrentAnimationFrameIndex() == 0)
     return NS_OK;
 
   mAnimationFinished = false;
 
   if (mAnimating)
     StopAnimation();
 
   mFrameBlender.ResetAnimation();
-
-  mAnim->currentAnimationFrameIndex = 0;
+  if (mAnim) {
+    mAnim->ResetAnimation();
+  }
+
   UpdateImageContainer();
 
   // Note - We probably want to kick off a redecode somewhere around here when
   // we fix bug 500402.
 
   // Update display if we were animating before
   if (mAnimating && mStatusTracker) {
-    mStatusTracker->FrameChanged(&(mAnim->firstFrameRefreshArea));
+    nsIntRect rect = mAnim->GetFirstFrameRefreshArea();
+    mStatusTracker->FrameChanged(&rect);
   }
 
   if (ShouldAnimate()) {
     StartAnimation();
     // The animation may not have been running before, if mAnimationFinished
     // was false (before we changed it to true in this function). So, mark the
     // animation as running.
     mAnimating = true;
@@ -1630,39 +1497,37 @@ RasterImage::ResetAnimation()
 //******************************************************************************
 // [notxpcom] void requestRefresh ([const] in TimeStamp aTime);
 NS_IMETHODIMP_(void)
 RasterImage::SetAnimationStartTime(const mozilla::TimeStamp& aTime)
 {
   if (mError || mAnimating || !mAnim)
     return;
 
-  mAnim->currentAnimationFrameTime = aTime;
+  mAnim->SetAnimationFrameTime(aTime);
 }
 
 NS_IMETHODIMP_(float)
 RasterImage::GetFrameIndex(uint32_t aWhichFrame)
 {
   MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument");
   return (aWhichFrame == FRAME_FIRST || !mAnim)
          ? 0.0f
-         : mAnim->currentAnimationFrameIndex;
+         : mAnim->GetCurrentAnimationFrameIndex();
 }
 
 void
 RasterImage::SetLoopCount(int32_t aLoopCount)
 {
   if (mError)
     return;
 
-  // -1  infinite
-  //  0  no looping, one iteration
-  //  1  one loop, two iterations
-  //  ...
-  mLoopCount = aLoopCount;
+  if (mAnim) {
+    mAnim->SetLoopCount(aLoopCount);
+  }
 }
 
 nsresult
 RasterImage::AddSourceData(const char *aBuffer, uint32_t aCount)
 {
   MutexAutoLock lock(mDecodingMutex);
 
   if (mError)
@@ -1924,16 +1789,20 @@ RasterImage::OnNewSourceData()
 
   // Reset some flags
   mDecoded = false;
   mHasSourceData = false;
   mHasSize = false;
   mWantFullDecode = true;
   mDecodeRequest = nullptr;
 
+  if (mAnim) {
+    mAnim->SetDoneDecoding(false);
+  }
+
   // We always need the size first.
   rv = InitDecoder(/* aDoSizeDecode = */ true);
   CONTAINER_ENSURE_SUCCESS(rv);
 
   return NS_OK;
 }
 
 nsresult
--- a/image/src/RasterImage.h
+++ b/image/src/RasterImage.h
@@ -115,33 +115,30 @@ class nsIThreadPool;
  * disposal.  So, in the middle of DoComposite when composing Frame 3, right
  * after destroying Frame 2's area, we copy compositingFrame to
  * prevCompositingFrame.  When DoComposite gets called to do Frame 4, we
  * copy prevCompositingFrame back, and then draw Frame 4 on top.
  *
  * @par
  * The mAnim structure has members only needed for animated images, so
  * it's not allocated until the second frame is added.
- *
- * @note
- * mAnimationMode and mLoopCount are not in the mAnim structure because
- * they have public setters.
  */
 
 class ScaleRequest;
 
 namespace mozilla {
 namespace layers {
 class LayerManager;
 class ImageContainer;
 class Image;
 }
 namespace image {
 
 class Decoder;
+class FrameAnimator;
 
 class RasterImage : public ImageResource
                   , public nsIProperties
                   , public SupportsWeakPtr<RasterImage>
 #ifdef DEBUG
                   , public imgIContainerDebug
 #endif
 {
@@ -319,30 +316,16 @@ private:
       return *mDecodeRequest->mStatusTracker;
     } else {
       return *mStatusTracker;
     }
   }
 
   nsresult OnImageDataCompleteCore(nsIRequest* aRequest, nsISupports*, nsresult aStatus);
 
-  struct Anim
-  {
-    //! Area of the first frame that needs to be redrawn on subsequent loops.
-    nsIntRect                  firstFrameRefreshArea;
-    uint32_t                   currentAnimationFrameIndex; // 0 to numFrames-1
-
-    // the time that the animation advanced to the current frame
-    TimeStamp                  currentAnimationFrameTime;
-
-    Anim() :
-      currentAnimationFrameIndex(0)
-    {}
-  };
-
   /**
    * Each RasterImage has a pointer to one or zero heap-allocated
    * DecodeRequests.
    */
   struct DecodeRequest
   {
     DecodeRequest(RasterImage* aImage)
       : mImage(aImage)
@@ -541,84 +524,35 @@ private:
                                     const nsIntRect &aSubimage,
                                     uint32_t aFlags);
 
   nsresult CopyFrame(uint32_t aWhichFrame,
                      uint32_t aFlags,
                      gfxImageSurface **_retval);
 
   /**
-   * Advances the animation. Typically, this will advance a single frame, but it
-   * may advance multiple frames. This may happen if we have infrequently
-   * "ticking" refresh drivers (e.g. in background tabs), or extremely short-
-   * lived animation frames.
-   *
-   * @param aTime the time that the animation should advance to. This will
-   *              typically be <= TimeStamp::Now().
-   *
-   * @param [out] aDirtyRect a pointer to an nsIntRect which encapsulates the
-   *        area to be repainted after the frame is advanced.
-   *
-   * @returns true, if the frame was successfully advanced, false if it was not
-   *          able to be advanced (e.g. the frame to which we want to advance is
-   *          still decoding). Note: If false is returned, then aDirtyRect will
-   *          remain unmodified.
-   */
-  bool AdvanceFrame(mozilla::TimeStamp aTime, nsIntRect* aDirtyRect);
-
-  /**
-   * Gets the length of a single loop of this image, in milliseconds.
-   *
-   * If this image is not finished decoding, is not animated, or it is animated
-   * but does not loop, returns 0.
-   */
-  uint32_t GetSingleLoopTime() const;
-
-  /**
    * Deletes and nulls out the frame in mFrames[framenum].
    *
    * Does not change the size of mFrames.
    *
    * @param framenum The index of the frame to be deleted.
    *                 Must lie in [0, mFrames.Length() )
    */
   void DeleteImgFrame(uint32_t framenum);
 
   imgFrame* GetImgFrameNoDecode(uint32_t framenum);
   imgFrame* GetImgFrame(uint32_t framenum);
   imgFrame* GetDrawableImgFrame(uint32_t framenum);
   imgFrame* GetCurrentImgFrame();
   uint32_t GetCurrentImgFrameIndex() const;
-  mozilla::TimeStamp GetCurrentImgFrameEndTime() const;
 
   size_t SizeOfDecodedWithComputedFallbackIfHeap(gfxASurface::MemoryLocation aLocation,
                                                  mozilla::MallocSizeOf aMallocSizeOf) const;
 
-  inline void EnsureAnimExists()
-  {
-    if (!mAnim) {
-
-      // Create the animation context
-      mAnim = new Anim();
-
-      // We don't support discarding animated images (See bug 414259).
-      // Lock the image and throw away the key.
-      //
-      // Note that this is inefficient, since we could get rid of the source
-      // data too. However, doing this is actually hard, because we're probably
-      // calling ensureAnimExists mid-decode, and thus we're decoding out of
-      // the source buffer. Since we're going to fix this anyway later, and
-      // since we didn't kill the source data in the old world either, locking
-      // is acceptable for the moment.
-      LockImage();
-
-      // Notify our observers that we are starting animation.
-      CurrentStatusTracker().RecordImageIsAnimated();
-    }
-  }
+  void EnsureAnimExists();
 
   nsresult InternalAddFrameHelper(uint32_t framenum, imgFrame *frame,
                                   uint8_t **imageData, uint32_t *imageLength,
                                   uint32_t **paletteData, uint32_t *paletteLength,
                                   imgFrame** aRetFrame);
   nsresult InternalAddFrame(uint32_t framenum, int32_t aX, int32_t aY, int32_t aWidth, int32_t aHeight,
                             gfxASurface::gfxImageFormat aFormat, uint8_t aPaletteDepth,
                             uint8_t **imageData, uint32_t *imageLength,
@@ -666,20 +600,17 @@ private: // data
   // The last frame we decoded for multipart images.
   imgFrame*                  mMultipartDecodedFrame;
 
   nsCOMPtr<nsIProperties>    mProperties;
 
   // IMPORTANT: if you use mAnim in a method, call EnsureImageIsDecoded() first to ensure
   // that the frames actually exist (they may have been discarded to save memory, or
   // we maybe decoding on draw).
-  RasterImage::Anim*        mAnim;
-
-  //! # loops remaining before animation stops (-1 no stop)
-  int32_t                    mLoopCount;
+  FrameAnimator* mAnim;
 
   // Discard members
   uint32_t                   mLockCount;
   DiscardTracker::Node       mDiscardTrackerNode;
 
   // Source data members
   nsCString                  mSourceDataMimeType;
 
@@ -786,20 +717,16 @@ protected:
 
   friend class ImageFactory;
 };
 
 inline NS_IMETHODIMP RasterImage::GetAnimationMode(uint16_t *aAnimationMode) {
   return GetAnimationModeInternal(aAnimationMode);
 }
 
-inline NS_IMETHODIMP RasterImage::SetAnimationMode(uint16_t aAnimationMode) {
-  return SetAnimationModeInternal(aAnimationMode);
-}
-
 // Asynchronous Decode Requestor
 //
 // We use this class when someone calls requestDecode() from within a decode
 // notification. Since requestDecode() involves modifying the decoder's state
 // (for example, possibly shutting down a header-only decode and starting a
 // full decode), we don't want to do this from inside a decoder.
 class imgDecodeRequestor : public nsRunnable
 {
--- a/image/src/imgFrame.cpp
+++ b/image/src/imgFrame.cpp
@@ -504,17 +504,17 @@ nsresult imgFrame::ImageUpdated(const ns
   nsIntRect boundsRect(mOffset, mSize);
   mDecoded.IntersectRect(mDecoded, boundsRect);
 
   mDirty = true;
 
   return NS_OK;
 }
 
-bool imgFrame::GetIsDirty()
+bool imgFrame::GetIsDirty() const
 {
   MutexAutoLock lock(mDirtyMutex);
   return mDirty;
 }
 
 nsIntRect imgFrame::GetRect() const
 {
   return nsIntRect(mOffset, mSize);
@@ -791,18 +791,21 @@ int32_t imgFrame::GetBlendMethod() const
   return mBlendMethod;
 }
 
 void imgFrame::SetBlendMethod(int32_t aBlendMethod)
 {
   mBlendMethod = (int8_t)aBlendMethod;
 }
 
+// This can be called from any thread.
 bool imgFrame::ImageComplete() const
 {
+  MutexAutoLock lock(mDirtyMutex);
+
   return mDecoded.IsEqualInterior(nsIntRect(mOffset, mSize));
 }
 
 // A hint from the image decoders that this image has no alpha, even
 // though we created is ARGB32.  This changes our format to RGB24,
 // which in turn will cause us to Optimize() to RGB24.  Has no effect
 // after Optimize() is called, though in all cases it will be just a
 // performance win -- the pixels are still correct and have the A byte
--- a/image/src/imgFrame.h
+++ b/image/src/imgFrame.h
@@ -36,17 +36,17 @@ public:
   nsresult Optimize();
 
   void Draw(gfxContext *aContext, gfxPattern::GraphicsFilter aFilter,
             const gfxMatrix &aUserSpaceToImageSpace, const gfxRect& aFill,
             const nsIntMargin &aPadding, const nsIntRect &aSubimage,
             uint32_t aImageFlags = imgIContainer::FLAG_NONE);
 
   nsresult ImageUpdated(const nsIntRect &aUpdateRect);
-  bool GetIsDirty();
+  bool GetIsDirty() const;
 
   nsIntRect GetRect() const;
   gfxASurface::gfxImageFormat GetFormat() const;
   bool GetNeedsBackground() const;
   uint32_t GetImageBytesPerRow() const;
   uint32_t GetImageDataLength() const;
   bool GetIsPaletted() const;
   bool GetHasAlpha() const;
@@ -147,17 +147,17 @@ private: // data
   nsRefPtr<gfxQuartzImageSurface> mQuartzSurface;
 #endif
 
   nsIntSize    mSize;
   nsIntPoint   mOffset;
 
   nsIntRect    mDecoded;
 
-  mozilla::Mutex mDirtyMutex;
+  mutable mozilla::Mutex mDirtyMutex;
 
   // The palette and image data for images that are paletted, since Cairo
   // doesn't support these images.
   // The paletted data comes first, then the image data itself.
   // Total length is PaletteDataLength() + GetImageDataLength().
   uint8_t*     mPalettedImageData;
 
   // Note that the data stored in gfxRGBA is *non-alpha-premultiplied*.
--- a/image/src/moz.build
+++ b/image/src/moz.build
@@ -12,16 +12,17 @@ EXPORTS += [
     'imgRequest.h',
     'imgRequestProxy.h',
 ]
 
 CPP_SOURCES += [
     'ClippedImage.cpp',
     'Decoder.cpp',
     'DiscardTracker.cpp',
+    'FrameAnimator.cpp',
     'FrameBlender.cpp',
     'FrameSequence.cpp',
     'FrozenImage.cpp',
     'Image.cpp',
     'ImageFactory.cpp',
     'ImageMetadata.cpp',
     'ImageOps.cpp',
     'ImageWrapper.cpp',