2

I'm developing a WebExtension that uses the createMediaElementSource function.

The problem is that this operation can fail, and when it does, it does so without throwing an error. It simply returns an audio node that produces no output, and displays the following warning in the browser console:

The HTMLMediaElement passed to createMediaElementSource has a cross-origin resource, the node will output silence.

Further, the affected <audio>/<video> element will no longer output any sound.


The following snippet demonstrates the problem - as soon as the "Create audio node" button is pressed, the audio becomes permanently muted.

function createAudioNode() {
  const audioElement = document.querySelector('audio')
  const audioContext = new AudioContext()
  const audioNode = audioContext.createMediaElementSource(audioElement)
  audioNode.connect(audioContext.destination)
}
<audio controls src="https://upload.wikimedia.org/wikipedia/commons/4/40/Toreador_song_cleaned.ogg">
  Your browser does not support the <code>audio</code> element.
</audio>
<br>
<button onclick="createAudioNode()">Create audio node</button>

This is an unacceptable user experience - if something goes wrong, I want to display an error message, not just silently (literally) break the media playback.


So, my question is: How can I prevent this from happening? I can think of two ways to handle this:

  • Predict that createMediaElementSource will fail, and not execute it at all.
  • Detect that createMediaElementSource has failed, and undo it.

Is either one of these possible? Or is this simply not doable with the current Web Audio API?

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • In case it matters, my extension has the `` permission, which - as far as I know - *should* make it exempt from cross-origin restrictions. – Aran-Fey Sep 18 '22 at 14:30
  • Try to think out of the box. Instead of fighting with `createMediaElementSource`, I would try to check directly informations of the audio source to know whether or not it's conform to web audio api and predict a potential failure. – KeitelDOG Sep 22 '22 at 21:14
  • Many people seem to be misunderstanding my goal. Please don't give me tips how to maximize the chances of `createMediaElementSource` working. I'm not looking for ways to increase my odds of winning the lottery, I'm looking for a way to predict whether I will win the lottery or not. It's about knowledge. – Aran-Fey Sep 23 '22 at 06:41
  • But of course, if you can find a way to make me win the lottery every time, that also works. – Aran-Fey Sep 23 '22 at 06:49

3 Answers3

1

I tried to take a detour and attach using a stream of the audio element. Also suggested here. One of my ideas was also to connect it to oscillator to detect if sound is muted.

But it threw an Exception already when trying to capture the stream. But the audio played on so this is my solution. It is my understanding that createMediaElementSource will not work for same reason that createMediaStreamSource which is the same reason that MediaElement.captureStream didn't work.

function createAudioNode() {
  const audioElement = document.querySelector('audio')
  const audioContext = new AudioContext()

  try {
    var stream = audioElement.captureStream()
  } catch (ex) {
    console.log("createMediaElementSource() will not work because " + ex.message)
    return
  }


  const audioNode = audioContext.createMediaElementSource(audioElement)
  audioNode.connect(audioContext.destination)
}
<audio controls src="https://upload.wikimedia.org/wikipedia/commons/4/40/Toreador_song_cleaned.ogg">
  Your browser does not support the <code>audio</code> element.
</audio>
<br>
<button onclick="createAudioNode()">Create audio node</button>
IT goldman
  • 14,885
  • 2
  • 14
  • 28
0

The error message I get in my console when trying to run your example shows that the example fails due to cross-origin resource sharing permissions:

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a "preflight" request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.

The server can permit such resource loading via the Access-Control-Allow-Origin header.

This GitHub issue on the web-audio-api repo looks very relevant: A way to a) detect if MediaElementAudioSourceNode is CORS-restricted & b) revert createMediaElementSource

Within that issue thread, there is a comment saying:

To check if the HTMLAudioElement is a cross-origin resource served without Access-Control-Allow-Origin header listing the origin requested from is to set the crossorigin attribute on the element then observe both loadedmetadata (fired when the header is set) and error (fired when the header is not set) events.

See the full comment for a code snippet that prints those errors to the console.

Also possibly related: Firefox WebAudio createMediaElementSource not working


To check if the resource can be fetched, you can send a fetch request for the src of the media element, and use the success and failure callbacks of the Promise.then method.

function createAudioNode() {
  const audioElement = document.querySelector('audio')
  fetch(audioElement.src, { method: "GET", mode: "cors" }).then(
    success => {
      const audioContext = new AudioContext()
      const audioNode = audioContext.createMediaElementSource(audioElement)
      audioNode.connect(audioContext.destination)
    },
    failure => console.log(failure)
  );
}

So if you are intent on not adding a crossorigin attribute to the audio element, you can detect whether createMediaElementSource by checking that the fetch succeeds (ie. the server supports CORS), and that the crossorigin attribute exists.

I don't know whether doing an extra fetch results in consuming network resources on the client side. I presume that caching (if configured) could prevent wasteful network resource consumption.

starball
  • 20,030
  • 7
  • 43
  • 238
  • Thanks, that github issue is definitely relevant. Unfortunately though, I can't find any valuable information in there. I may be misunderstanding something, but the `loadedmetadata` thing seems to be a red herring - the audio loads just fine after all; it's only the `createMediaElementSource` that fails. – Aran-Fey Sep 18 '22 at 22:18
  • About that answer suggesting to set `crossorigin="anonymous"`: That's a workaround that *might* help bypass the crossorigin restriction, but it's not an answer to my question. I'm looking for a way to predict or undo the problem; I don't want to blindly change some metadata in the hopes that it will make things better. I want to operate on the assumption that the website was built this way for a reason; not arbitrarily switch things around. – Aran-Fey Sep 18 '22 at 22:22
  • I think you need to use `crossorigin="anonymous"` regardless. The response headers in your example contain the `access-control-allow-origin: *`, and the example is getting an error that goes away if `crossorigin="anonymous"` is added. – starball Sep 18 '22 at 23:26
  • I updated the answer to provide a possible way to check if the fetch will fail (due to CORS or other reasons) ahead of time. – starball Sep 19 '22 at 03:36
  • Hmm, the code you posted doesn't work for me. The `fetch` succeeds and the audio becomes muted. [Here's](https://jsfiddle.net/8ayq4j2r/) a jsfiddle. – Aran-Fey Sep 19 '22 at 07:51
  • To clarify my stance on `crossorigin="anonymous"`: I'm not opposed to using it. But my question is how to predict or undo the failure of `createMediaElementSource`. Setting `crossorigin` does neither of those things. It can sometimes prevent the failure, sure, but that's not what the question is about. – Aran-Fey Sep 19 '22 at 22:40
  • If the fetch succeeds the problem isn't that the server isn't supporting CORS. If I add a `crossorigin` attribute to the audio element in your JSFiddle, `createMediaElementSource` succeeds. The error in this scenario (where the server supports CORS) seems to be due to user-agent behaviour, and so far, the presence of `crossorigin` is the only thing I've found to make a difference. In that case, I think you need two checks: one fetch request to see if the server supports CORS, and one check to see if the audio element has the `crossorigin` attribute present. – starball Sep 22 '22 at 18:54
0

According to audio element https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#attr-crossorigin , not setting crossorigin attribute could limit the usage of audio cors.

This enumerated attribute indicates whether to use CORS to fetch the related audio file. CORS-enabled resources can be reused in the element without being tainted. The allowed values are:

anonymous Sends a cross-origin request without a credential. In other words, it sends the Origin: HTTP header without a cookie, X.509 certificate, or performing HTTP Basic authentication. If the server does not give credentials to the origin site (by not setting the Access-Control-Allow-Origin: HTTP header), the image will be tainted, and its usage restricted.

use-credentials Sends a cross-origin request with a credential. In other words, it sends the Origin: HTTP header with a cookie, a certificate, or performing HTTP Basic authentication. If the server does not give credentials to the origin site (through Access-Control-Allow-Credentials: HTTP header), the image will be tainted and its usage restricted.

When not present, the resource is fetched without a CORS request (i.e. without sending the Origin: HTTP header), preventing its non-tainted used in elements. If invalid, it is handled as if the enumerated keyword anonymous was used. See CORS settings attributes for additional information.

So try to add crossorigin="anonymous" or as you wish to tell explicitly to use the cors.

function createAudioNode() {
  const audioElement = document.querySelector('audio')
  const audioContext = new AudioContext()
  const audioNode = audioContext.createMediaElementSource(audioElement)
  audioNode.connect(audioContext.destination)
}
<audio crossorigin="anonymous" controls src="https://upload.wikimedia.org/wikipedia/commons/4/40/Toreador_song_cleaned.ogg">
  Your browser does not support the <code>audio</code> element.
</audio>
<br>
<button onclick="createAudioNode()">Create audio node</button>
KeitelDOG
  • 4,750
  • 4
  • 18
  • 33
  • This doesn't solve my problem though. Let me give you an analogy. I'm a blindfolded hunter trying to shoot a deer. If I pull the trigger, I have no idea if my shot hit or missed. Maybe the deer will run away, or maybe it will fall to the ground dead. You're giving me advice how to maximize my chances of hitting the deer. But what I want is a way to tell whether I've hit the deer or not. My goal isn't murder; it's knowledge. – Aran-Fey Sep 23 '22 at 06:38
  • Yes I know, since you also mention cross origin problem, and I saw that your example did not have it and gave me the same error. Adding crossorigin attribute allowed the use of it. But if you are unsure that the Audio response will allow cors for anyone, you can fetch directly part of the resource to get headers and read the header that allows cross origin usage, and then if header is not present or is not * of GET, then you can be sure the error will be raise further. By combining the check request with the crossorigin attribute, it will work for sure. – KeitelDOG Sep 23 '22 at 20:43
  • @Aran-Fey can you share an audio url that you're sure will have the error? – KeitelDOG Sep 23 '22 at 20:45
  • What error do you mean? An example where `createMediaElementSource` fails is already included in the question. If you're looking for an audio file that doesn't load if you set `crossorigin="anonymous"`, unfortunately I don't know one. – Aran-Fey Sep 23 '22 at 20:52
  • It's because cors is not controlled by client, but server. I have a website that serve audio files with Laravel PHP, and if I don't setup CORS to allow anyone to use it in browsers, well, browser won't allow to use it, even if you set crossorigin attribute on tag, or cors option in request. There is request to check headers of files and see if the server set it up to allow any browser to use it. – KeitelDOG Sep 23 '22 at 20:56
  • To be honest, I didn't answer for upvotes or scores. But because I own a website that serve some songs and LRC lyrics, and I know how annoying it can be when serving those contents to browsers and Apps. – KeitelDOG Sep 23 '22 at 21:01