Bug 464158. Implement media type switching based on the MIME type of the loaded resouce. r=doublec,sr=roc
☠☠ backed out by bd6ded525554 ☠ ☠
authorMatthew Gregan <kinetik@flim.org>
Wed, 03 Dec 2008 10:16:15 +1300
changeset 22182 3f5a6da199fcfb3620265f2abfac3ed7f26394ea
parent 22181 1d84189da181888ef276b4dc76271b7fa27e6337
child 22183 ce72e9a5dca03ddcb5902b74c2e97c528da72ece
child 22187 bd6ded5255549b5c936ea2c0ae6959f05306385e
push id3843
push userrocallahan@mozilla.com
push dateTue, 02 Dec 2008 21:22:28 +0000
treeherdermozilla-central@31e5958cb97a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdoublec, roc
bugs464158
milestone1.9.2a1pre
Bug 464158. Implement media type switching based on the MIME type of the loaded resouce. r=doublec,sr=roc
content/html/content/public/nsHTMLMediaElement.h
content/html/content/src/nsHTMLMediaElement.cpp
content/media/video/test/Makefile.in
content/media/video/test/test_bug463162.xhtml
content/media/video/test/test_media_selection.html
--- a/content/html/content/public/nsHTMLMediaElement.h
+++ b/content/html/content/public/nsHTMLMediaElement.h
@@ -168,38 +168,41 @@ public:
   static void InitMediaTypes();
   /**
    * Shutdown data for available media types
    */
   static void ShutdownMediaTypes();
 
 protected:
   /**
-   * Figure out which resource to load (either the 'src' attribute or
-   * a <source> child) and create the decoder for it.
+   * Figure out which resource to load (either the 'src' attribute or a
+   * <source> child) and return the associated URI.
    */
-  nsresult PickMediaElement();
+  nsresult PickMediaElement(nsIURI** aURI);
   /**
    * Create a decoder for the given aMIMEType. Returns false if we
    * were unable to create the decoder.
    */
   PRBool CreateDecoder(const nsACString& aMIMEType);
   /**
-   * Initialize a decoder to load the given URI.
-   */
-  nsresult InitializeDecoder(const nsAString& aURISpec);
-  /**
    * Initialize a decoder to load the given channel. The decoder's stream
    * listener is returned via aListener.
    */
   nsresult InitializeDecoderForChannel(nsIChannel *aChannel,
                                        nsIStreamListener **aListener);
 
+  /**
+   * Create a URI for the given aURISpec string.
+   */
+  nsresult NewURIFromString(const nsAutoString& aURISpec, nsIURI** aURI);
+
   nsRefPtr<nsMediaDecoder> mDecoder;
 
+  nsCOMPtr<nsIChannel> mChannel;
+
   // Error attribute
   nsCOMPtr<nsIDOMHTMLMediaError> mError;
 
   // Media loading flags. See: 
   //   http://www.whatwg.org/specs/web-apps/current-work/#video)
   nsMediaNetworkState mNetworkState;
   nsMediaReadyState mReadyState;
 
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -91,16 +91,74 @@ public:
   
   NS_IMETHOD Run() {
     return mProgress ?
       mElement->DispatchProgressEvent(mName) :
       mElement->DispatchSimpleEvent(mName);
   }
 };
 
+class nsMediaLoadListener : public nsIStreamListener
+{
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIREQUESTOBSERVER
+  NS_DECL_NSISTREAMLISTENER
+
+public:
+  nsMediaLoadListener(nsHTMLMediaElement* aElement)
+    : mElement(aElement)
+  {
+    NS_ABORT_IF_FALSE(mElement, "Must pass an element to call back");
+  }
+
+private:
+  nsRefPtr<nsHTMLMediaElement> mElement;
+  nsCOMPtr<nsIStreamListener> mNextListener;
+};
+
+NS_IMPL_ISUPPORTS2(nsMediaLoadListener, nsIRequestObserver, nsIStreamListener)
+
+NS_IMETHODIMP nsMediaLoadListener::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
+{
+  nsresult rv;
+
+  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+  if (channel &&
+      mElement &&
+      NS_SUCCEEDED(mElement->LoadWithChannel(channel, getter_AddRefs(mNextListener))) &&
+      mNextListener) {
+    rv = mNextListener->OnStartRequest(aRequest, aContext);
+  } else {
+    // If LoadWithChannel did not return a listener, we abort the connection
+    // since we aren't interested in keeping the channel alive ourselves.
+    rv = NS_BINDING_ABORTED;
+  }
+
+  // The element is only needed until we've had a chance to call
+  // LoadWithChannel.
+  mElement = nsnull;
+
+  return rv;
+}
+
+NS_IMETHODIMP nsMediaLoadListener::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatus)
+{
+  if (mNextListener) {
+    return mNextListener->OnStopRequest(aRequest, aContext, aStatus);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsMediaLoadListener::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext,
+                                                   nsIInputStream* aStream, PRUint32 aOffset, PRUint32 aCount)
+{
+  NS_ABORT_IF_FALSE(mNextListener, "Must have a listener");
+  return mNextListener->OnDataAvailable(aRequest, aContext, aStream, aOffset, aCount);
+}
+
 // nsIDOMHTMLMediaElement
 NS_IMPL_URI_ATTR(nsHTMLMediaElement, Src, src)
 NS_IMPL_BOOL_ATTR(nsHTMLMediaElement, Controls, controls)
 NS_IMPL_BOOL_ATTR(nsHTMLMediaElement, Autoplay, autoplay)
 
 /* readonly attribute nsIDOMHTMLMediaError error; */
 NS_IMETHODIMP nsHTMLMediaElement::GetError(nsIDOMHTMLMediaError * *aError)
 {
@@ -148,27 +206,79 @@ NS_IMETHODIMP nsHTMLMediaElement::GetTot
 {
   *aTotalBytes = mDecoder ? PRUint32(mDecoder->GetTotalBytes()) : 0;
   return NS_OK;
 }
 
 /* void load (); */
 NS_IMETHODIMP nsHTMLMediaElement::Load()
 {
-  return LoadWithChannel(nsnull, nsnull);
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = PickMediaElement(getter_AddRefs(uri));
+  if (NS_FAILED(rv))
+    return rv;
+
+  if (mChannel) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+    mChannel = nsnull;
+  }
+
+  rv = NS_NewChannel(getter_AddRefs(mChannel),
+                     uri,
+                     nsnull,
+                     nsnull,
+                     nsnull,
+                     nsIRequest::LOAD_NORMAL);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // The listener holds a strong reference to us.  This creates a reference
+  // cycle which is manually broken in the listener's OnStartRequest method
+  // after it is finished with the element.
+  nsCOMPtr<nsIStreamListener> listener = new nsMediaLoadListener(this);
+  NS_ENSURE_TRUE(listener, NS_ERROR_OUT_OF_MEMORY);
+
+  nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(mChannel);
+  if (hc) {
+    // Use a byte range request from the start of the resource.
+    // This enables us to detect if the stream supports byte range
+    // requests, and therefore seeking, early.
+    hc->SetRequestHeader(NS_LITERAL_CSTRING("Range"),
+                         NS_LITERAL_CSTRING("bytes=0-"),
+                         PR_FALSE);
+  }
+
+  rv = mChannel->AsyncOpen(listener, nsnull);
+  if (NS_FAILED(rv)) {
+    // OnStartRequest is guaranteed to be called if the open succeeds.  If
+    // the open failed, the listener's OnStartRequest will never be called,
+    // so we need to break the element->channel->listener->element reference
+    // cycle here.  The channel holds the only reference to the listener,
+    // and is useless now anyway, so drop our reference to it to allow it to
+    // be destroyed.
+    mChannel = nsnull;
+    return rv;
+  }
+
+  mNetworkState = nsIDOMHTMLMediaElement::LOADING;
+
+  return NS_OK;
 }
 
 nsresult nsHTMLMediaElement::LoadWithChannel(nsIChannel *aChannel,
                                              nsIStreamListener **aListener)
 {
-  NS_ASSERTION((aChannel == nsnull) == (aListener == nsnull),
-               "channel and listener should both be null or both non-null");
+  NS_ENSURE_ARG_POINTER(aChannel);
+  NS_ENSURE_ARG_POINTER(aListener);
+
+  *aListener = nsnull;
 
-  if (aListener) {
-    *aListener = nsnull;
+  if (mDecoder) {
+    mDecoder->ElementUnavailable();
+    mDecoder->Shutdown();
+    mDecoder = nsnull;
   }
 
   if (mBegun) {
     mBegun = PR_FALSE;
     
     mError = new nsHTMLMediaError(nsHTMLMediaError::MEDIA_ERR_ABORTED);
     DispatchProgressEvent(NS_LITERAL_STRING("abort"));
     return NS_OK;
@@ -184,23 +294,20 @@ nsresult nsHTMLMediaElement::LoadWithCha
     mNetworkState = nsIDOMHTMLMediaElement::EMPTY;
     ChangeReadyState(nsIDOMHTMLMediaElement::DATA_UNAVAILABLE);
     mPaused = PR_TRUE;
     // TODO: The current playback position must be set to 0.
     // TODO: The currentLoop DOM attribute must be set to 0.
     DispatchSimpleEvent(NS_LITERAL_STRING("emptied"));
   }
 
-  nsresult rv;
-  if (aChannel) {
-    rv = InitializeDecoderForChannel(aChannel, aListener);
-  } else {
-    rv = PickMediaElement();
+  nsresult rv = InitializeDecoderForChannel(aChannel, aListener);
+  if (NS_FAILED(rv)) {
+    return rv;
   }
-  NS_ENSURE_SUCCESS(rv, rv);
 
   mBegun = PR_TRUE;
   mEnded = PR_FALSE;
 
   DispatchAsyncProgressEvent(NS_LITERAL_STRING("loadstart"));
 
   return NS_OK;
 }
@@ -361,16 +468,20 @@ nsHTMLMediaElement::nsHTMLMediaElement(n
 }
 
 nsHTMLMediaElement::~nsHTMLMediaElement()
 {
   if (mDecoder) {
     mDecoder->Shutdown();
     mDecoder = nsnull;
   }
+  if (mChannel) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+    mChannel = nsnull;
+  }
 }
 
 NS_IMETHODIMP
 nsHTMLMediaElement::Play(void)
 {
   if (!mDecoder)
     return NS_OK;
 
@@ -559,16 +670,31 @@ void nsHTMLMediaElement::ShutdownMediaTy
 #ifdef MOZ_WAVE
     for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(gWaveTypes); i++) {
       catMan->DeleteCategoryEntry("Gecko-Content-Viewers", gWaveTypes[i], PR_FALSE);
     }
 #endif
   }
 }
 
+static PRBool CanDecode(const nsACString& aType)
+{
+#ifdef MOZ_OGG
+  if (IsOggType(aType)) {
+    return PR_TRUE;
+  }
+#endif
+#ifdef MOZ_WAVE
+  if (IsWaveType(aType)) {
+    return PR_TRUE;
+  }
+#endif
+  return PR_FALSE;
+}
+
 PRBool nsHTMLMediaElement::CreateDecoder(const nsACString& aType)
 {
 #ifdef MOZ_OGG
   if (IsOggType(aType)) {
     mDecoder = new nsOggDecoder();
     if (mDecoder && !mDecoder->Init()) {
       mDecoder = nsnull;
     }
@@ -591,91 +717,94 @@ nsresult nsHTMLMediaElement::InitializeD
   nsCAutoString mimeType;
   aChannel->GetContentType(mimeType);
 
   if (!CreateDecoder(mimeType))
     return NS_ERROR_FAILURE;
 
   mNetworkState = nsIDOMHTMLMediaElement::LOADING;
   mDecoder->ElementAvailable(this);
-  
+
   return mDecoder->Load(nsnull, aChannel, aListener);
 }
 
-nsresult nsHTMLMediaElement::PickMediaElement()
+nsresult nsHTMLMediaElement::NewURIFromString(const nsAutoString& aURISpec, nsIURI** aURI)
 {
-  // Implements:
-  // http://www.whatwg.org/specs/web-apps/current-work/#pick-a
-  nsAutoString src;
-  if (GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) {
-#ifdef MOZ_OGG
-    // Currently assuming an Ogg file
-    // TODO: Instantiate decoder based on type
-    if (mDecoder) {
-      mDecoder->ElementUnavailable();
-      mDecoder->Shutdown();
-      mDecoder = nsnull;
-    }
-
-    mDecoder = new nsOggDecoder();
-    if (mDecoder && !mDecoder->Init()) {
-      mDecoder = nsnull;
-    }
-#endif
-    return InitializeDecoder(src);
-  }
+  NS_ENSURE_ARG_POINTER(aURI);
 
-  // Checking of 'source' elements as per:
-  // http://www.whatwg.org/specs/web-apps/current-work/#pick-a
-  PRUint32 count = GetChildCount();
-  for (PRUint32 i = 0; i < count; ++i) {
-    nsIContent* child = GetChildAt(i);
-    NS_ASSERTION(child, "GetChildCount lied!");
-    
-    nsCOMPtr<nsIContent> source = do_QueryInterface(child);
-    if (source) {
-      nsAutoString type;
-      nsAutoString src;
-      if (source->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src) &&
-          source->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type) &&
-          CreateDecoder(NS_ConvertUTF16toUTF8(type)))
-        return InitializeDecoder(src);
-    }
-  }
-
-  return NS_ERROR_DOM_INVALID_STATE_ERR;
-}
-
-nsresult nsHTMLMediaElement::InitializeDecoder(const nsAString& aURISpec)
-{
-  mNetworkState = nsIDOMHTMLMediaElement::LOADING;
+  *aURI = nsnull;
 
   nsCOMPtr<nsIDocument> doc = GetOwnerDoc();
   if (!doc) {
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
-  nsresult rv;
-  nsCOMPtr<nsIURI> uri;
-  nsCOMPtr<nsIURI> baseURL = GetBaseURI();
-  rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri),
-                                                 aURISpec,
-                                                 doc,
-                                                 baseURL);
+  nsCOMPtr<nsIURI> baseURI = GetBaseURI();
+  nsresult rv = nsContentUtils::NewURIWithDocumentCharset(aURI,
+                                                          aURISpec,
+                                                          doc,
+                                                          baseURI);
   NS_ENSURE_SUCCESS(rv, rv);
+ 
+  PRBool equal;
+  if (aURISpec.IsEmpty() &&
+      doc->GetDocumentURI() &&
+      NS_SUCCEEDED(doc->GetDocumentURI()->Equals(*aURI, &equal)) &&
+      equal) {
+    // It's not possible for a media resource to be embedded in the current
+    // document we extracted aURISpec from, so there's no point returning
+    // the current document URI just to let the caller attempt and fail to
+    // decode it.
+    NS_RELEASE(*aURI);
+    return NS_ERROR_DOM_INVALID_STATE_ERR;
+  }
 
-  if (mDecoder) {
-    mDecoder->ElementAvailable(this);
-    rv = mDecoder->Load(uri, nsnull, nsnull);
-    if (NS_FAILED(rv)) {
-      mDecoder->Shutdown();
-      mDecoder = nsnull;
+  return NS_OK;
+}
+
+nsresult nsHTMLMediaElement::PickMediaElement(nsIURI** aURI)
+{
+  NS_ENSURE_ARG_POINTER(aURI);
+
+  // Implements:
+  // http://www.whatwg.org/specs/web-apps/current-work/#pick-a-media-resource
+  nsAutoString src;
+  if (GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) {
+    return NewURIFromString(src, aURI);
+  }
+
+  // Checking of 'source' elements as per:
+  // http://www.whatwg.org/specs/web-apps/current-work/#pick-a-media-resource
+  PRUint32 count = GetChildCount();
+  for (PRUint32 i = 0; i < count; ++i) {
+    nsIContent* child = GetChildAt(i);
+    NS_ASSERTION(child, "GetChildCount lied!");
+
+    nsCOMPtr<nsIContent> source = do_QueryInterface(child);
+    if (source &&
+        source->Tag() == nsGkAtoms::source &&
+        source->IsNodeOfType(nsINode::eHTML)) {
+      nsAutoString type;
+      nsAutoString src;
+      if (source->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) {
+        if (source->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type)) {
+          if (CanDecode(NS_ConvertUTF16toUTF8(type))) {
+            return NewURIFromString(src, aURI);
+          }
+        } else if (i + 1 == count) {
+          // The last source element is permitted to omit the type
+          // attribute, in which case we need to open the URI and examine
+          // the channel's MIME type.
+          return NewURIFromString(src, aURI);
+        }
+      }
     }
   }
-  return rv;
+
+  return NS_ERROR_DOM_INVALID_STATE_ERR;
 }
 
 void nsHTMLMediaElement::MetadataLoaded()
 {
   mNetworkState = nsIDOMHTMLMediaElement::LOADED_METADATA;
   DispatchAsyncSimpleEvent(NS_LITERAL_STRING("durationchange"));
   DispatchAsyncSimpleEvent(NS_LITERAL_STRING("loadedmetadata"));
   // TODO: Seek to the start time, as set in the start attribute.
@@ -882,16 +1011,20 @@ void nsHTMLMediaElement::UpdateMediaSize
 }
 
 void nsHTMLMediaElement::DestroyContent()
 {
   if (mDecoder) {
     mDecoder->Shutdown();
     mDecoder = nsnull;
   }
+  if (mChannel) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+    mChannel = nsnull;
+  }
   nsGenericHTMLElement::DestroyContent();
 }
 
 void nsHTMLMediaElement::Freeze()
 {
   mPausedBeforeFreeze = mPaused;
   if (!mPaused) {
     Pause();
--- a/content/media/video/test/Makefile.in
+++ b/content/media/video/test/Makefile.in
@@ -40,22 +40,24 @@ srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = content/media/video/test
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = 	test_autoplay.html \
                 test_bug461281.html \
+                test_bug463162.xhtml \
                 test_constants.html \
                 test_controls.html \
                 test_currentTime.html \
                 test_duration1.html \
                 test_ended1.html \
                 test_ended2.html \
+                test_media_selection.html \
                 test_networkState.html \
                 test_paused.html \
                 test_readyState.html \
                 test_seek1.html \
                 test_seek2.html \
                 test_seek3.html \
                 test_seek4.html \
                 test_seek5.html \
new file mode 100644
--- /dev/null
+++ b/content/media/video/test/test_bug463162.xhtml
@@ -0,0 +1,31 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:html="http://www.w3.org/1999/xhtml"
+      xmlns:svg="http://www.w3.org/2000/svg">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=463162
+-->
+<head>
+  <title>Test for Bug 463162</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>        
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=463162">Mozilla Bug 463162</a>
+<audio id='a1'><sauce type="audio/x-wav" src="r11025_s16_c1.wav"/></audio>
+<audio id='a2'><source type="audio/x-wav" src="r11025_s16_c1.wav"/></audio>
+<audio id='a3'><html:source type="audio/x-wav" src="r11025_s16_c1.wav"/></audio>
+<audio id='a4'><svg:source type="audio/x-wav" src="r11025_s16_c1.wav"/></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+ok($("a1").networkState == HTMLMediaElement.EMPTY, "audio a1 with bogus child should not be loading");
+ok($("a2").networkState >= HTMLMediaElement.LOADING, "audio a2 with valid child should be loading");
+ok($("a3").networkState >= HTMLMediaElement.LOADING, "audio a3 with valid child should be loading");
+ok($("a4").networkState == HTMLMediaElement.EMPTY, "audio a4 with bogus child should not be loading");
+]]>
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/media/video/test/test_media_selection.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media test: media selection</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function maketest(expect_load, attach_media, name, type, check_metadata) {
+  return function (testNum) {
+    var e = document.createElement('video');
+
+    if (expect_load) {
+      // this could be loadedmetadata, but needs bug 466410 fixed
+      e.addEventListener('loadedfirstframe', function () {
+          ok(e.networkState >= e.LOADED_METADATA,
+             'test ' +  testNum + ' networkState ' + e.networkState + ' expected >= ' + e.LOADED_METADATA);
+          is(e.currentSrc.substring(e.currentSrc.length - name.length), name, 'test ' + testNum);
+          check_metadata(e);
+          e.parentNode.removeChild(e);
+          runNextTest();
+        }, false);
+    }
+
+    attach_media(e, name, type);
+
+    if (expect_load) {
+      ok(e.networkState >= e.LOADING,
+         'test ' + testNum + ' networkState ' + e.networkState + ' expected >= ' + e.LOADING);
+    } else {
+      ok(e.networkState == e.EMPTY,
+         'test ' + testNum + ' networkState ' + e.networkState + ' expected ' + e.EMPTY);
+      is(e.currentSrc, '', 'test ' + testNum);
+      e.parentNode.removeChild(e);
+      runNextTest();
+    }
+  }
+}
+
+function set_src(element, name, type) {
+  element.src = name;
+  document.body.appendChild(element);
+}
+
+function add_source(element, name, type) {
+  do_add_source(element, name, type);
+  document.body.appendChild(element);
+}
+
+function do_add_source(element, name, type) {
+  var source = document.createElement('source');
+  if (type) {
+    source.type = type;
+  }
+  source.src = name;
+  element.appendChild(source);
+}
+
+function add_sources_last(element, name, type) {
+  do_add_source(element, name, 'unsupported/type');
+  do_add_source(element, name, type);
+  document.body.appendChild(element);
+}
+
+function add_sources_first(element, name, type) {
+  do_add_source(element, name, type);
+  do_add_source(element, name, 'unsupported/type');
+  document.body.appendChild(element);
+}
+
+function late_add_sources_last(element, name, type) {
+  document.body.appendChild(element);
+  do_add_source(element, name, 'unsupported/type');
+  do_add_source(element, name, type);
+}
+
+function late_add_sources_first(element, name, type) {
+  document.body.appendChild(element);
+  do_add_source(element, name, type);
+  do_add_source(element, name, 'unsupported/type');
+}
+
+function check_ogg(e) {
+  ok(e.videoWidth == 320 && e.videoHeight == 240, "video should be 320x240");
+}
+
+function check_wav(e) {
+  ok(e.duration > 0.9 && e.duration < 1.1, "duration should be around 1.0");
+}
+
+var nextTest  = 0;
+var subTests = [
+                maketest(true, set_src, '320x240.ogg', null, check_ogg),
+                maketest(true, add_source, '320x240.ogg', null, check_ogg),
+                maketest(true, add_source, '320x240.ogg', 'application/ogg', check_ogg),
+                maketest(true, add_sources_last, '320x240.ogg', null, check_ogg),
+                maketest(true, add_sources_first, '320x240.ogg', 'application/ogg', check_ogg),
+                maketest(true, set_src, 'r11025_u8_c1.wav', null, check_wav),
+                maketest(true, add_source, 'r11025_u8_c1.wav', null, check_wav),
+                maketest(true, add_source, 'r11025_u8_c1.wav', 'audio/x-wav', check_wav),
+                maketest(true, add_sources_last, 'r11025_u8_c1.wav', null, check_wav),
+                maketest(true, add_sources_first, 'r11025_u8_c1.wav', 'audio/x-wav', check_wav),
+
+                // type hint matches a decoder, actual type matches different decoder
+                maketest(true, add_source, '320x240.ogg', 'audio/x-wav', check_ogg),
+                maketest(true, add_source, 'r11025_u8_c1.wav', 'application/ogg', check_wav),
+
+                // should not start loading, type excludes it from media candiate list
+                maketest(false, add_source, '320x240.ogg', 'bogus/type', null),
+                maketest(false, add_source, 'r11025_u8_c1.wav', 'bogus/type', null),
+                maketest(false, add_source, 'unknown.raw', 'bogus/type', null),
+
+                // should start loading, then fire error, needs bug 462455 fixed
+                // maketest(true, add_source, 'unknown.raw', 'application/ogg', null),
+                // maketest(true, add_source, 'unknown.raw', 'audio/x-wav', null),
+
+                // element doesn't notice source children attached later, needs bug 462455 fixed
+                // maketest(true, late_add_sources_last, '320x240.ogg', null, 0.2, 0.4),
+                // maketest(true, late_add_sources_first, '320x240.ogg', 'application/ogg', 0.2, 0.4),
+                // maketest(true, late_add_sources_last, 'r11025_u8_c1.wav', null, 0.2, 0.4),
+                // maketest(true, late_add_sources_first, 'r11025_u8_c1.wav', 'audio/x-wav', 0.2, 0.4),
+
+                SimpleTest.finish
+                ];
+
+function runNextTest() {
+  setTimeout(function () {
+      dump('subtest ' + nextTest + '\n');
+      subTests[nextTest](nextTest);
+      nextTest += 1;
+    }, 0);
+}
+
+addLoadEvent(runNextTest);
+</script>
+</pre>
+</body>
+</html>