1

I'm writing a userscript to try to decrease an overly loud audio element. How do I get access to an audio element that has been created, but not placed in the DOM anywhere?

On the site itself, it has something akin to:

function () {
    var audioElement = document.createElement('audio');
    audioElement.setAttribute('src', 'http://www.example.com/sound.mp3');

    callbackFn = function() {
        audioElement.volume = way_too_high;
        audioElement.load();
        audioElement.play();
    };

    //...
}();

In my userscript, I want to do something like

function () {
    newCallbackFunction = function() {
        var audioElement = document.querySelector('audio'); // doesn't work, presumably because never added to DOM
        audioElement.volume = 0.1 * audioElement.volume;
        audioElement.load();
        audioElement.play();
    };

    // ...
}();

I can't seem to get access to it, though. I don't really understand where these objects live. If they're not in the DOM, how can they be played? They just seem to exist in the document somewhere since they can be played, but it is not obvious to me how to find them.

Ayjay
  • 3,413
  • 15
  • 20
  • How soon does the site's Javascript there run? Can you run anything yourself before that function executes? (maybe post a link to the site so we can test solutions ourselves?) – CertainPerformance Feb 17 '19 at 01:47
  • 1
    Audio objects can be created and played without being a part of the DOM hierarchy. In this case, you'll need to track a collection of audio objects manually, rather than rely of document queries to acquire the audio objects. – Dacre Denny Feb 17 '19 at 01:48
  • Given the autoplay policies of browsers recently changed [Autoplay Policy Changes](https://developers.google.com/web/updates/2017/09/autoplay-policy-changes), some user action should call `callbackFn` else the `HTMLMediaElement` should not output audio at all, at any volume. What user action triggers audio playback? – guest271314 Feb 17 '19 at 02:06
  • @CertainPerformance FWIW If all the code is included at the question OP can simply remove and replace the handler at `click` or other user action event that triggers media playback with reference to the `audioElement`. `let a = elementWithClickEventAttached; a.removeEventListener('click', callbackFn); a.addEventListener('click', modifiedCallbackFn /* set volume */)`. – guest271314 Feb 17 '19 at 02:27
  • @CertainPerformance https://raidtime.net/en/game/tool/warframe/alarm is the site I'm writing this for, with the current userscript being https://pastebin.com/vEVLySMv (not currently working, though) – Ayjay Feb 17 '19 at 02:29
  • @Ayjay The first link is crashing tab at Chromium/Chrome and Firefox. Can you answer the above questions? What user action causes media playback? – guest271314 Feb 17 '19 at 02:30
  • @CertainPerformance I'm also interested in a general solution to this problem - it would be nice to know how to edit sounds on a webpage with a userscript without having to get in before they're loaded and muck about with all that. – Ayjay Feb 17 '19 at 02:31
  • @guest271314 the sound is triggered via an AJAX request every 15 seconds I believe, or alternatively a test sound is triggered on the `mouseup` of the volume control slider. – Ayjay Feb 17 '19 at 02:32
  • @Ayjay Then you can remove and replace the `mouseup` event handler as described above, before using the element which acts as a "slider". An AJAX request alone cannot cause media playback. A user action is now required unless the client has specifically disabled new autoplay policies at browsers. – guest271314 Feb 17 '19 at 02:33
  • @guest271314 I'm currently replacing the event handler, it's just that I cannot access the audio element via the DOM since it was never added to it. I'm looking for a way to access audio elements that exist in the webpage but have not been added to the DOM, though it seems like this may not be possible and the only way to do this is to obtain a reference to the audio element at the time it's created. – Ayjay Feb 17 '19 at 02:42

2 Answers2

1

Because the callbackFn is called asynchronously, you can alter HTMLMediaElement.prototype's setter on its volume property, so that changes to the volume on an element with the src you're interested in will result in the original setter being called with a volume 1/10th of what it would be called with by default:

const { prototype } = HTMLMediaElement;
const { set: setter, get: getter } = Object.getOwnPropertyDescriptor(prototype, 'volume');
Object.defineProperty(prototype, 'volume', {
  get() {
    return getter.call(this);
  },
  set(arg) {
    const newArg = this.src.endsWith('/sounds/warframe_alarm.mp3')
    ? arg / 10
    : arg;
    setter.call(this, newArg);
  }
});

Of course, note that this is only a hack for when you can't alter the existing JS of a page, such as with a userscript.

I'm looking for a way to access audio elements that exist in the webpage but have not been added to the DOM

Pretty sure the only way to do something like this would be to use a hack like overwriting document.createElement with code that runs before the page's code that uses createElement runs.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Live demo: https://jsfiddle.net/nx5artfo/ (though it'd be easier to just paste the code into a userscript on OP's page, it does work.) The `.volume` may be set at any time, regardless of when the `.src` is set - this approach completely ignores the `src` part, and only intercepts calls to change the `.volume` – CertainPerformance Feb 17 '19 at 03:24
0

Given the code at the question and the fact that autoplay policy of modern browsers have changed, one you locate which element in the DOM begins media playback after user action you can remove and replace the event handler with a function which sets volume and calls the original handler, for example

<!DOCTYPE html>
<html>

<head>
</head>

<body>
  click
  <script>
    (function() {
      var audioElement = document.createElement('audio');
      audioElement.setAttribute('src', 'https://upload.wikimedia.org/wikipedia/commons/6/6e/Micronesia_National_Anthem.ogg');

      callbackFn = function() {
        audioElement.load();
        audioElement.play();
      };
      document.onclick = callbackFn;

      //...
      function intercept(volume, audioElement) {
        const fn = document.onclick;
        document.onclick = null;
        document.onmouseup = function() {
          audioElement.volume = volume;
          fn();
        };
      }

      intercept(.2, audioElement)
    })();
  </script>
</body>

</html>

plnkr https://plnkr.co/edit/fNlhODuwC5Z0jXchoWBE?p=preview

guest271314
  • 1
  • 15
  • 104
  • 177