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>
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>
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>