Bug 611994 - Fire timeupdate based on time change, not timer. r=roc a=roc
authorChris Pearce <chris@pearce.org.nz>
Thu, 25 Nov 2010 07:34:57 +1300
changeset 58179 a6d06ecdfde141aa4692cf75f012e26dab250ee1
parent 58178 53f49c89634b03a533bbe65997ce9ecc26c42d80
child 58180 605656b6f7798199e79938098bbd5e629dd84edb
push id17186
push usercpearce@mozilla.com
push dateWed, 24 Nov 2010 18:37:38 +0000
treeherdermozilla-central@a6d06ecdfde1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc, roc
bugs611994
milestone2.0b8pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 611994 - Fire timeupdate based on time change, not timer. r=roc a=roc
content/html/content/public/nsHTMLMediaElement.h
content/html/content/src/nsHTMLMediaElement.cpp
content/media/nsBuiltinDecoder.cpp
content/media/nsMediaDecoder.cpp
content/media/nsMediaDecoder.h
content/media/test/Makefile.in
content/media/test/seek1.js
content/media/test/seek10.js
content/media/test/seek11.js
content/media/test/seek2.js
content/media/test/seek3.js
content/media/test/seek4.js
content/media/test/seek5.js
content/media/test/test_timeupdate_seek.html
content/media/test/test_timeupdate_small_files.html
content/media/wave/nsWaveDecoder.cpp
--- a/content/html/content/public/nsHTMLMediaElement.h
+++ b/content/html/content/public/nsHTMLMediaElement.h
@@ -44,31 +44,34 @@
 #include "nsIChannel.h"
 #include "nsIHttpChannel.h"
 #include "nsThreadUtils.h"
 #include "nsIDOMRange.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsILoadGroup.h"
 #include "nsIObserver.h"
 #include "ImageLayers.h"
-
 #include "nsAudioStream.h"
 
 // Define to output information on decoding and painting framerate
 /* #define DEBUG_FRAME_RATE 1 */
 
 typedef PRUint16 nsMediaNetworkState;
 typedef PRUint16 nsMediaReadyState;
 
 class nsHTMLMediaElement : public nsGenericHTMLElement,
                            public nsIObserver
 {
   typedef mozilla::layers::ImageContainer ImageContainer;
 
 public:
+
+  typedef mozilla::TimeStamp TimeStamp;
+  typedef mozilla::TimeDuration TimeDuration;
+
   enum CanPlayStatus {
     CANPLAY_NO,
     CANPLAY_MAYBE,
     CANPLAY_YES
   };
 
   nsHTMLMediaElement(already_AddRefed<nsINodeInfo> aNodeInfo,
                      mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER);
@@ -334,16 +337,24 @@ public:
   virtual nsresult SetAcceptHeader(nsIHttpChannel* aChannel) = 0;
 
   /**
    * Sets the required request headers on the HTTP channel for
    * video or audio requests.
    */
   void SetRequestHeaders(nsIHttpChannel* aChannel);
 
+  /**
+   * Fires a timeupdate event. If aPeriodic is PR_TRUE, the event will only
+   * be fired if we've not fired a timeupdate event (for any reason) in the
+   * last 250ms, as required by the spec when the current time is periodically
+   * increasing during playback.
+   */
+  void FireTimeUpdate(PRBool aPeriodic);
+
 protected:
   class MediaLoadListener;
 
   /**
    * Changes mHasPlayedOrSeeked to aValue. If mHasPlayedOrSeeked changes
    * we'll force a reflow so that the video frame gets reflowed to reflect
    * the poster hiding or showing immediately.
    */
@@ -586,16 +597,24 @@ protected:
   // PRELOAD_UNDEFINED, its value is changed by calling
   // UpdatePreloadAction().
   PreloadAction mPreloadAction;
 
   // Size of the media. Updated by the decoder on the main thread if
   // it changes. Defaults to a width and height of -1 inot set.
   nsIntSize mMediaSize;
 
+  // Time that the last timeupdate event was fired. Read/Write from the
+  // main thread only.
+  TimeStamp mTimeUpdateTime;
+
+  // Media 'currentTime' value when the last timeupdate event occurred.
+  // Read/Write from the main thread only.
+  float mLastCurrentTime;
+
   nsRefPtr<gfxASurface> mPrintSurface;
 
   // Reference to the source element last returned by GetNextSource().
   // This is the child source element which we're trying to load from.
   nsCOMPtr<nsIContent> mSourceLoadCandidate;
 
   // An audio stream for writing audio directly from JS.
   nsRefPtr<nsAudioStream> mAudioStream;
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -119,16 +119,19 @@ static PRLogModuleInfo* gMediaElementEve
 
 #include "nsIContentSecurityPolicy.h"
 #include "nsIChannelPolicy.h"
 #include "nsChannelPolicy.h"
 
 using namespace mozilla::dom;
 using namespace mozilla::layers;
 
+// Number of milliseconds between timeupdate events as defined by spec
+#define TIMEUPDATE_MS 250
+
 // Under certain conditions there may be no-one holding references to
 // a media element from script, DOM parent, etc, but the element may still
 // fire meaningful events in the future so we can't destroy it yet:
 // 1) If the element is delaying the load event (or would be, if it were
 // in a document), then events up to loadeddata or error could be fired,
 // so we need to stay alive.
 // 2) If the element is not paused and playback has not ended, then
 // we will (or might) play, sending timeupdate and ended events and possibly
@@ -503,17 +506,17 @@ void nsHTMLMediaElement::AbortExistingLo
     ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING);
     mPaused = PR_TRUE;
 
     if (fireTimeUpdate) {
       // Since we destroyed the decoder above, the current playback position
       // will now be reported as 0. The playback position was non-zero when
       // we destroyed the decoder, so fire a timeupdate event so that the
       // change will be reflected in the controls.
-      DispatchAsyncEvent(NS_LITERAL_STRING("timeupdate"));
+      FireTimeUpdate(PR_FALSE);
     }
     DispatchEvent(NS_LITERAL_STRING("emptied"));
   }
 
   // We may have changed mPaused, mAutoplaying, mNetworkState and other
   // things which can affect AddRemoveSelfReference
   AddRemoveSelfReference();
 
@@ -1143,17 +1146,17 @@ NS_IMETHODIMP nsHTMLMediaElement::Pause(
 
   PRBool oldPaused = mPaused;
   mPaused = PR_TRUE;
   mAutoplaying = PR_FALSE;
   // We changed mPaused and mAutoplaying which can affect AddRemoveSelfReference
   AddRemoveSelfReference();
 
   if (!oldPaused) {
-    DispatchAsyncEvent(NS_LITERAL_STRING("timeupdate"));
+    FireTimeUpdate(PR_FALSE);
     DispatchAsyncEvent(NS_LITERAL_STRING("pause"));
   }
 
   return NS_OK;
 }
 
 /* attribute float volume; */
 NS_IMETHODIMP nsHTMLMediaElement::GetVolume(float *aVolume)
@@ -1261,16 +1264,17 @@ nsHTMLMediaElement::nsHTMLMediaElement(a
     mNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY),
     mReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING),
     mLoadWaitStatus(NOT_WAITING),
     mVolume(1.0),
     mChannels(0),
     mRate(0),
     mPreloadAction(PRELOAD_UNDEFINED),
     mMediaSize(-1,-1),
+    mLastCurrentTime(0.0),
     mAllowAudioData(PR_FALSE),
     mBegun(PR_FALSE),
     mLoadedFirstFrame(PR_FALSE),
     mAutoplaying(PR_TRUE),
     mAutoplayEnabled(PR_TRUE),
     mPaused(PR_TRUE),
     mMuted(PR_FALSE),
     mPlayingBeforeSeek(PR_FALSE),
@@ -1370,16 +1374,17 @@ NS_IMETHODIMP nsHTMLMediaElement::Play()
   // TODO: If the playback has ended, then the user agent must set
   // seek to the effective start.
   // TODO: The playback rate must be set to the default playback rate.
   if (mPaused) {
     DispatchAsyncEvent(NS_LITERAL_STRING("play"));
     switch (mReadyState) {
     case nsIDOMHTMLMediaElement::HAVE_METADATA:
     case nsIDOMHTMLMediaElement::HAVE_CURRENT_DATA:
+      FireTimeUpdate(PR_FALSE);
       DispatchAsyncEvent(NS_LITERAL_STRING("waiting"));
       break;
     case nsIDOMHTMLMediaElement::HAVE_FUTURE_DATA:
     case nsIDOMHTMLMediaElement::HAVE_ENOUGH_DATA:
       DispatchAsyncEvent(NS_LITERAL_STRING("playing"));
       break;
     }
   }
@@ -2010,23 +2015,24 @@ void nsHTMLMediaElement::Error(PRUint16 
 }
 
 void nsHTMLMediaElement::PlaybackEnded()
 {
   NS_ASSERTION(mDecoder->IsEnded(), "Decoder fired ended, but not in ended state");
   // We changed the state of IsPlaybackEnded which can affect AddRemoveSelfReference
   AddRemoveSelfReference();
 
+  FireTimeUpdate(PR_FALSE);
   DispatchAsyncEvent(NS_LITERAL_STRING("ended"));
 }
 
 void nsHTMLMediaElement::SeekStarted()
 {
   DispatchAsyncEvent(NS_LITERAL_STRING("seeking"));
-  DispatchAsyncEvent(NS_LITERAL_STRING("timeupdate"));
+  FireTimeUpdate(PR_FALSE);
 }
 
 void nsHTMLMediaElement::SeekCompleted()
 {
   mPlayingBeforeSeek = PR_FALSE;
   SetPlayedOrSeeked(PR_TRUE);
   DispatchAsyncEvent(NS_LITERAL_STRING("seeked"));
   // We changed whether we're seeking so we need to AddRemoveSelfReference
@@ -2071,16 +2077,17 @@ void nsHTMLMediaElement::UpdateReadyStat
     // a chance to run.
     // The arrival of more data can't change us out of this readyState.
     return;
   }
 
   if (aNextFrame != NEXT_FRAME_AVAILABLE) {
     ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_CURRENT_DATA);
     if (!mWaitingFired && aNextFrame == NEXT_FRAME_UNAVAILABLE_BUFFERING) {
+      FireTimeUpdate(PR_FALSE);
       DispatchAsyncEvent(NS_LITERAL_STRING("waiting"));
       mWaitingFired = PR_TRUE;
     }
     return;
   }
 
   // Now see if we should set HAVE_ENOUGH_DATA.
   // If it's something we don't know the size of, then we can't
@@ -2565,8 +2572,30 @@ void nsHTMLMediaElement::SetRequestHeade
   SetAcceptHeader(aChannel);
 
   // Set the Referer header
   nsIDocument* doc = GetOwnerDoc();
   if (doc) {
     aChannel->SetReferrer(doc->GetDocumentURI());
   }
 }
+
+void nsHTMLMediaElement::FireTimeUpdate(PRBool aPeriodic)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+
+  TimeStamp now = TimeStamp::Now();
+  float time = 0;
+  GetCurrentTime(&time);
+
+  // Fire a timupdate event if this is not a periodic update (i.e. it's a
+  // timeupdate event mandated by the spec), or if it's a periodic update
+  // and TIMEUPDATE_MS has passed since the last timeupdate event fired and
+  // the time has changed.
+  if (!aPeriodic ||
+      (mLastCurrentTime != time &&
+       (mTimeUpdateTime.IsNull() ||
+        now - mTimeUpdateTime >= TimeDuration::FromMilliseconds(TIMEUPDATE_MS)))) {
+    DispatchAsyncEvent(NS_LITERAL_STRING("timeupdate"));
+    mTimeUpdateTime = now;
+    mLastCurrentTime = time;
+  }
+}
--- a/content/media/nsBuiltinDecoder.cpp
+++ b/content/media/nsBuiltinDecoder.cpp
@@ -140,18 +140,16 @@ void nsBuiltinDecoder::Shutdown()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   
   if (mShuttingDown)
     return;
 
   mShuttingDown = PR_TRUE;
 
-  StopTimeUpdate();
-
   // This changes the decoder state to SHUTDOWN and does other things
   // necessary to unblock the state machine thread if it's blocked, so
   // the asynchronous shutdown in nsDestroyStateMachine won't deadlock.
   if (mDecoderStateMachine) {
     mDecoderStateMachine->Shutdown();
   }
 
   // Force any outstanding seek and byterange requests to complete
@@ -387,18 +385,16 @@ void nsBuiltinDecoder::MetadataLoaded(PR
     else {
       ChangeState(mNextState);
     }
   }
 
   if (resourceIsLoaded) {
     ResourceLoaded();
   }
-
-  StartTimeUpdate();
 }
 
 void nsBuiltinDecoder::ResourceLoaded()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
 
   // Don't handle ResourceLoaded if we are shutting down, or if
   // we need to ignore progress data due to seeking (in the case
--- a/content/media/nsMediaDecoder.cpp
+++ b/content/media/nsMediaDecoder.cpp
@@ -58,32 +58,28 @@
 #endif
 
 // Number of milliseconds between progress events as defined by spec
 #define PROGRESS_MS 350
 
 // Number of milliseconds of no data before a stall event is fired as defined by spec
 #define STALL_MS 3000
 
-// Number of milliseconds between timeupdate events as defined by spec
-#define TIMEUPDATE_MS 250
-
 // Number of estimated seconds worth of data we need to have buffered 
 // ahead of the current playback position before we allow the media decoder
 // to report that it can play through the entire media without the decode
 // catching up with the download. Having this margin make the
 // nsMediaDecoder::CanPlayThrough() calculation more stable in the case of
 // fluctuating bitrates.
 #define CAN_PLAY_THROUGH_MARGIN 20
 
 nsMediaDecoder::nsMediaDecoder() :
   mElement(0),
   mRGBWidth(-1),
   mRGBHeight(-1),
-  mLastCurrentTime(0.0),
   mVideoUpdateLock(nsnull),
   mPixelAspectRatio(1.0),
   mFrameBufferLength(0),
   mPinnedForSeek(PR_FALSE),
   mSizeChanged(PR_FALSE),
   mShuttingDown(PR_FALSE)
 {
   MOZ_COUNT_CTOR(nsMediaDecoder);
@@ -235,62 +231,21 @@ nsresult nsMediaDecoder::StopProgress()
     return NS_OK;
 
   nsresult rv = mProgressTimer->Cancel();
   mProgressTimer = nsnull;
 
   return rv;
 }
 
-static void TimeUpdateCallback(nsITimer* aTimer, void* aClosure)
-{
-  nsMediaDecoder* decoder = static_cast<nsMediaDecoder*>(aClosure);
-  decoder->FireTimeUpdate();
-}
-
 void nsMediaDecoder::FireTimeUpdate()
 {
   if (!mElement)
     return;
-
-  TimeStamp now = TimeStamp::Now();
-  float time = GetCurrentTime();
-
-  // If TIMEUPDATE_MS has passed since the last timeupdate event fired and the time
-  // has changed, fire a timeupdate event.
-  if ((mTimeUpdateTime.IsNull() ||
-       now - mTimeUpdateTime >= TimeDuration::FromMilliseconds(TIMEUPDATE_MS)) &&
-       mLastCurrentTime != time) {
-    mElement->DispatchEvent(NS_LITERAL_STRING("timeupdate"));
-    mTimeUpdateTime = now;
-    mLastCurrentTime = time;
-  }
-}
-
-nsresult nsMediaDecoder::StartTimeUpdate()
-{
-  if (mTimeUpdateTimer)
-    return NS_OK;
-
-  mTimeUpdateTimer = do_CreateInstance("@mozilla.org/timer;1");
-  return mTimeUpdateTimer->InitWithFuncCallback(TimeUpdateCallback,
-                                                this,
-                                                TIMEUPDATE_MS,
-                                                nsITimer::TYPE_REPEATING_SLACK);
-}
-
-nsresult nsMediaDecoder::StopTimeUpdate()
-{
-  if (!mTimeUpdateTimer)
-    return NS_OK;
-
-  nsresult rv = mTimeUpdateTimer->Cancel();
-  mTimeUpdateTimer = nsnull;
-
-  return rv;
+  mElement->FireTimeUpdate(PR_TRUE);
 }
 
 void nsMediaDecoder::SetVideoData(const gfxIntSize& aSize,
                                   float aPixelAspectRatio,
                                   Image* aImage)
 {
   nsAutoLock lock(mVideoUpdateLock);
 
--- a/content/media/nsMediaDecoder.h
+++ b/content/media/nsMediaDecoder.h
@@ -289,64 +289,47 @@ public:
 protected:
 
   // Start timer to update download progress information.
   nsresult StartProgress();
 
   // Stop progress information timer.
   nsresult StopProgress();
 
-  // Start timer to send timeupdate event
-  nsresult StartTimeUpdate();
-
-  // Stop timeupdate timer
-  nsresult StopTimeUpdate();
-
   // Ensures our media stream has been pinned.
   void PinForSeek();
 
   // Ensures our media stream has been unpinned.
   void UnpinForSeek();
 
 protected:
   // Timer used for updating progress events
   nsCOMPtr<nsITimer> mProgressTimer;
 
-  // Timer used for updating timeupdate events
-  nsCOMPtr<nsITimer> mTimeUpdateTimer;
-
   // This should only ever be accessed from the main thread.
   // It is set in Init and cleared in Shutdown when the element goes away.
   // The decoder does not add a reference the element.
   nsHTMLMediaElement* mElement;
 
   PRInt32 mRGBWidth;
   PRInt32 mRGBHeight;
 
   nsRefPtr<ImageContainer> mImageContainer;
 
   // Time that the last progress event was fired. Read/Write from the
   // main thread only.
   TimeStamp mProgressTime;
 
-  // Time that the last timeupdate event was fired. Read/Write from the
-  // main thread only.
-  TimeStamp mTimeUpdateTime;
-
   // Time that data was last read from the media resource. Used for
   // computing if the download has stalled and to rate limit progress events
   // when data is arriving slower than PROGRESS_MS. A value of null indicates
   // that a stall event has already fired and not to fire another one until
   // more data is received. Read/Write from the main thread only.
   TimeStamp mDataTime;
 
-  // Media 'currentTime' value when the last timeupdate event occurred.
-  // Read/Write from the main thread only.
-  float mLastCurrentTime;
-
   // Lock around the video RGB, width and size data. This
   // is used in the decoder backend threads and the main thread
   // to ensure that repainting the video does not use these
   // values while they are out of sync (width changed but
   // not height yet, etc).
   // Backends that are updating the height, width or writing
   // to the RGB buffer must obtain this lock first to ensure that
   // the video element does not use video data or sizes that are
--- a/content/media/test/Makefile.in
+++ b/content/media/test/Makefile.in
@@ -133,17 +133,16 @@ include $(topsrcdir)/config/rules.mk
 		test_readyState.html \
 		test_replay_metadata.html \
 		test_seek.html \
 		test_seek2.html \
 		test_seek_out_of_range.html \
 		test_source.html \
 		test_source_write.html \
 		test_standalone.html \
-		test_timeupdate_seek.html \
 		test_timeupdate_small_files.html \
 		test_volume.html \
 		test_video_to_canvas.html \
 		use_large_cache.js \
 		test_audiowrite.html \
 		$(NULL)
 
 # These tests are disabled until we figure out random failures.
--- a/content/media/test/seek1.js
+++ b/content/media/test/seek1.js
@@ -24,16 +24,17 @@ function startTest() {
   v.currentTime=seekTime;
   seekFlagStart = v.seeking;
   return false;
 }
 
 function seekStarted() {
   if (completed)
     return false;
+  ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
   v.pause();
   startPassed = true;
   return false;
 }
 
 function seekEnded() {
   if (completed)
     return false;
--- a/content/media/test/seek10.js
+++ b/content/media/test/seek10.js
@@ -13,16 +13,17 @@ function startTest() {
 }
 
 function done(evt) {
   ok(true, "We don't acutally test anything...");
   finish();
 }
 
 function seeking() {
+  ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
   v.onerror = done;
   v.src = "not a valid video file.";
   v.load(); // Cause the existing stream to close.
 }
 
 v.addEventListener("loadeddata", startTest, false);
 v.addEventListener("seeking", seeking, false);
 
--- a/content/media/test/seek11.js
+++ b/content/media/test/seek11.js
@@ -7,34 +7,39 @@ var completed = false;
 var target = 0;
 
 function startTest() {
   if (completed)
     return false;
   target = v.duration / 2;
   v.currentTime = target;
   v.currentTime = target;
+  v._seekTarget = target;
   return false;
 }
 
 function startSeeking() {
+  ok(v.currentTime >= v._seekTarget - 0.1,
+     "Video currentTime should be around " + v._seekTarget + ": " + v.currentTime);
   if (!seekedNonZero) {
     v.currentTime = target;
+    v._seekTarget = target;
   }
 }
 
 function seekEnded() {
   if (completed)
     return false;
 
   if (v.currentTime > 0) {
     ok(v.currentTime > target - 0.1 && v.currentTime < target + 0.1,
        "Seek to wrong destination " + v.currentTime);
     seekedNonZero = true;
     v.currentTime = 0.0;
+    v._seekTarget = 0.0;
   } else {
     ok(seekedNonZero, "Successfully seeked to nonzero");
     ok(true, "Seek back to zero was successful");
     completed = true;
     finish();
   }
 
   return false;
--- a/content/media/test/seek2.js
+++ b/content/media/test/seek2.js
@@ -14,16 +14,17 @@ function startTest() {
   v.play();
   return false;
 }
 
 function seekStarted() {
   if (completed)
     return false;
 
+  ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
   startPassed = true;
   return false;
 }
 
 function seekEnded() {
   if (completed)
     return false;
 
--- a/content/media/test/seek3.js
+++ b/content/media/test/seek3.js
@@ -1,36 +1,45 @@
 function test_seek3(v, seekTime, is, ok, finish) {
 
 // Test seeking works if current time is set but video is not played.
 var startPassed = false;
 var completed = false;
+var gotTimeupdate = false;
 
 function startTest() {
   if (completed)
     return false;
 
   v.currentTime=seekTime;
   return false;
 }
 
+function timeupdate() {
+  gotTimeupdate = true;
+  v.removeEventListener("timeupdate", timeupdate, false);
+}
+
 function seekStarted() {
   if (completed)
     return false;
 
+  ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
+  v.addEventListener("timeupdate", timeupdate, false);
   startPassed = true;
   return false;
 }
 
 function seekEnded() {
   if (completed)
     return false;
 
   var t = v.currentTime;
   ok(Math.abs(t - seekTime) <= 0.1, "Video currentTime should be around " + seekTime + ": " + t);
+  ok(gotTimeupdate, "Should have got timeupdate between seeking and seekended");
   completed = true;
   finish();
   return false;
 }
 
 v.addEventListener("loadedmetadata", startTest, false);
 v.addEventListener("seeking", seekStarted, false);
 v.addEventListener("seeked", seekEnded, false);
--- a/content/media/test/seek4.js
+++ b/content/media/test/seek4.js
@@ -4,24 +4,28 @@ function test_seek4(v, seekTime, is, ok,
 var seekCount = 0;
 var completed = false;
 
 function startTest() {
   if (completed)
     return false;
 
   v.currentTime=seekTime;
+  v._seekTarget=seekTime;
   return false;
 }
 
 function seekStarted() {
   if (completed)
     return false;
 
+  ok(v.currentTime >= v._seekTarget - 0.1,
+     "Video currentTime should be around " + v._seekTarget + ": " + v.currentTime);
   v.currentTime=seekTime/2;
+  v._seekTarget=seekTime/2;
   return false;
 }
 
 function seekEnded() {
   if (completed)
     return false;
 
   seekCount++;
--- a/content/media/test/seek5.js
+++ b/content/media/test/seek5.js
@@ -11,16 +11,17 @@ function startTest() {
 
   v.currentTime=seekTime;
   return false;
 }
 
 function seekStarted() {
   if (completed)
     return false;
+  ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
   startPassed = true;
   v.play();
   return false;
 }
 
 function seekEnded() {
   if (completed)
     return false;
deleted file mode 100644
--- a/content/media/test/test_timeupdate_seek.html
+++ /dev/null
@@ -1,85 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Media test: timeupdate and seek</title>
-  <script type="text/javascript" src="/MochiKit/packed.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-  <script type="text/javascript" src="manifest.js"></script>
-</head>
-<body>
-<pre id="test">
-<script class="testbody" type="text/javascript">
-// Test if the currentTime when a seek is requested is set to
-// the requested seek time as per 4.8.10.9 'seeking' in the
-// WHATWG spec.
-//
-// http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html#seeking
-
-var manager = new MediaTestManager;
-
-function do_loadedmetadata(e) {
-  var v = e.target;
-  if (v._finished)
-    return false;
-
-  var duration = v.duration;
-  v._seekTime = Math.round(duration / 2);
-  ok(!isNaN(duration), v._name + ": duration must be non NaN");
-  ok(v._seekTime >= 0.0, v._name + ": must have non-negative seek target");
-  ok(!isNaN(v._seekTime), v._name + ": seek target must be non NaN");
-  v.currentTime = v._seekTime;
-  return false;
-}
-
-function do_seeking(e) {
-  var v = e.target;
-  if (v._finished)
-    return false;
-
-  v._seeking = true;
-  ok(v.currentTime == v._seekTime,
-      v._name + ": currentTime of " + v.currentTime +
-     " should be requested seek time of " + v._seekTime);
-  return false;
-}
-
-function do_timeupdate(e) {
-  var v = e.target;
-  if (v._finished)
-    return false;
-
-  ok(v.currentTime == v._seekTime,
-      v._name + ": currentTime of " + v.currentTime +
-     " should be  requested seek time of " + v._seekTime);
-  v._finished = true;
-  v.pause();
-  v.parentNode.removeChild(v);
-  v.src = "";
-  ok(true, v._name + ": finished");
-  manager.finished(v.token);
-  return false;
-}
-
-function startTest(test, token) {
-  var type = /^video/.test(test.type) ? "video" : "audio";
-  var v = document.createElement(type);
-  v.token = token;
-  manager.started(token);
-  v.src = test.name;
-  ok(true, test.name + ": started");
-  v._name = test.name;
-  v._seeking = false;
-  v._finished = false;
-  v.addEventListener("loadedmetadata", do_loadedmetadata, false);
-  v.addEventListener("seeking", do_seeking, false);
-  v.addEventListener("timeupdate", do_timeupdate, false);
-  document.body.appendChild(v);
-}
-
-manager.runTests(gSeekTests, startTest);
-
-</script>
-</pre>
-</body>
-</html>
--- a/content/media/test/test_timeupdate_small_files.html
+++ b/content/media/test/test_timeupdate_small_files.html
@@ -14,36 +14,54 @@ https://bugzilla.mozilla.org/show_bug.cg
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=495319">Mozilla Bug 495319</a>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 var manager = new MediaTestManager;
 
 function timeupdate(e) {
-  e.target._timeupdateCount++;
+  var v = e.target;
+  v._timeupdateCount++;
+  ok(!v.gotEnded, v._name + " - shouldn't get timeupdate after ended");
 }
 
 function ended(e) {
   var v = e.target;
+  v.gotEnded = true;
   ok(v._timeupdateCount > 0, v._name + " - should see at least one timeupdate: " + v.currentTime);
   v._finished = true;
-  v.parentNode.removeChild(v);
-  manager.finished(v.token);
+  // Finish the test after 500ms. We shouldn't receive any timeupdate events
+  // after the ended event, so this gives time for any pending timeupdate events
+  // to fire so we can ensure we don't regress behaviour.
+  setTimeout(
+    function() {
+      // Remove the event listeners before removing the video from the document.
+      // We should receive a timeupdate and pause event when we remove the element
+      // from the document (as the element is specified to behave as if pause() was
+      // invoked when it's removed from a document), and we don't want those
+      // confusing the test results.
+      v.removeEventListener("ended", ended, false);
+      v.removeEventListener("timeupdate", timeupdate, false);
+      v.parentNode.removeChild(v);
+      manager.finished(v.token);
+    },
+    500);
 }
 
 function startTest(test, token) {
   var type = /^video/.test(test.type) ? "video" : "audio";
   var v = document.createElement(type);
   v.token = token;
   manager.started(token);
   v.src = test.name;
   v._name = test.name;
   v._timeupdateCount = 0;
   v._finished = false;
+  v.gotEnded = false;
   v.autoplay = true;
   v.addEventListener("ended", ended, false);
   v.addEventListener("timeupdate", timeupdate, false);
   document.body.appendChild(v);
 }
 
 manager.runTests(gSmallTests, startTest);
 
--- a/content/media/wave/nsWaveDecoder.cpp
+++ b/content/media/wave/nsWaveDecoder.cpp
@@ -1441,17 +1441,16 @@ nsWaveDecoder::MetadataLoaded()
 
   mMetadataLoadedReported = PR_TRUE;
 
   if (mResourceLoaded) {
     ResourceLoaded();
   } else {
     StartProgress();
   }
-  StartTimeUpdate();
 }
 
 void
 nsWaveDecoder::PlaybackEnded()
 {
   if (mShuttingDown) {
     return;
   }
@@ -1559,17 +1558,16 @@ nsWaveDecoder::NotifyDownloadEnded(nsres
 
 void
 nsWaveDecoder::Shutdown()
 {
   if (mShuttingDown)
     return;
 
   mShuttingDown = PR_TRUE;
-  StopTimeUpdate();
 
   nsMediaDecoder::Shutdown();
 
   // An event that gets posted to the main thread, when the media element is
   // being destroyed, to destroy the decoder. Since the decoder shutdown can
   // block and post events this cannot be done inside destructor calls. So
   // this event is posted asynchronously to the main thread to perform the
   // shutdown.