TextTrackList onchange event not working in IE and

2019-02-21 14:18发布

问题:

The TextTrackList.onchange event is not working in IE and Edge. In Chrome and FireFox it works fine.

Is there any alternative I can use? Ive searched through the available events but can't find any.

Or how can I create a workaround? So it works amongst all browsers?

https://www.javascripture.com/TextTrackList

var video = document.getElementById('video');

video.textTracks.addEventListener('change', function () {
  console.log("TextTracks change event fired!");
});
video {
  max-width: 400px;
  max-height: 400px;
}
<video controls id="video">
  <source src="https://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_30mb.mp4" type="video/mp4" />
  <track label="Caption #1" kind="subtitles" srclang="nl" src="path/to/caption1.vtt">
  <track label="Caption #2" kind="subtitles" srclang="en" src="path/to/caption2.vtt">
  <track label="Caption #3" kind="subtitles" srclang="de" src="path/to/caption3.vtt">
</video>

回答1:

You might be able to create a kind of polyfill.

First to detect if we support the event or not, we can check for ('onchange' in window.TextTrackList). So we can integrate our unperfect polyfill conditionally and leave the correct implementations unchanged.

Then, we can iterate every x-time over our TextTrackList's TextTracks in order to find which one is the active one, it should be the one with its mode set to "showing".

Now, we just have to store the previous active track and check if the current one is the same. Otherwise, fire the Event.

So a simple implementation could be

// in an outer scope
// textTracks is our TextTrackList object
var active = getActive();

// start polling
poll();

function poll() {
  // schedule next check in a frame
  requestAnimationFrame(poll);
  var current = getActive();

  if (current !== active) {
    active = current; // update the active one
    // dispatchEvent is not supported on TextTrackList in IE...
    onchange({
      target: textTracks
    });
  }
}

function getActive() {
  for (var i = 0; i < textTracks.length; i++) {
    if (textTracks[i].mode === 'showing') {
      return textTracks[i];
    }
  }
}

But to implement a better polyfill, we will want to override the original addEventListener, removeEventListener and onchange properties of the TextTrackList prototype.

Here is a rough implementation, which will not take care of the third param of [add/remove]EventListener.

(function() {
  /* Tries to implement an 'change' Event on TextTrackList Objects when not implemented */

  if (window.TextTrackList && !('onchange' in window.TextTrackList.prototype)) {
    var textTracksLists = [], // we will store all the TextTrackLists instances
      polling = false; // and run only one polling loop

    var proto = TextTrackList.prototype,

      original_addEvent = proto.addEventListener,
      original_removeEvent = proto.removeEventListener;

    var onchange = {
      get: getonchange,
      set: setonchange
    };

    Object.defineProperty(proto, 'onchange', onchange);
    Object.defineProperty(proto, 'addEventListener', fnGetter(addListener));
    Object.defineProperty(proto, 'removeEventListener', fnGetter(removeListener));

    function fnGetter(fn) {
      return {
        get: function() {
          return fn;
        }
      };
    }


    /* 	When we add a new EventListener, we attach a new object on our instance
    	This object set as '._fakeevent' will hold informations about
    		the current EventListeners
    		the current onchange handler
    		the parent <video> element if any
    		the current activeTrack
    */
    function initFakeEvent(instance) {
      // first try to grab the video element from where we were generated
      // this is useful to not run useless tests when the video is detached
      var vid_elems = document.querySelectorAll('video'),
        vid_elem = null;
      for (var i = 0; i < vid_elems.length; i++) {
        if (vid_elems[i].textTracks === instance) {
          vid_elem = vid_elems[i];
          break;
        }
      }

      textTracksLists.push(instance);
      instance._fakeevent = {
        parentElement: vid_elem,
        listeners: {
          change: []
        }
      }
      if (!polling) { // if we are the first instance being initialised
        polling = true;
        requestAnimationFrame(poll); // start the checks
      }

      return instance._fakeevent;
    }

    function getonchange() {
      var fakeevent = this._fakeevent;
      if (!fakeevent || typeof fakeevent !== 'object') {
        return null;
      }
      return fakeevent.onchange || null;
    }

    function setonchange(fn) {
      var fakeevent = this._fakeevent;
      if (!fakeevent) {
        fakeevent = initFakeEvent(this);
      }
      if (fn === null) fakeevent.onchange = null;
      if (typeof fn !== 'function') return fn;
      return fakeevent.onchange = fn;
    }

    function addListener(type, fn, options) {
      if (type !== 'change') { // we only handle change for now
        return original_addEvent.bind(this)(type, fn, options);
      }
      if (!fn || typeof fn !== 'object' && typeof fn !== 'function') {
        throw new TypeError('Argument 2 of EventTarget.addEventListener is not an object.');
      }
      var fakeevent = this._fakeevent;
      if (!fakeevent) {
        fakeevent = initFakeEvent(this);
      }
      if (typeof fn === 'object') {
        if (typeof fn.handleEvent === 'function') {
          fn = fn.handleEvent;
        } else return;
      }
      // we don't handle options yet...
      if (fakeevent.listeners[type].indexOf(fn) < 0) {
        fakeevent.listeners[type].push(fn);
      }
    }

    function removeListener(type, fn, options) {
      if (type !== 'change') { // we only handle change for now
        return original_removeEvent.call(this, arguments);
      }
      var fakeevent = this._fakeevent;
      if (!fakeevent || !fn || typeof fn !== 'object' && typeof fn !== 'function') {
        return
      }
      if (typeof fn === 'object') {
        if (typeof fn.handleEvent === 'function') {
          fn = fn.handleEvent;
        } else return;
      }
      // we don't handle options yet...
      var index = fakeevent.listeners[type].indexOf(fn);
      if (index > -1) {
        fakeevent.listeners[type].splice(index, 1);
      }
    }


    function poll() {
      requestAnimationFrame(poll);
      textTracksLists.forEach(check);
    }

    function check(instance) {
      var fakeevent = instance._fakeevent;
      // if the parent vid not in screen, we probably have not changed
      if (fakeevent.parentElement && !fakeevent.parentElement.parentElement) {
        return;
      }
      // get the current active track
      var current = getActiveTrack(instance);
      // has changed
      if (current !== fakeevent.active) {
        if (instance.onchange) {
          try {
            instance.onchange({
              type: 'change',
              target: instance
            });
          } catch (e) {}
        }
        fakeevent.listeners.change.forEach(call, this);
      }
      fakeevent.active = current;
    }

    function getActiveTrack(textTracks) {
      for (var i = 0; i < textTracks.length; i++) {
        if (textTracks[i].mode === 'showing') {
          return textTracks[i];
        }
      }
      return null;
    }

    function call(fn) {
      fn({
        type: 'change',
        target: this
      });
    }


  }
})();

var video = document.getElementById('video');

video.textTracks.onchange = function ontrackchange(e) {
  console.log('changed');
};
video {
  max-width: 400px;
  max-height: 400px;
}
<video controls id="video">
  <source src="https://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_30mb.mp4" type="video/mp4" />
   <track label="Caption #1" kind="subtitles" srclang="nl" src="path/to/caption1.vtt">
   <track label="Caption #2" kind="subtitles" srclang="en" src="path/to/caption2.vtt">
   <track label="Caption #3" kind="subtitles" srclang="de" src="path/to/caption3.vtt">
</video>



回答2:

You are correct, it is an issue with IE and Edge. What you can do is listen to an event on the track load. Just pay attention that the track has got to be on the same domain, otherwise, you will get a silent error (CURS) and the event lister will not list the event.

I have created a code pan so you could try it https://codepen.io/shahar-polak/project/live/AnVpEw/

NOTE: The code will trigger one event in IE and Edge and two events on Chrome and Firefox. Make sure you check for the client browser before using in production.

const video = document.getElementById('video');
const tracks = Array.from(document.querySelectorAll('track'));
video.textTracks.addEventListener('change', function() {
  console.log('TextTracks change event fired!');
});
// For IE and Edge
tracks.forEach((track) => {
  track.addEventListener("load", function() {
    console.log('change track');
  }, false);
})
video {
  max-width: 400px;
  max-height: 400px;
}
<video controls id="video" >
    <source src="https://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_30mb.mp4" type="video/mp4"/>
    <track label="Caption #1" kind="subtitles" srclang="nl" src="./en.vtt">
    <track label="Caption #2" kind="subtitles" srclang="en" src="./en.vtt">
    <track label="Caption #3" kind="subtitles" srclang="de" src="https://iandevlin.com/html5/dynamic-track/captions/sintel-en.vtt">
</video>