Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

JavaScript

Vince Brown
Vince Brown
16,249 Points

How to handle event delegation with media events.

I have created a custom audio player using the HTML audio element, and a object namespace to handle all the methods for the players.

On my site most of the time the players will be there on page load, but in some instances there are audio players that get appended later.

Currently my AudioPlayer object's play and pause method delegate the event to the correct classes so the players that get appended can play and pause fine. However I have some methods that are fired when some of the media events such as "loadedmetadata" and "canplay" fire. These events work fine for players that are on the page at initial load, but do not work for players that get appended to the page later.

So my question is how do I handle event delegation with media events ?

Ex ) I have a updateDuration method that fires "onloadedmetadata" and updates elements with the audio files duration. This does not work with appended elements.

HTML

<div class="audio-player">
  <div class="audio-player-controls">
    <button class="audio-play"><i class="fa fa-play"></i>
      <span>Play</span>
    </button>
    <button class="audio-pause"><i class="fa fa-pause"></i>
      <span>Pause</span>
    </button>
    <span class="audio-currenttime audio-time">00:00</span>
    <progress class="audio-progress" value="0"></progress>
    <span class="audio-duration audio-time">00:00</span>
  </div>
  <audio class="js-audio" preload="metadata" src="song.mp3">
  </audio>
</div>

jQuery

(function(root, $, window, document, undefined) {

  var AudioPlayer = {
    // actual html <audio> elem
    $track      : $( '.audio-player audio' ),

    init: function () {
      // bind events
      this.bind();
    },

    bind: function () {
      this.$track.on( 'loadedmetadata', this.updateDuration );
      this.$track.on( 'canplay', this.getOffset );
      this.$track.on( 'canplay', this.skipTime );
      this.$track.on( 'timeupdate', this.updateTime );
      $( 'body' ).on( 'click', '.audio-play', this.play );
      $( 'body' ).on( 'click', '.audio-pause', this.pause );
    },

    updateDuration: function() {
      console.log( 'working' );
      // Store progress element
      var $progress = $( this ).siblings( '.audio-player-controls')
                               .find( '.audio-progress' );
      // store duration element
      var $duration = $( this ).siblings( '.audio-player-controls')
                               .find( '.audio-duration' );
      // set progress elem max attr to the files duration
      $progress.prop( 'max', Math.floor( this.duration ) );
      // set duration elems text to formatted duration
      $duration.text( AudioPlayer.toHHMMSS( this.duration ) );
    },

    play: function() {
      $el    = $( this ),
      $pause = $el.siblings( '.audio-pause' ),
      $track = $el.parent().siblings( 'audio' );
      // hide play button
      $el.hide();
      // show pause button
      $pause.css( 'display', 'inline-block' );
      // focus play button
      $pause.focus();
      // trigger play of audio file.
      $track.trigger( 'play' );
    },

    pause: function() {
      $el    = $( this ),
      $play = $el.siblings( '.audio-play' ),
      $track = $el.parent().siblings( 'audio' );
      // hide pause button
      $el.hide();
      // Show play button
      $play.css( 'display', 'inline-block' );
      // focus play button
      $play.focus();
      // pause audio file.
      $track.trigger( 'pause' );
    },

    updateTime: function() {
      // store progress elem
      var $progress = $( this ).siblings( '.audio-player-controls')
                               .find( '.audio-progress' );
      // store current time elem
      var $currentTime = $( this ).siblings( '.audio-player-controls')
                                  .find( '.audio-currenttime' );
      // Continually Update progress value with the current time
      $progress.prop( 'value', this.currentTime );
      // Continually Update Current time with formatted play time.
      $currentTime.text( AudioPlayer.toHHMMSS( this.currentTime ) );
    },

    skipTime: function( audio ,offset ) {
      // get offset of clicked  progress bar and multiply by audio file duration.
      // Set the audio tracks current time
      audio.currentTime = Math.floor( audio.duration ) * ( offset );
    },

    toHHMMSS: function( totalsecs ) {
      var sec_num = parseInt(totalsecs, 10),
          hours = Math.floor(sec_num / 3600),
          minutes = Math.floor((sec_num - (hours * 3600)) / 60),
          seconds = sec_num - (hours * 3600) - (minutes * 60);

      if ( hours < 10 ) {
        hours = "0" + hours;
      }
      if ( minutes < 10 ) {
        minutes = "0" + minutes;
      }
      if ( seconds < 10 ) {
        seconds = "0" + seconds;
      }

      var time = minutes + ':' + seconds;
      return time;
    },

    getOffset: function() {
      // store progress element
      var $progress = $( this ).siblings( '.audio-player-controls')
                               .find( '.audio-progress' );
      // store this ( audio file ) for access in skip time
      var that = this;
      // on click of progress bar get offset and run skip time method
      $progress.on( 'click' , function( e ) {
        var offset = e.offsetX / e.target.offsetWidth;
        AudioPlayer.skipTime( that ,offset );
      });
    },

  };

Attempt to Solve

I have tried delegating from the body to the audio element, Like I was able to with the play and pause methods but this is not working.

ex)

$( 'body' ).on( 'timeupdate', 'audio', this.updateTime );

Any help would be greatly appreciated Cheers,

Vince

jack AM
jack AM
2,618 Points

Just curious, since you're creating custom events, where does 'loadedmetadata' and 'canplay' actually get triggered?

I see the $track.trigger('play') and $track.trigger('pause') in their respective areas. Which would explain the consistency of the two you mentioned as you dynamically load your content, but not the others.

Vince Brown
Vince Brown
16,249 Points

jack AM , They are not custom events but rather the media events that are handled by the browser when you embed audio or video Tags in your markup.

https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events

After looking into it more, these media events don't bubble like a click event , which makes sense because "canplay" and "loadedmetadata" wouldn't make sense on any other element besides the media elements themselves.

So have kind of hit a road block looking into a way I could access these events for media that gets appended to the page.

jack AM
jack AM
2,618 Points

I gotcha. Thanks for the info. I'm intrigued myself.

jack AM
jack AM
2,618 Points

Have you seen this by chance? http://stackoverflow.com/questions/10235919/the-canplay-canplaythrough-events-for-an-html5-video-are-not-called-on-firefox

What if your callback is firing before the dynamic content has finished loaded? It mentions checking the readystate of the media element before calling back.

Also.. https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState

Vince Brown
Vince Brown
16,249 Points

jack AM ,

I did a lot of stack overflow searching last night but did not come across that one , Thank you!

Going to play around with trying to use readystate I will let you know if I come up with a solution that works.