Mute audio created in iframe with Audio object - javascript

I am working with an iframe that contains code that we receive from a third party. This third party code contains a Canvas and contains a game created using Phaser.
I am looking for a way to mute the sound that this game does at some point.
We usually do it this way:
function mute(node) {
// search for audio elements within the iframe
// for each audio element,(video, audio) attempt to mute it
const videoEls = node.getElementsByTagName('video');
for (let i = 0; i < videoEls.length; i += 1) {
videoEls[i].muted = true;
}
const audioEls = node.getElementsByTagName('audio');
for (let j = 0; j < audioEls.length; j += 1) {
audioEls[j].muted = true;
}
}
After some research I found out that you can play sound in a web page using new Audio([url]) and then call the play method on the created object.
The issue with the mute function that we use is that, if the sound is created with new Audio([url]), it does not pick it up.
Is there a way from the container to list all the Audio elements that have been created within a document or is it just impossible, and that creates a way to play audio without the possibility for iframe container to mute it?

No, there is no way.
Not only can they use non appended <audio> elements like you guessed, but they can also use the Web Audio API (which I think phaser does) and for neither you have a way of accessing it from outside if they didn't expose such an option.
Your best move would be to ask the developer of this game that it exposes an API where you would be able to control this.
For instance, it could be some query-parameter in the URL ( https://thegame.url?muted=true) or even an API based on the Message API, where you'd be able to do iframe.contentWindow.postMessage({muted: true}) from your own page.

Related

Why does setSinkId does not work with BiquadFilters?

I'm trying to make music player with live ability to change output device (headphones or speakers).
I have working function to change destination device with setSinkId.
I also have working Biquad Filters (low pass, high pass...) and audio processor to generate gain level bars (image below).
Filter sliders and gain bars
Some of the code (it's a lot).
Setting output device:
if (audiooutput_ch1active == 0) {
if (typeof audiooutput_headphonesid !== "undefined") {
audiooutput_ch1active = 1;
await audio_ch1.setSinkId(audiooutput_headphonesid);
return;
}
} else if (audiooutput_ch1active == 1) {
if (typeof audiooutput_speakersid !== "undefined") {
audiooutput_ch1active = 0;
await audio_ch1.setSinkId(audiooutput_speakersid);
return;
}
}
Defining filters:
var filter_ch1_lowpass = audioCtx_ch1.createBiquadFilter();
filter_ch1_lowpass.type = "lowpass";
filter_ch1_lowpass.frequency.value = 12000;
var filter_ch1_highpass = audioCtx_ch1.createBiquadFilter();
filter_ch1_highpass.type = "highpass";
filter_ch1_highpass.frequency.value = 0;
var filter_ch1_lowshelf = audioCtx_ch1.createBiquadFilter();
filter_ch1_lowshelf.type = "lowshelf";
filter_ch1_lowshelf.frequency.value = 100;
filter_ch1_lowshelf.gain.value = 0;
Connecting filters and processor:
audio_ch1.src = path;
source_ch1 = audioCtx_ch1.createMediaElementSource(audio_ch1);
source_ch1.connect(filter_ch1_lowpass);
filter_ch1_lowpass.connect(filter_ch1_highpass);
filter_ch1_highpass.connect(filter_ch1_lowshelf);
filter_ch1_lowshelf.connect(processor_ch1);
filter_ch1_lowshelf.connect(audioCtx_ch1.destination);
processor_ch1.connect(filter_ch1_lowshelf);
When I connect filters to my audio context, I can't use setSinkId - Error: Uncaught (in promise) DOMException: The operation could not be performed and was aborted
When I skip code that connects filters, setSinkId works fine.
Does setSinkId not support audio context filters?
I'm new to JavaScript audio.
You are setting the sink of an <audio> element after you did route that <audio>'s output to your audio context. The <audio> already lost the control over its output, it's now the audio context that has control over it, you thus can't set the sink of the <audio> element anymore.
That you can call this method when not using the filter is part of large Chrome bug in the Web Audio API which I believe they're actively working on: basically, they really create the connections only when the audio graph gets connected to a destination.
You could avoid that error to be thrown by creating a MediaStream from your <audio>, by calling its captureStream() method, and then connecting a MediaStreamAudioSourceNode made from that MediaStream object to your audio graph. However you'd still only be setting the sink of the input <audio>, and not of the one you've been modifying in your audio context.
So instead, what you probably want is to set the sink of the audio context directly.
There is an active proposal to add a setSinkId() method on the AudioContext interface, latest Chrome Canary does expose it. However it's still a proposal, and AFAICT only Canary exposes it. So in a near future you'll just have to do
audioCtx_ch1.setSinkId(audiooutput_speakersid).catch(handlePossibleError);
but for now, you'll need to do some gymnastic by first creating a MediaStreamAudioDestinationNode where you'll connect your audio graph, set its MediaStream as the srcObject of another <audio> element on which you'll call setSinkId().
udio_ch1.src = path;
// might be better to use createMediaStreamSource(audio_ch1.captureStream()) anyway here
source_ch1 = audioCtx_ch1.createMediaElementSource(audio_ch1);
source_ch1.connect(filter_ch1_lowpass);
filter_ch1_lowpass.connect(filter_ch1_highpass);
filter_ch1_highpass.connect(filter_ch1_lowshelf);
filter_ch1_lowshelf.connect(processor_ch1);
processor_ch1.connect(filter_ch1_lowshelf);
const outputNode = audioCtx_ch1.createMediaStreamDestination();
filter_ch1_lowshelf.connect(outputNode);
const outputElem = new Audio();
outputElem.srcObject = outputNode.stream;
outputElem.setSinkId(audiooutput_speakersid);
outputElem.play();

Creating and removing `<audio>` tags via javascript (possible scope issue)

I am a neophyte JS developer with a past in server-side programming.
I am creating a simple web app that allows various users to engage in live audio chatting with one another. Whenever a new user logs into an audio chat room, the following ensures they can hear everyone talking
// plays remote streams
async function playStreams(streamList) {
await Promise.all(streamList.map(async (item, index) => {
// add an audio streaming unit, and play it
var audio = document.createElement('audio');
audio.addEventListener("loadeddata", function() {
audio.play();
});
audio.srcObject = item.remoteStream;
audio.id = 'audio-stream-'+item.streamID;
audio.muted = false;
}));
}
Essentially I pass a list of streams into that function and play all of them.
Now if a user leaves the environment, I feel the prudent thing to do is to destroy their <audio> element.
To achieve that, I tried
function stopStreams(streamList) {
streamList.forEach(function (item, index) {
let stream_id = item.streamID;
let audio_elem = document.getElementById('audio-stream-'+stream_id);
if (audio_elem) {
audio_elem.stop();
}
});
}
Unfortunately, audio_elem is always null in the function above. It is not that the streamIDs are mismatched - I have checked them.
Maybe this issue has to do with scoping? I am guessing the <audio> elements created within playStreams are scoped within that function, and thus stopStreams is unable to access them.
I need a domain expert to clarify whether this is actually the case. Moreover, I also need a solution regarding how to better handle this situation - one that cleans up successfully after itself.
p.s. a similar SO question came close to asking the same thing. But their case was not numerous <audio> elements being dynamically created and destroyed as users come and go. I do not know how to use that answer to solve my issue. My concepts are unclear.
I created a global dictionary like so -
const liveStreams = {};
Next, when I play live streams, I save all the <audio> elements in the aforementioned global dictionary -
// plays remote streams
async function playStreams(streamList) {
await Promise.all(streamList.map(async (item, index) => {
// add an audio streaming unit, and play it
var audio = document.createElement('audio');
audio.addEventListener("loadeddata", function() {
audio.play();
});
audio.srcObject = item.remoteStream;
audio.muted = false;
// log the audio object in a global dictionary
liveStreams[stream_id] = audio;
}));
}
I destroy the streams via accessing them from the liveStreams dictionary, like so -
function stopStreams(streamList) {
streamList.forEach(function (item, index) {
let stream_id = item.streamID;
// Check if liveStreams contains the audio element associated to stream_id
if (liveStreams.hasOwnProperty(stream_id)) {
let audio_elem = liveStreams[stream_id];
// Stop the playback
audio_elem.pause();// now the object becomes subject to garbage collection.
// Remove audio obj's ref from dictionary
delete liveStreams.stream_id;
}
});
}
And that does it.

Youtube iframe API with javascript tabs

I'm building an online 'TV' which will use YouTube live-streams for multiple channels.
The channels are contained within tabs. The videos need to be stopped when changing tabs otherwise you can hear the audio in the background.
Here's a link to the JSFiddle: https://jsfiddle.net/matlow/08k4csuh/
I've managed to turn the 'Channel 1' off when changing to another channel with:
var iframe = document.getElementsByClassName("tvscreen")[0].contentWindow;
and
iframe.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
In the tab javascript for loop which also handles the tabcontent[i].style.display = "none";
I think I need to use the for loop to call each instance of the iframe... but I'm quite new to javascript so I'm not quite sure how to achieve this.
It will also help to use iframe.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); so the video plays automatically again when clicking on the relevant tab... but again I'm not quite sure how to implement this.
I've been working on this for a few days so if anyone had any tips or pointers I would really appreciate it!
Thanks for reading! :)
You are not using YouTube's API properly. See https://developers.google.com/youtube/iframe_api_reference
In your fiddle, programmatic play is not possible, because you can't know when the YouTube player is ready, as you are not the one initialising it. Your attempts to play the video might take place too early.
Programmatic pause (you managed to pause the first video) is possible thanks to enablejsapi=1 in the iframe src and the fact that the player is ready at that point.
Here's a fork of your fiddle - https://jsfiddle.net/raven0us/ancr2fgz
I added a couple of comments. Check those out.
// load YouTube iframe API as soon as possible, taken from their docs
var tag = document.createElement('script');
tag.id = 'iframe-demo';
tag.src = 'https://www.youtube.com/iframe_api';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// initialised players are kept here so we don't have to destroy and reinit
var ytPlayers = {};
function mountChannel(channel) {
var player;
var iframeContainer = document.querySelectorAll('#' + channel + ' iframe');
// if the channel & iframe we want to "mount" exist, check for playing iframes before doing anything else
if (iframeContainer.length > 0) {
// Object.keys() is ECMA 5+, sorry about this, but no easy to check if an object is empty
// alternatively, you could have an array, but in that case, you won't be able to fetch a specific player as fast
// if you don't need that functionality, array is as good cause you will just loop through active players and destroy them
var activePlayersKeys = Object.keys(ytPlayers);
if (activePlayersKeys.length > 0) { // if players exist in the pool, destroy them
for (var i = 0; i < activePlayersKeys.length; i++) {
var activeChannel = activePlayersKeys[i];
var activePlayer = ytPlayers[activeChannel];
activePlayer.getIframe().classList.remove('playing'); // mark pause accordingly, by removing class, not necessary
activePlayer.pauseVideo();
}
}
// check if player already initialised and if player exists, check if it has resumeVideo as a function
if (ytPlayers.hasOwnProperty(channel)) {
ytPlayers[channel].playVideo();
} else {
var iframe = iframeContainer[0];
player = new YT.Player(iframe, {
events: {
'onReady': function (event) {
// event.target is the YT player
// get the actual DOM node iframe nad mark it as playing via a class, styling purposes, not necessary
event.target.getIframe().classList.add('playing');
// play the video
event.target.playVideo();
// video may not autoplay all the time in Chrome, despite its state being cued and this event getting triggered, this happens due to a lot of factors
},
// you should also implement `onStateChange` in order to track video state (as a result of user actions directly via YouTube controls) - https://developers.google.com/youtube/iframe_api_reference#Events
}
});
// append to the list
ytPlayers[channel] = player;
}
}
}
// Get the element with id="defaultOpen" and click on it
function onYouTubeIframeAPIReady() {
// YouTube API will call this when it's ready, only then attempt to "mount" the initial channel
document.getElementById("defaultOpen").click();
}
This is the first time I worked with YouTube's iframe API, but it seems reasonable.

how to share audio element among several pages in javascript

I have a website with an index page which only:
contains an iFrame
launches an audio player and starting to play tracks.
The different links are open in the iFrame, so I hide everything under index.html.
I create the audio player via the line:
var audio = document.createElement('audio");
I am confused with the scoping. I want to access this audio variable from other documents which will open in the iFrame.
Is there a way to store this audio element so any page can access it? Or is there a function allowing to retrieve it by type, like getElement('audio') ?
My only goal is start playing music on one page, and be able to control it from another (to be able to call audio.play() and audio.pause() from another.
You can do this by accessing the parent document context and getting the audio element from there.
In the parent
// Method 1
var audio = document.createElement('audio')
audio.id = 'my-specific-audio'
document.body.appendChild(audio)
// Method 2
window['my-audio-key'] = audio
In the child
// Method 1
var audio = window.parent.document.getElementById('my-specific-audio')
// Method 2
var audio = window.parent['my-audio-key']
One caveat: both frames must be of the same origin (domain). The browser does not allow cross-origin context access.
If you are using frames and you would like to control a single element, then you can declare the element and access it using parent. object. So, you have the:
var audio = document.createElement('audio");
Above code in the index.htm and in each of the page, you can use:
parent.audio.play();
parent.audio.pause();
Note: This works only if both the frames are from the same domain.

mediaElementjs: how to get instance of the player

I'm stuck with a little problem with MediaElement.js player.
To get the instance of the player, I do this (works with html5 compatible browser):
// Get player
this.playerId = $('div#shotlist-player video').attr('id');
this.player = window[this.playerId];
But it's not working as soon as it fallback in flash. In fact, it's not working because I'm not calling an instance of MediaElement itself. But I don't see how I can call it.
The player is created with
$('video').mediaelementplayer({....});
How can I get the mediaelement object?
------------EDIT----------------
Ok I finally found how to make it works:
// Get player
mePlayer = $('div#shotlist-player video.video-js')[0];
this.player = new MediaElementPlayer(mePlayer);
Now I can user mediaElement instance correctly.
This post is a lot of speculation, but may be correct. Docs are lacking (;
The answer by sidonaldson is perfectly acceptable if you wish to create a new MediaElement instance and get a handle on it. If there's one already present, it seems to try to reinitialize another instance on that element and freaks out.
I am pretty sure mediaelement.js augments the builtin HTML5 controls by providing a JavaScript API to manipulate Flash/Silverlight players via those elements. I may be wrong, but other advice I've seen on this issue in multiple places is to do something like:
$playButton.click(function() {
$('video, audio').each(function() {
$(this)[0].player.play();
});
});
To create a play button as an external DOM element which will fire off all players on the page. This indicates to me that the implementation is something like I've described.
Try:
var player = $('video').mediaelementplayer({
success: function (me) {
me.play();
}
});
// then you can use player.id to return the id
// or player.play();

Categories