Hassan Baig
Hassan Baig

Reputation: 15834

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.

Upvotes: 1

Views: 77

Answers (1)

Hassan Baig
Hassan Baig

Reputation: 15834

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.

Upvotes: 1

Related Questions