Bug 703379. Keep a table of media elements indexed by URI and when loading, try to find an existing element with the same URI and clone its decoder. r=doublec
authorRobert O'Callahan <robert@ocallahan.org>
Fri, 25 Nov 2011 15:06:22 +1300
changeset 82398 09d5d6e6dd28430855aff9a03601f3a56a37186b
parent 82397 1d9de4178e98dc1e0fdc24a10a0b73e15f843932
child 82399 d727c6fb36eb52ae5059725f0397ce2ebb061eef
push id519
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 00:38:35 +0000
treeherdermozilla-beta@788ea1ef610b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdoublec
bugs703379
milestone11.0a1
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 703379. Keep a table of media elements indexed by URI and when loading, try to find an existing element with the same URI and clone its decoder. r=doublec
content/html/content/public/nsHTMLMediaElement.h
content/html/content/src/nsHTMLMediaElement.cpp
content/media/test/Makefile.in
content/media/test/allowed.sjs
content/media/test/cancellable_request.sjs
content/media/test/dynamic_redirect.sjs
content/media/test/dynamic_resource.sjs
content/media/test/file_access_controls.html
content/media/test/manifest.js
content/media/test/redirect.sjs
content/media/test/referer.sjs
content/media/test/test_decoder_disable.html
content/media/test/test_load_same_resource.html
content/media/test/test_preload_actions.html
--- a/content/html/content/public/nsHTMLMediaElement.h
+++ b/content/html/content/public/nsHTMLMediaElement.h
@@ -365,32 +365,49 @@ protected:
    * Create a decoder for the given aMIMEType. Returns null if we
    * were unable to create the decoder.
    */
   already_AddRefed<nsMediaDecoder> CreateDecoder(const nsACString& aMIMEType);
 
   /**
    * Initialize a decoder as a clone of an existing decoder in another
    * element.
+   * mLoadingSrc must already be set.
    */
   nsresult InitializeDecoderAsClone(nsMediaDecoder* aOriginal);
 
   /**
    * Initialize a decoder to load the given channel. The decoder's stream
    * listener is returned via aListener.
+   * mLoadingSrc must already be set.
    */
   nsresult InitializeDecoderForChannel(nsIChannel *aChannel,
                                        nsIStreamListener **aListener);
 
   /**
    * Finish setting up the decoder after Load() has been called on it.
+   * Called by InitializeDecoderForChannel/InitializeDecoderAsClone.
    */
   nsresult FinishDecoderSetup(nsMediaDecoder* aDecoder);
 
   /**
+   * Call this after setting up mLoadingSrc and mDecoder.
+   */
+  void AddMediaElementToURITable();
+  /**
+   * Call this before clearing mLoadingSrc.
+   */
+  void RemoveMediaElementFromURITable();
+  /**
+   * Call this to find a media element with the same NodePrincipal and mLoadingSrc
+   * set to aURI, and with a decoder on which Load() has been called.
+   */
+  nsHTMLMediaElement* LookupMediaElementURITable(nsIURI* aURI);
+
+  /**
    * Execute the initial steps of the load algorithm that ensure existing
    * loads are aborted, the element is emptied, and a new load ID is
    * created.
    */
   void AbortExistingLoads();
 
   /**
    * Create a URI for the given aURISpec string.
@@ -426,17 +443,17 @@ protected:
    * Asynchronously awaits a stable state, and then causes SelectResource()
    * to be run on the main thread's event loop.
    */
   void QueueSelectResourceTask();
 
   /**
    * The resource-fetch algorithm step of the load algorithm.
    */
-  nsresult LoadResource(nsIURI* aURI);
+  nsresult LoadResource();
 
   /**
    * Selects the next <source> child from which to load a resource. Called
    * during the resource selection algorithm. Stores the return value in
    * mSourceLoadCandidate before returning.
    */
   nsIContent* GetNextSource();
 
@@ -487,21 +504,21 @@ protected:
     PRELOAD_UNDEFINED = 0, // not determined - used only for initialization
     PRELOAD_NONE = 1,      // do not preload
     PRELOAD_METADATA = 2,  // preload only the metadata (and first frame)
     PRELOAD_ENOUGH = 3     // preload enough data to allow uninterrupted
                            // playback
   };
 
   /**
-   * Suspends the load of resource at aURI, so that it can be resumed later
+   * Suspends the load of mLoadingSrc, so that it can be resumed later
    * by ResumeLoad(). This is called when we have a media with a 'preload'
    * attribute value of 'none', during the resource selection algorithm.
    */
-  void SuspendLoad(nsIURI* aURI);
+  void SuspendLoad();
 
   /**
    * Resumes a previously suspended load (suspended by SuspendLoad(uri)).
    * Will continue running the resource selection algorithm.
    * Sets mPreloadAction to aAction.
    */
   void ResumeLoad(PreloadAction aAction);
 
@@ -530,16 +547,17 @@ protected:
    **/
   void GetCurrentSpec(nsCString& aString);
 
   /**
    * Process any media fragment entries in the URI
    */
   void ProcessMediaFragmentURI();
 
+  // The current decoder. Load() has been called on this decoder.
   nsRefPtr<nsMediaDecoder> mDecoder;
 
   // A reference to the ImageContainer which contains the current frame
   // of video to display.
   nsRefPtr<ImageContainer> mImageContainer;
 
   // Holds a reference to the first channel we open to the media resource.
   // Once the decoder is created, control over the channel passes to the
@@ -592,21 +610,21 @@ protected:
   double mVolume;
 
   // Current number of audio channels.
   PRUint32 mChannels;
 
   // Current audio sample rate.
   PRUint32 mRate;
 
-  // URI of the resource we're attempting to load. When the decoder is
-  // successfully initialized, we rely on it to record the URI we're playing,
-  // and clear mLoadingSrc. This stores the value we return in the currentSrc
-  // attribute until the decoder is initialized. Use GetCurrentSrc() to access
-  // the currentSrc attribute.
+  // URI of the resource we're attempting to load. This stores the value we
+  // return in the currentSrc attribute. Use GetCurrentSrc() to access the
+  // currentSrc attribute.
+  // This is always the original URL we're trying to load --- before
+  // redirects etc.
   nsCOMPtr<nsIURI> mLoadingSrc;
   
   // Stores the current preload action for this element. Initially set to
   // PRELOAD_UNDEFINED, its value is changed by calling
   // UpdatePreloadAction().
   PreloadAction mPreloadAction;
 
   // Size of the media. Updated by the decoder on the main thread if
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -84,16 +84,17 @@
 #include "nsIDocShellTreeItem.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsIAppShell.h"
 #include "nsWidgetsCID.h"
 
 #include "nsIPrivateDOMEvent.h"
 #include "nsIDOMNotifyAudioAvailableEvent.h"
 #include "nsMediaFragmentURIParser.h"
+#include "nsURIHashKey.h"
 
 #ifdef MOZ_OGG
 #include "nsOggDecoder.h"
 #endif
 #ifdef MOZ_WAVE
 #include "nsWaveDecoder.h"
 #endif
 #ifdef MOZ_WEBM
@@ -474,34 +475,38 @@ void nsHTMLMediaElement::AbortExistingLo
   // Abort any already-running instance of the resource selection algorithm.
   mLoadWaitStatus = NOT_WAITING;
 
   // Set a new load ID. This will cause events which were enqueued
   // with a different load ID to silently be cancelled.
   mCurrentLoadID++;
 
   bool fireTimeUpdate = false;
+
   if (mDecoder) {
+    RemoveMediaElementFromURITable();
     fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0;
     mDecoder->Shutdown();
     mDecoder = nsnull;
   }
+  mLoadingSrc = nsnull;
 
   if (mNetworkState == nsIDOMHTMLMediaElement::NETWORK_LOADING ||
       mNetworkState == nsIDOMHTMLMediaElement::NETWORK_IDLE)
   {
     DispatchEvent(NS_LITERAL_STRING("abort"));
   }
 
   mError = nsnull;
   mLoadedFirstFrame = false;
   mAutoplaying = true;
   mIsLoadingFromSourceChildren = false;
   mSuspendedAfterFirstFrame = false;
   mAllowSuspendAfterFirstFrame = true;
+  mLoadIsSuspended = false;
   mSourcePointer = nsnull;
 
   // TODO: The playback rate must be set to the default playback rate.
 
   if (mNetworkState != nsIDOMHTMLMediaElement::NETWORK_EMPTY) {
     mNetworkState = nsIDOMHTMLMediaElement::NETWORK_EMPTY;
     ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING);
     mPaused = true;
@@ -643,22 +648,22 @@ void nsHTMLMediaElement::SelectResource(
     if (NS_SUCCEEDED(rv)) {
       LOG(PR_LOG_DEBUG, ("%p Trying load from src=%s", this, NS_ConvertUTF16toUTF8(src).get()));
       NS_ASSERTION(!mIsLoadingFromSourceChildren,
         "Should think we're not loading from source children by default");
       mLoadingSrc = uri;
       if (mPreloadAction == nsHTMLMediaElement::PRELOAD_NONE) {
         // preload:none media, suspend the load here before we make any
         // network requests.
-        SuspendLoad(uri);
+        SuspendLoad();
         mIsRunningSelectResource = false;
         return;
       }
 
-      rv = LoadResource(uri);
+      rv = LoadResource();
       if (NS_SUCCEEDED(rv)) {
         mIsRunningSelectResource = false;
         return;
       }
     }
     NoSupportedMediaSourceError();
   } else {
     // Otherwise, the source elements will be used.
@@ -745,55 +750,54 @@ void nsHTMLMediaElement::LoadFromSourceC
 
     mLoadingSrc = uri;
     NS_ASSERTION(mNetworkState == nsIDOMHTMLMediaElement::NETWORK_LOADING,
                  "Network state should be loading");
 
     if (mPreloadAction == nsHTMLMediaElement::PRELOAD_NONE) {
       // preload:none media, suspend the load here before we make any
       // network requests.
-      SuspendLoad(uri);
+      SuspendLoad();
       return;
     }
 
-    if (NS_SUCCEEDED(LoadResource(uri))) {
+    if (NS_SUCCEEDED(LoadResource())) {
       return;
     }
 
     // If we fail to load, loop back and try loading the next resource.
     DispatchAsyncSourceError(child);
   }
   NS_NOTREACHED("Execution should not reach here!");
 }
 
-void nsHTMLMediaElement::SuspendLoad(nsIURI* aURI)
+void nsHTMLMediaElement::SuspendLoad()
 {
   mLoadIsSuspended = true;
   mNetworkState = nsIDOMHTMLMediaElement::NETWORK_IDLE;
   DispatchAsyncEvent(NS_LITERAL_STRING("suspend"));
   ChangeDelayLoadStatus(false);
 }
 
 void nsHTMLMediaElement::ResumeLoad(PreloadAction aAction)
 {
   NS_ASSERTION(mLoadIsSuspended, "Can only resume preload if halted for one");
-  nsCOMPtr<nsIURI> uri = mLoadingSrc;
   mLoadIsSuspended = false;
   mPreloadAction = aAction;
   ChangeDelayLoadStatus(true);
   mNetworkState = nsIDOMHTMLMediaElement::NETWORK_LOADING;
   if (!mIsLoadingFromSourceChildren) {
     // We were loading from the element's src attribute.
-    if (NS_FAILED(LoadResource(uri))) {
+    if (NS_FAILED(LoadResource())) {
       NoSupportedMediaSourceError();
     }
   } else {
     // We were loading from a child <source> element. Try to resume the
     // load of that child, and if that fails, try the next child.
-    if (NS_FAILED(LoadResource(uri))) {
+    if (NS_FAILED(LoadResource())) {
       LoadFromSourceChildren();
     }
   }
 }
 
 static bool IsAutoplayEnabled()
 {
   return Preferences::GetBool("media.autoplay.enabled");
@@ -868,36 +872,44 @@ void nsHTMLMediaElement::UpdatePreloadAc
       // value "none". The preload value has changed to preload:metadata, so
       // resume the load. We'll pause the load again after we've read the
       // metadata.
       ResumeLoad(PRELOAD_METADATA);
     }
   }
 }
 
-nsresult nsHTMLMediaElement::LoadResource(nsIURI* aURI)
+nsresult nsHTMLMediaElement::LoadResource()
 {
   NS_ASSERTION(mDelayingLoadEvent,
                "Should delay load event (if in document) during load");
 
   // If a previous call to mozSetup() was made, kill that media stream
   // in order to use this new src instead.
   if (mAudioStream) {
     mAudioStream->Shutdown();
     mAudioStream = nsnull;
   }
 
   if (mChannel) {
     mChannel->Cancel(NS_BINDING_ABORTED);
     mChannel = nsnull;
   }
 
+  nsHTMLMediaElement* other = LookupMediaElementURITable(mLoadingSrc);
+  if (other) {
+    // Clone it.
+    nsresult rv = InitializeDecoderAsClone(other->mDecoder);
+    if (NS_SUCCEEDED(rv))
+      return rv;
+  }
+
   PRInt16 shouldLoad = nsIContentPolicy::ACCEPT;
   nsresult rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_MEDIA,
-                                          aURI,
+                                          mLoadingSrc,
                                           NodePrincipal(),
                                           static_cast<nsGenericElement*>(this),
                                           EmptyCString(), // mime type
                                           nsnull, // extra
                                           &shouldLoad,
                                           nsContentUtils::GetContentPolicy(),
                                           nsContentUtils::GetSecurityManager());
   NS_ENSURE_SUCCESS(rv, rv);
@@ -915,17 +927,17 @@ nsresult nsHTMLMediaElement::LoadResourc
   NS_ENSURE_SUCCESS(rv,rv);
   if (csp) {
     channelPolicy = do_CreateInstance("@mozilla.org/nschannelpolicy;1");
     channelPolicy->SetContentSecurityPolicy(csp);
     channelPolicy->SetLoadType(nsIContentPolicy::TYPE_MEDIA);
   }
   nsCOMPtr<nsIChannel> channel;
   rv = NS_NewChannel(getter_AddRefs(channel),
-                     aURI,
+                     mLoadingSrc,
                      nsnull,
                      loadGroup,
                      nsnull,
                      nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY,
                      channelPolicy);
   NS_ENSURE_SUCCESS(rv,rv);
 
   // The listener holds a strong reference to us.  This creates a
@@ -944,17 +956,17 @@ nsresult nsHTMLMediaElement::LoadResourc
       new nsCORSListenerProxy(loadListener,
                               NodePrincipal(),
                               channel,
                               false,
                               &rv);
   } else {
     rv = nsContentUtils::GetSecurityManager()->
            CheckLoadURIWithPrincipal(NodePrincipal(),
-                                     aURI,
+                                     mLoadingSrc,
                                      nsIScriptSecurityManager::STANDARD);
     listener = loadListener;
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(channel);
   if (hc) {
     // Use a byte range request from the start of the resource.
@@ -986,19 +998,21 @@ nsresult nsHTMLMediaElement::LoadWithCha
 {
   NS_ENSURE_ARG_POINTER(aChannel);
   NS_ENSURE_ARG_POINTER(aListener);
 
   *aListener = nsnull;
 
   AbortExistingLoads();
 
+  nsresult rv = aChannel->GetOriginalURI(getter_AddRefs(mLoadingSrc));
+  NS_ENSURE_SUCCESS(rv, rv);
+
   ChangeDelayLoadStatus(true);
-
-  nsresult rv = InitializeDecoderForChannel(aChannel, aListener);
+  rv = InitializeDecoderForChannel(aChannel, aListener);
   if (NS_FAILED(rv)) {
     ChangeDelayLoadStatus(false);
     return rv;
   }
 
   DispatchAsyncEvent(NS_LITERAL_STRING("loadstart"));
 
   return NS_OK;
@@ -1012,16 +1026,17 @@ NS_IMETHODIMP nsHTMLMediaElement::MozLoa
 
   nsCOMPtr<nsIContent> content = do_QueryInterface(aOther);
   nsHTMLMediaElement* other = static_cast<nsHTMLMediaElement*>(content.get());
   if (!other || !other->mDecoder)
     return NS_OK;
 
   ChangeDelayLoadStatus(true);
 
+  mLoadingSrc = other->mLoadingSrc;
   nsresult rv = InitializeDecoderAsClone(other->mDecoder);
   if (NS_FAILED(rv)) {
     ChangeDelayLoadStatus(false);
     return rv;
   }
 
   DispatchAsyncEvent(NS_LITERAL_STRING("loadstart"));
 
@@ -1236,16 +1251,86 @@ NS_IMETHODIMP nsHTMLMediaElement::SetMut
     mAudioStream->SetVolume(mMuted ? 0.0 : mVolume);
   }
 
   DispatchAsyncEvent(NS_LITERAL_STRING("volumechange"));
 
   return NS_OK;
 }
 
+class MediaElementSetForURI : public nsURIHashKey {
+public:
+  MediaElementSetForURI(const nsIURI* aKey) : nsURIHashKey(aKey) {}
+  MediaElementSetForURI(const MediaElementSetForURI& toCopy)
+    : nsURIHashKey(toCopy), mElements(toCopy.mElements) {}
+  nsTArray<nsHTMLMediaElement*> mElements;
+};
+
+typedef nsTHashtable<MediaElementSetForURI> MediaElementURITable;
+// Elements in this table must have non-null mDecoder and mLoadingSrc, and those
+// can't change while the element is in the table. The table is keyed by
+// the element's mLoadingSrc. Each entry has a list of all elements with the
+// same mLoadingSrc.
+static MediaElementURITable* gElementTable;
+
+void
+nsHTMLMediaElement::AddMediaElementToURITable()
+{
+  NS_ASSERTION(mDecoder && mDecoder->GetStream(), "Call this only with decoder Load called");
+  if (!gElementTable) {
+    gElementTable = new MediaElementURITable();
+    gElementTable->Init();
+  }
+  MediaElementSetForURI* entry = gElementTable->PutEntry(mLoadingSrc);
+  entry->mElements.AppendElement(this);
+}
+
+void
+nsHTMLMediaElement::RemoveMediaElementFromURITable()
+{
+  NS_ASSERTION(mDecoder, "Don't call this without decoder!");
+  NS_ASSERTION(mLoadingSrc, "Can't have decoder without source!");
+  if (!gElementTable)
+    return;
+  MediaElementSetForURI* entry = gElementTable->GetEntry(mLoadingSrc);
+  if (!entry)
+    return;
+  entry->mElements.RemoveElement(this);
+  if (entry->mElements.IsEmpty()) {
+    gElementTable->RemoveEntry(mLoadingSrc);
+    if (gElementTable->Count() == 0) {
+      delete gElementTable;
+      gElementTable = nsnull;
+    }
+  }
+}
+
+nsHTMLMediaElement*
+nsHTMLMediaElement::LookupMediaElementURITable(nsIURI* aURI)
+{
+  if (!gElementTable)
+    return nsnull;
+  MediaElementSetForURI* entry = gElementTable->GetEntry(aURI);
+  if (!entry)
+    return nsnull;
+  for (PRUint32 i = 0; i < entry->mElements.Length(); ++i) {
+    nsHTMLMediaElement* elem = entry->mElements[i];
+    bool equal;
+    // Look for elements that have the same principal.
+    // XXX when we implement crossorigin for video, we'll also need to check
+    // for the same crossorigin mode here. Ditto for anything else that could
+    // cause us to send different headers.
+    if (NS_SUCCEEDED(elem->NodePrincipal()->Equals(NodePrincipal(), &equal)) && equal) {
+      NS_ASSERTION(elem->mDecoder && elem->mDecoder->GetStream(), "Decoder gone");
+      return elem;
+    }
+  }
+  return nsnull;
+}
+
 nsHTMLMediaElement::nsHTMLMediaElement(already_AddRefed<nsINodeInfo> aNodeInfo)
   : nsGenericHTMLElement(aNodeInfo),
     mCurrentLoadID(0),
     mNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY),
     mReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING),
     mLoadWaitStatus(NOT_WAITING),
     mVolume(1.0),
     mChannels(0),
@@ -1292,26 +1377,24 @@ nsHTMLMediaElement::nsHTMLMediaElement(a
 
 nsHTMLMediaElement::~nsHTMLMediaElement()
 {
   NS_ASSERTION(!mHasSelfReference,
                "How can we be destroyed if we're still holding a self reference?");
 
   UnregisterFreezableElement();
   if (mDecoder) {
+    RemoveMediaElementFromURITable();
     mDecoder->Shutdown();
-    mDecoder = nsnull;
   }
   if (mChannel) {
     mChannel->Cancel(NS_BINDING_ABORTED);
-    mChannel = nsnull;
   }
   if (mAudioStream) {
     mAudioStream->Shutdown();
-    mAudioStream = nsnull;
   }
 }
 
 void nsHTMLMediaElement::StopSuspendingAfterFirstFrame()
 {
   mAllowSuspendAfterFirstFrame = false;
   if (!mSuspendedAfterFirstFrame)
     return;
@@ -1342,19 +1425,23 @@ void nsHTMLMediaElement::SetPlayedOrSeek
 NS_IMETHODIMP nsHTMLMediaElement::Play()
 {
   StopSuspendingAfterFirstFrame();
   SetPlayedOrSeeked(true);
 
   if (mNetworkState == nsIDOMHTMLMediaElement::NETWORK_EMPTY) {
     nsresult rv = Load();
     NS_ENSURE_SUCCESS(rv, rv);
-  } else if (mLoadIsSuspended) {
+  }
+  if (mLoadIsSuspended) {
     ResumeLoad(PRELOAD_ENOUGH);
-  } else if (mDecoder) {
+  }
+  // Even if we just did Load() or ResumeLoad(), we could already have a decoder
+  // here if we managed to clone an existing decoder.
+  if (mDecoder) {
     if (mDecoder->IsEnded()) {
       SetCurrentTime(0);
     }
     if (!mPausedForInactiveDocument) {
       nsresult rv = mDecoder->Play();
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
@@ -1806,16 +1893,18 @@ nsHTMLMediaElement::CreateDecoder(const 
     }
   }
 #endif
   return nsnull;
 }
 
 nsresult nsHTMLMediaElement::InitializeDecoderAsClone(nsMediaDecoder* aOriginal)
 {
+  NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
+
   nsMediaStream* originalStream = aOriginal->GetStream();
   if (!originalStream)
     return NS_ERROR_FAILURE;
   nsRefPtr<nsMediaDecoder> decoder = aOriginal->Clone();
   if (!decoder)
     return NS_ERROR_FAILURE;
 
   LOG(PR_LOG_DEBUG, ("%p Cloned decoder %p from %p", this, decoder.get(), aOriginal));
@@ -1843,16 +1932,18 @@ nsresult nsHTMLMediaElement::InitializeD
   }
 
   return FinishDecoderSetup(decoder);
 }
 
 nsresult nsHTMLMediaElement::InitializeDecoderForChannel(nsIChannel *aChannel,
                                                          nsIStreamListener **aListener)
 {
+  NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
+
   nsCAutoString mimeType;
   aChannel->GetContentType(mimeType);
 
   nsRefPtr<nsMediaDecoder> decoder = CreateDecoder(mimeType);
   if (!decoder) {
     return NS_ERROR_FAILURE;
   }
 
@@ -1873,20 +1964,20 @@ nsresult nsHTMLMediaElement::InitializeD
   // which owns the channel.
   mChannel = nsnull;
 
   return FinishDecoderSetup(decoder);
 }
 
 nsresult nsHTMLMediaElement::FinishDecoderSetup(nsMediaDecoder* aDecoder)
 {
+  NS_ASSERTION(mLoadingSrc, "mLoadingSrc set up");
+
   mDecoder = aDecoder;
-
-  // Decoder has assumed ownership responsibility for remembering the URI.
-  mLoadingSrc = nsnull;
+  AddMediaElementToURITable();
 
   // Force a same-origin check before allowing events for this media resource.
   mMediaSecurityVerified = false;
 
   // The new stream has not been suspended by us.
   mPausedForInactiveDocument = false;
   // But we may want to suspend it now.
   // This will also do an AddRemoveSelfReference.
@@ -2014,19 +2105,21 @@ void nsHTMLMediaElement::ResourceLoaded(
 void nsHTMLMediaElement::NetworkError()
 {
   Error(nsIDOMMediaError::MEDIA_ERR_NETWORK);
 }
 
 void nsHTMLMediaElement::DecodeError()
 {
   if (mDecoder) {
+    RemoveMediaElementFromURITable();
     mDecoder->Shutdown();
     mDecoder = nsnull;
   }
+  mLoadingSrc = nsnull;
   if (mIsLoadingFromSourceChildren) {
     mError = nsnull;
     if (mSourceLoadCandidate) {
       DispatchAsyncSourceError(mSourceLoadCandidate);
       QueueLoadFromSourceTask();
     } else {
       NS_WARNING("Should know the source we were loading from!");
     }
@@ -2664,23 +2757,20 @@ void nsHTMLMediaElement::FireTimeUpdate(
     mFragmentEnd = -1.0;
     mFragmentStart = -1.0;
     mDecoder->SetEndTime(mFragmentEnd);
   }
 }
 
 void nsHTMLMediaElement::GetCurrentSpec(nsCString& aString)
 {
-  if (mDecoder) {
-    nsMediaStream* stream = mDecoder->GetStream();
-    if (stream) {
-      stream->URI()->GetSpec(aString);
-    }
-  } else if (mLoadingSrc) {
+  if (mLoadingSrc) {
     mLoadingSrc->GetSpec(aString);
+  } else {
+    aString.Truncate();
   }
 }
 
 /* attribute double initialTime; */
 NS_IMETHODIMP nsHTMLMediaElement::GetInitialTime(double *aTime)
 {
   // If there is no start fragment then the initalTime is zero.
   // Clamp to duration if it is greater than duration.
--- a/content/media/test/Makefile.in
+++ b/content/media/test/Makefile.in
@@ -68,16 +68,17 @@ include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = \
 		allowed.sjs \
 		can_play_type_ogg.js \
 		can_play_type_wave.js \
 		can_play_type_webm.js \
 		cancellable_request.sjs \
 		dynamic_redirect.sjs \
+		dynamic_resource.sjs \
 		file_access_controls.html \
 		fragment_play.js \
 		fragment_noplay.js \
 		manifest.js \
 		reactivate_helper.html \
 		redirect.sjs \
 		referer.sjs \
 		seek1.js \
@@ -115,16 +116,17 @@ include $(topsrcdir)/config/rules.mk
 		test_decode_error.html \
 		test_decoder_disable.html \
 		test_delay_load.html \
 		test_error_on_404.html \
 		test_error_in_video_document.html \
 		test_info_leak.html \
 		test_load.html \
 		test_load_candidates.html \
+		test_load_same_resource.html \
 		test_load_source.html \
 		test_loop.html \
 		test_media_selection.html \
 		test_mozLoadFrom.html \
 		test_networkState.html \
 		test_new_audio.html \
 		test_paused.html \
 		test_paused_after_ended.html \
--- a/content/media/test/allowed.sjs
+++ b/content/media/test/allowed.sjs
@@ -1,21 +1,36 @@
+function parseQuery(request, key) {
+  var params = request.queryString.split('&');
+  for (var j = 0; j < params.length; ++j) {
+    var p = params[j];
+	if (p == key)
+	  return true;
+    if (p.indexOf(key + "=") == 0)
+	  return p.substring(key.length + 1);
+	if (p.indexOf("=") < 0 && key == "")
+	  return p;
+  }
+  return false;
+}
+
 var types = {
   ogg: "video/ogg",
   ogv: "video/ogg",
   oga: "audio/ogg",
   webm: "video/webm",
   wav: "audio/x-wav"
 };
 
 // Return file with name as per the query string with access control
 // allow headers.
 function handleRequest(request, response)
 {
-  var resource = request.queryString;
+  var resource = parseQuery(request, "");
+
   var file = Components.classes["@mozilla.org/file/directory_service;1"].
                         getService(Components.interfaces.nsIProperties).
                         get("CurWorkD", Components.interfaces.nsILocalFile);
   var fis  = Components.classes['@mozilla.org/network/file-input-stream;1'].
                         createInstance(Components.interfaces.nsIFileInputStream);
   var bis  = Components.classes["@mozilla.org/binaryinputstream;1"].
                         createInstance(Components.interfaces.nsIBinaryInputStream);
   var paths = "tests/content/media/test/" + resource;
--- a/content/media/test/cancellable_request.sjs
+++ b/content/media/test/cancellable_request.sjs
@@ -1,8 +1,22 @@
+function parseQuery(request, key) {
+  var params = request.queryString.split('&');
+  for (var j = 0; j < params.length; ++j) {
+    var p = params[j];
+	if (p == key)
+	  return true;
+    if (p.indexOf(key + "=") == 0)
+	  return p.substring(key.length + 1);
+	if (p.indexOf("=") < 0 && key == "")
+	  return p;
+  }
+  return false;
+}
+
 function push32BE(array, input) {
   array.push(String.fromCharCode((input >> 24) & 0xff));
   array.push(String.fromCharCode((input >> 16) & 0xff));
   array.push(String.fromCharCode((input >> 8) & 0xff));
   array.push(String.fromCharCode((input) & 0xff));
 }
 
 function push32LE(array, input) {
@@ -55,31 +69,31 @@ function poll(f) {
   if (f()) {
     return;
   }
   new Timer(function() { poll(f); }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
 }
 
 function handleRequest(request, response)
 {
-  var cancel = request.queryString.match(/^cancelkey=(.*)$/);
+  var cancel = parseQuery(request, "cancelkey");
   if (cancel) {
     setState(cancel[1], "cancelled");
     response.setStatusLine(request.httpVersion, 200, "OK");
     response.write("Cancel approved!");
     return;
   }
 
   var samples = [];
   for (var i = 0; i < 100000; ++i) {
     samples.push(0);
   }
   var bytes = buildWave(samples, 44100).join("");
 
-  var key = request.queryString.match(/^key=(.*)$/);
+  var key = parseQuery(request, "key");
   response.setHeader("Content-Type", "audio/x-wav");
   response.setHeader("Content-Length", ""+bytes.length, false);
 
   var out = new BinaryOutputStream(response.bodyOutputStream);
 
   var start = 0, end = bytes.length - 1;
   if (request.hasHeader("Range"))
   {
--- a/content/media/test/dynamic_redirect.sjs
+++ b/content/media/test/dynamic_redirect.sjs
@@ -1,14 +1,28 @@
+function parseQuery(request, key) {
+  var params = request.queryString.split('&');
+  for (var j = 0; j < params.length; ++j) {
+    var p = params[j];
+	if (p == key)
+	  return true;
+    if (p.indexOf(key + "=") == 0)
+	  return p.substring(key.length + 1);
+	if (p.indexOf("=") < 0 && key == "")
+	  return p;
+  }
+  return false;
+}
+
 // Return seek.ogv file content for the first request with a given key.
 // All subsequent requests return a redirect to a different-origin resource.
 function handleRequest(request, response)
 {
-  var key = (request.queryString.match(/^key=(.*)&/))[1];
-  var resource = (request.queryString.match(/res=(.*)$/))[1];
+  var key = parseQuery(request, "key");
+  var resource = parseQuery(request, "res");
 
   if (getState(key) == "redirect") {
     var origin = request.host == "mochi.test" ? "example.org" : "mochi.test:8888";
     response.setStatusLine(request.httpVersion, 303, "See Other");
     response.setHeader("Location", "http://" + origin + "/tests/content/media/test/" + resource);
     response.setHeader("Content-Type", "text/html");
     return;
   }
new file mode 100644
--- /dev/null
+++ b/content/media/test/dynamic_resource.sjs
@@ -0,0 +1,48 @@
+function parseQuery(request, key) {
+  var params = request.queryString.split('&');
+  for (var j = 0; j < params.length; ++j) {
+    var p = params[j];
+	if (p == key)
+	  return true;
+    if (p.indexOf(key + "=") == 0)
+	  return p.substring(key.length + 1);
+	if (p.indexOf("=") < 0 && key == "")
+	  return p;
+  }
+  return false;
+}
+
+// Return resource1 file content for the first request with a given key.
+// All subsequent requests return resource2. Both must be video/ogg.
+function handleRequest(request, response)
+{
+  var key = parseQuery(request, "key");
+  var resource1 = parseQuery(request, "res1");
+  var resource2 = parseQuery(request, "res2");
+
+  var resource = getState(key) == "2" ? resource2 : resource1;
+  setState(key, "2");
+
+  var file = Components.classes["@mozilla.org/file/directory_service;1"].
+                        getService(Components.interfaces.nsIProperties).
+                        get("CurWorkD", Components.interfaces.nsILocalFile);
+  var fis  = Components.classes['@mozilla.org/network/file-input-stream;1'].
+                        createInstance(Components.interfaces.nsIFileInputStream);
+  var bis  = Components.classes["@mozilla.org/binaryinputstream;1"].
+                        createInstance(Components.interfaces.nsIBinaryInputStream);
+  var paths = "tests/content/media/test/" + resource;
+  var split = paths.split("/");
+  for(var i = 0; i < split.length; ++i) {
+    file.append(split[i]);
+  }
+  fis.init(file, -1, -1, false);
+  dump("file=" + file + "\n");
+  bis.setInputStream(fis);
+  var bytes = bis.readBytes(bis.available());
+  response.setStatusLine(request.httpVersion, 206, "Partial Content");
+  response.setHeader("Content-Range", "bytes 0-" + (bytes.length - 1) + "/" + bytes.length);
+  response.setHeader("Content-Length", ""+bytes.length, false);
+  response.setHeader("Content-Type", "video/ogg", false);
+  response.write(bytes, bytes.length);
+  bis.close();
+}
--- a/content/media/test/file_access_controls.html
+++ b/content/media/test/file_access_controls.html
@@ -135,17 +135,23 @@ function nextTest() {
 
   gVideo = null;
   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
   Components.utils.forceGC();  
   
   gVideo = createVideo();
   gVideo.expectedResult = gTests[gTestNum].result;
   gVideo.testDescription = gTests[gTestNum].description;
-  gVideo.src = gTests[gTestNum].url;
+  // Uniquify the resource URL to ensure that the resources loaded by earlier or subsequent tests
+  // don't overlap with the resources we load here, which are loaded with non-default preferences set.
+  // We also want to make sure that an HTTP fetch actually happens for each testcase.
+  var url = gTests[gTestNum].url;
+  var random = Math.floor(Math.random()*1000000000);
+  url += (url.search(/\?/) < 0 ? "?" : "&") + "rand=" + random;
+  gVideo.src = url;
   //dump("Starting test " + gTestNum + " at " + gVideo.src + " expecting:" + gVideo.expectedResult + "\n");
   if (!gTestedRemoved) {
     document.body.appendChild(gVideo); 
     // Will cause load() to be invoked.
   } else {
     gVideo.load();
   }
   gTestNum++;
--- a/content/media/test/manifest.js
+++ b/content/media/test/manifest.js
@@ -20,19 +20,23 @@ var gProgressTests = [
   { name:"seek.ogv", type:"video/ogg", duration:3.966, size:285310 },
   { name:"320x240.ogv", type:"video/ogg", width:320, height:240, duration:0.233, size:28942 },
   { name:"seek.webm", type:"video/webm", duration:3.966, size:215529 },
   { name:"bogus.duh", type:"bogus/duh" }
 ];
 
 // Used by test_mozLoadFrom.  Need one test file per decoder backend, plus
 // anything for testing clone-specific bugs.
+var cloneKey = Math.floor(Math.random()*100000000);
 var gCloneTests = gSmallTests.concat([
   // Actual duration is ~200ms, we have Content-Duration lie about it.
   { name:"bug520908.ogv", type:"video/ogg", duration:9000 },
+  // short-video is more like 1s, so if you load this twice you'll get an unexpected duration
+  { name:"dynamic_resource.sjs?key=" + cloneKey + "&res1=320x240.ogv&res2=short-video.ogv",
+    type:"video/ogg", duration:0.233 },
 ]);
 
 // Used by test_play_twice.  Need one test file per decoder backend, plus
 // anything for testing bugs that occur when replying a played file.
 var gReplayTests = gSmallTests.concat([
   { name:"bug533822.ogg", type:"audio/ogg" },
 ]);
 
--- a/content/media/test/redirect.sjs
+++ b/content/media/test/redirect.sjs
@@ -1,25 +1,26 @@
+function parseQuery(request, key) {
+  var params = request.queryString.split('&');
+  for (var j = 0; j < params.length; ++j) {
+    var p = params[j];
+	if (p == key)
+	  return true;
+    if (p.indexOf(key + "=") == 0)
+	  return p.substring(key.length + 1);
+	if (p.indexOf("=") < 0 && key == "")
+	  return p;
+  }
+  return false;
+}
+
 // Return file content for the first request with a given key.
 // All subsequent requests return a redirect to a different-origin resource.
 function handleRequest(request, response)
 {
-  var params = request.queryString.split('&');
-  var domain = null;
-  var file = null;
-  var allowed = false;
-
-  for (var i=0; i<params.length; i++) {
-    var kv = params[i].split('=');
-    if (kv.length == 1 && kv[0] == 'allowed') {
-      allowed = true;
-    } else if (kv.length == 2 && kv[0] == 'file') {
-      file = kv[1];
-    } else if (kv.length == 2 && kv[0] == 'domain') {
-      domain = kv[1];
-    }
-  }
+  var domain = parseQuery(request, "domain");
+  var file = parseQuery(request, "file");
+  var allowed = parseQuery(request, "allowed");
 
   response.setStatusLine(request.httpVersion, 303, "See Other");
   response.setHeader("Location", "http://" + domain + "/tests/content/media/test/" + (allowed ? "allowed.sjs?" : "") + file);
   response.setHeader("Content-Type", "text/html");
-
 }
--- a/content/media/test/referer.sjs
+++ b/content/media/test/referer.sjs
@@ -1,14 +1,29 @@
+function parseQuery(request, key) {
+  var params = request.queryString.split('&');
+  for (var j = 0; j < params.length; ++j) {
+    var p = params[j];
+	if (p == key)
+	  return true;
+    if (p.indexOf(key + "=") == 0)
+	  return p.substring(key.length + 1);
+	if (p.indexOf("=") < 0 && key == "")
+	  return p;
+  }
+  return false;
+}
+
 function handleRequest(request, response)
 {
   var referer = request.hasHeader("Referer") ? request.getHeader("Referer")
                                              : undefined; 
   if (referer == "http://mochi.test:8888/tests/content/media/test/test_referer.html") {
-    var [ignore, name, type] = request.queryString.match(/name=(.*)&type=(.*)$/);
+    var name = parseQuery(request, "name");
+	var type = parseQuery(request, "type");
     var file = Components.classes["@mozilla.org/file/directory_service;1"].
                           getService(Components.interfaces.nsIProperties).
                           get("CurWorkD", Components.interfaces.nsILocalFile);
     var fis  = Components.classes['@mozilla.org/network/file-input-stream;1'].
                           createInstance(Components.interfaces.nsIFileInputStream);
     var bis  = Components.classes["@mozilla.org/binaryinputstream;1"].
                           createInstance(Components.interfaces.nsIBinaryInputStream);
     var paths = "tests/content/media/test/" + name;
--- a/content/media/test/test_decoder_disable.html
+++ b/content/media/test/test_decoder_disable.html
@@ -72,21 +72,21 @@ function videoError(event, id) {
   gLoadError[id]++;
   gErrorCount++;
   if (gErrorCount >= 4) {
     finishTest();
   }
 }
 
 </script>
-
+<!-- We make the resource URIs unique to ensure that they are (re)loaded with the new disable-decoder prefs. -->
 <video id="video1">
-  <source type="video/ogg" src="320x240.ogv" onerror="videoError(event, 'video1');"/>
-  <source type="audio/wave" src="r11025_u8_c1.wav" id='s2' onerror="videoError(event, 'video1');"/>
+  <source type="video/ogg" src="320x240.ogv?decoder_disabled=1" onerror="videoError(event, 'video1');"/>
+  <source type="audio/wave" src="r11025_u8_c1.wav?decoder_disabled=1" id='s2' onerror="videoError(event, 'video1');"/>
 </video>
 
-<video id="video2" src="320x240.ogv" onerror="videoError(event, 'video2');"></video>
-<video id="video3" src="r11025_u8_c1.wav" onerror="videoError(event, 'video3');"></video>
+<video id="video2" src="320x240.ogv?decoder_disabled=2" onerror="videoError(event, 'video2');"></video>
+<video id="video3" src="r11025_u8_c1.wav?decoder_disabled=2" onerror="videoError(event, 'video3');"></video>
 
 </pre>
 
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_load_same_resource.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test loading of the same resource in multiple elements</title>
+  <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">
+
+var manager = new MediaTestManager;
+
+function cloneLoaded(event) {
+  ok(true, "Clone loaded OK");
+  var e = event.target;
+
+  if (e._expectedDuration) {
+    ok(Math.abs(e.duration - e._expectedDuration) < 0.1,
+       "Clone " + e.currentSrc + " duration: " + e.duration + " expected: " + e._expectedDuration);
+  }
+
+  manager.finished(e.token);
+}
+
+function tryClone(event) {
+  var e = event.target;
+  var clone = e.cloneNode(false);
+  clone.token = e.token;
+  
+  if (e._expectedDuration) {
+    ok(Math.abs(e.duration - e._expectedDuration) < 0.1,
+       e.currentSrc + " duration: " + e.duration + " expected: " + e._expectedDuration);
+    clone._expectedDuration = e._expectedDuration;
+  }
+
+  clone.addEventListener("loadeddata", cloneLoaded, false);
+}
+
+// This test checks that loading the same URI twice in different elements at the same time
+// uses the same resource without doing another network fetch. One of the gCloneTests
+// uses dynamic_resource.sjs to return one resource on the first fetch and a different resource
+// on the second fetch. These resources have different lengths, so if the cloned element
+// does a network fetch it will get a resource with the wrong length and we get a test
+// failure.
+
+function initTest(test, token) {
+  var elemType = /^audio/.test(test.type) ? "audio" : "video";
+  var e = document.createElement(elemType);
+  if (e.canPlayType(test.type)) {
+    e.src = test.name;
+    if (test.duration) {
+      e._expectedDuration = test.duration;
+    }
+    ok(true, "Trying to load " + test.name);
+    e.addEventListener("loadeddata", tryClone, false);
+    e.load();
+    e.token = token;
+    manager.started(token);
+  }
+}
+
+manager.runTests(gCloneTests, initTest);
+
+</script>
+</pre>
+</body>
+</html>
--- a/content/media/test/test_preload_actions.html
+++ b/content/media/test/test_preload_actions.html
@@ -25,16 +25,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 var manager = new MediaTestManager;
 
 manager.onFinished = function() {
   is(gotLoadEvent, true, "Should not have delayed the load event indefinitely");
 };
 
 var test = getPlayableVideo(gSeekTests);
+var baseName = test.name;
 var gTest = test;
 var bogusSrc = "bogus.duh";
 var bogusType = "video/bogus";
 var gotLoadEvent = false;
 var finished = false;
 
 addLoadEvent(function() {gotLoadEvent=true;});
 
@@ -139,18 +140,19 @@ var tests = [
       document.body.appendChild(v);
     },
   },
   {
     // 4. Add preload:none video to document. Call play(), should load then play through.
     suspend:
     function(e) {
       var v = e.target;
-      if (v._gotSuspend) 
+      if (v._gotSuspend) {
         return; // We can receive multiple suspend events, like the one after download completes.
+      }
       v._gotSuspend = true;
       is(v._gotLoadStart, true, "(4) Must get loadstart.");
       is(v._gotLoadedMetaData, false, "(4) Must not get loadedmetadata.");
       is(v.readyState, v.HAVE_NOTHING, "(4) ReadyState must be HAVE_NOTHING");
       is(v.networkState, v.NETWORK_IDLE, "(4) NetworkState must be NETWORK_IDLE");
       v.play(); // Should load and play through.
     },
     
@@ -552,21 +554,30 @@ var tests = [
       v.preload = "none";
       v.src = test.name; // Schedules async section to continue load algorithm.
       document.body.appendChild(v);
       v.play(); // Should cause preload:none to be overridden.
     },  
     }
 ];
 
+var iterationCount = 0;
 function startTest(test, token) {
+  if (test == tests[0]) {
+    ++iterationCount;
+  }
+  if (iterationCount == 2) {
+    // Do this series of tests on logically different resources
+    test.name = baseName + "?" + Math.floor(Math.random()*100000);
+  }
   var v = document.createElement("video");
   v.token = token;
   test.setup(v);
   manager.started(token);
 }
 
-manager.runTests(tests, startTest);
+var twiceTests = tests.concat(tests);
+manager.runTests(twiceTests, startTest);
 
 </script>
 </pre>
 </body>
 </html>