muxinc / media-chrome

Custom elements (web components) for making audio and video player controls that look great in your website or app.
https://media-chrome.org
MIT License
1.83k stars 76 forks source link

Doesn't recognize media when elements are created dynamically in DOM #940

Open Patronics opened 4 months ago

Patronics commented 4 months ago

if either the media-control-bar or media-controller are created dynamically with JS, such as with the code

var trackNumber = 1
const controlBarHTML = `<media-control-bar mediacontroller='mediaControllerForTrack${trackNumber}'>
              <media-play-button></media-play-button>
              <media-mute-button></media-mute-button>
              <media-time-display showduration></media-time-display>
              <media-volume-range></media-volume-range>
              <media-time-range></media-time-range>
              <media-pip-button></media-pip-button>
              <media-fullscreen-button></media-fullscreen-button>
            </media-control-bar>`*/
          document.getElementById("media-container").InsertAdjacentHTML('beforeend',controlBarHTML)

Both the media-controller and the media-control-bar are created, but they're unable to connect to each other. This results in very cumbersome use when handling multiple tracks that need to be independently controlled. The only workaround I've found so far is prepopulating the HTML with a bunch of identical elements in a hidden div, where each has a unique ID, and they are moved into place with the Javascript.

Also worth noting, the <audio> tag I'm applying the media controller to is in a shadow-dom, so that may be a factor in this issue. Regardless, my workaround is functional, although impractical to scale

Patronics commented 4 months ago

If the issue is related to the shadow dom, this pull request's commit might help resolve it?

heff commented 4 months ago

In the example code you have, it's using media-container, which is like media-controller but without the state management, and would result in the the problem you're describing. Is that the issue?

Otherwise, any chance you can make a code sandbox/code pen that demonstrates this?

Patronics commented 4 months ago

In the example code you have, it's using media-container, which is like media-controller but without the state management, and would result in the the problem you're describing. Is that the issue?

thanks for the suggestion, but in this case media-container was actually just a generic name for the container I was inserting elements into, as a generic example (in my actual code it has a different name, "waveform-container"). I didn't include the media-controller code in the example because it works when the media-control-bar is not created programmatically but instead preexisting in the html source, and the method of inserting it with my media is somewhat convoluted (the media element itself is inside an open shadow-dom from another project).

I can try to look into creating a more minimal demonstration template.

heff commented 4 months ago

Ok cool, that'd be really helpful. This could be in the depths of the element association logic so will be hard to debug without an example.

Patronics commented 4 months ago

Okay, here's a pair of example source code files that demonstrate the issue. The layout of elements produced is identical, the only difference is how they're created, and whether the end-result works. The setup assumes a file named 'demoSong.mp3' in the same directory as the HTML source file.

Working version:

<html>
<body>
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome@3/+esm"></script>
<media-controller id='mediaController' audio></media-controller>
<div id="waveform">
  <!-- the waveform will be rendered here -->
</div>
<div id="new-container">
<media-control-bar mediacontroller='mediaController'>
    <media-play-button></media-play-button>
    <media-mute-button></media-mute-button>
    <media-time-display showduration></media-time-display>
    <media-volume-range></media-volume-range>
    <media-time-range></media-time-range>
    <media-pip-button></media-pip-button>
    <media-fullscreen-button></media-fullscreen-button>
  </media-control-bar>
</div>

<script type="module">

import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'

const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  url: './demoSong.mp3',
  mediaControls: false,
})
//this section is important, this allows media-chrome to recognize the wavesurfer instance, by attaching to the <audio> tag
var mediaShadowRoot=document.getElementById('waveform').getElementsByTagName('div')[0].shadowRoot;
var shadowAudioNode = mediaShadowRoot.childNodes[5]
shadowAudioNode.setAttribute('slot','media');
mediaShadowRoot.appendChild(document.getElementById('mediaController'))
mediaShadowRoot.getElementById('mediaController').appendChild(shadowAudioNode)

wavesurfer.on('interaction', () => {
  wavesurfer.play()
})
</script>
</body>

</html>

Broken Version:

<html>
<body>
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome@3/+esm"></script>
<media-controller id='mediaController' audio></media-controller>
<div id="waveform">
  <!-- the waveform will be rendered here -->
</div>
<div id="new-container">

</div>

<script type="module">

import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'

const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  url: './demoSong.mp3',
  mediaControls: false,
})
//this section is important, this allows media-chrome to recognize the wavesurfer instance, by attaching to the <audio> tag
var mediaShadowRoot=document.getElementById('waveform').getElementsByTagName('div')[0].shadowRoot;
var shadowAudioNode = mediaShadowRoot.childNodes[5]
shadowAudioNode.setAttribute('slot','media');
mediaShadowRoot.appendChild(document.getElementById('mediaController'))
mediaShadowRoot.getElementById('mediaController').appendChild(shadowAudioNode)
//this section should work like in the previous example, but doesn't
const controlBarHTML = `<media-control-bar mediacontroller='mediaController'>
    <media-play-button></media-play-button>
    <media-mute-button></media-mute-button>
    <media-time-display showduration></media-time-display>
    <media-volume-range></media-volume-range>
    <media-time-range></media-time-range>
    <media-pip-button></media-pip-button>
    <media-fullscreen-button></media-fullscreen-button>
  </media-control-bar>`
document.getElementById("new-container").insertAdjacentHTML('beforeend',controlBarHTML)

wavesurfer.on('interaction', () => {
  wavesurfer.play()
})
</script>
</body>

</html>

Also worth noting that the same problematic behavior is observed if the media-controller element is created such as with insertAdjacentHTML or the following construction example rather than moved into place with AppendChild, as it is in the above examples.

var mediaController = document.createElement("media-controller");
         mediaController.id = `mediaController`
         mediaController.setAttribute("audio","")
         mediaShadowRoot.appendChild(mediaController)
         mediaShadowRoot.getElementById(`mediaController`).appendChild(shadowAudioNode)