View file mediaelement-mediaelement-6d66563/src/js/features/tracks.js

File size: 26.99Kb
'use strict';

import document from 'global/document';
import mejs from '../core/mejs';
import i18n from '../core/i18n';
import {config} from '../player';
import MediaElementPlayer from '../player';
import {convertSMPTEtoSeconds} from '../utils/time';
import {isString, createEvent} from '../utils/general';
import {addClass, removeClass, hasClass, siblings, ajax, fadeIn, fadeOut, visible} from '../utils/dom';

/**
 * Closed Captions (CC) button
 *
 * This feature enables the displaying of a CC button in the control bar, and also contains the methods to start media
 * with a certain language (if available), toggle captions, etc.
 */

// Feature configuration
Object.assign(config, {
	/**
	 * Default language to start media using ISO 639-2 Language Code List (en, es, it, etc.)
	 * If there are multiple tracks for one language, the last track node found is activated
	 * @see https://www.loc.gov/standards/iso639-2/php/code_list.php
	 * @type {String}
	 */
	startLanguage: '',
	/**
	 * @type {?String}
	 */
	tracksText: null,
	/**
	 * @type {?String}
	 */
	chaptersText: null,
	/**
	 * Avoid to screen reader speak captions over an audio track.
	 *
	 * @type {Boolean}
	 */
	tracksAriaLive: false,
	/**
	 * Remove the [cc] button when no track nodes are present
	 * @type {Boolean}
	 */
	hideCaptionsButtonWhenEmpty: true,
	/**
	 * Change captions to pop-up if true and only one track node is found
	 * @type {Boolean}
	 */
	toggleCaptionsButtonWhenOnlyOne: false,
	/**
	 * @type {String}
	 */
	slidesSelector: ''
});

Object.assign(MediaElementPlayer.prototype, {
	/**
	 * @type {Boolean}
	 */
	hasChapters: false,

	/**
	 * Feature constructor.
	 *
	 * Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list
	 * @param {MediaElementPlayer} player
	 * @param {HTMLElement} controls
	 * @param {HTMLElement} layers
	 * @param {HTMLElement} media
	 */
	buildtracks (player, controls, layers, media) {

		this.findTracks();

		if (!player.tracks.length && (!player.trackFiles || !player.trackFiles.length === 0)) {
			return;
		}

		const
			t = this,
			attr = t.options.tracksAriaLive ? ' role="log" aria-live="assertive" aria-atomic="false"' : '',
			tracksTitle = isString(t.options.tracksText) ? t.options.tracksText : i18n.t('mejs.captions-subtitles'),
			chaptersTitle = isString(t.options.chaptersText) ? t.options.chaptersText : i18n.t('mejs.captions-chapters'),
			total = player.trackFiles === null ? player.tracks.length : player.trackFiles.length
		;

		// If browser will do native captions, prefer mejs captions, loop through tracks and hide
		if (t.domNode.textTracks) {
			for (let i = t.domNode.textTracks.length - 1; i >= 0; i--) {
				t.domNode.textTracks[i].mode = 'hidden';
			}
		}

		t.cleartracks(player);

		player.captions = document.createElement('div');
		player.captions.className = `${t.options.classPrefix}captions-layer ${t.options.classPrefix}layer`;
		player.captions.innerHTML = `<div class="${t.options.classPrefix}captions-position ${t.options.classPrefix}captions-position-hover"${attr}>` +
			`<span class="${t.options.classPrefix}captions-text"></span>` +
		`</div>`;
		player.captions.style.display = 'none';
		layers.insertBefore(player.captions, layers.firstChild);

		player.captionsText = player.captions.querySelector(`.${t.options.classPrefix}captions-text`);

		player.captionsButton = document.createElement('div');
		player.captionsButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}captions-button`;
		player.captionsButton.innerHTML =
			`<button type="button" aria-controls="${t.id}" title="${tracksTitle}" aria-label="${tracksTitle}" tabindex="0"></button>` +
			`<div class="${t.options.classPrefix}captions-selector ${t.options.classPrefix}offscreen">` +
				`<ul class="${t.options.classPrefix}captions-selector-list">` +
					`<li class="${t.options.classPrefix}captions-selector-list-item">` +
						`<input type="radio" class="${t.options.classPrefix}captions-selector-input" ` +
							`name="${player.id}_captions" id="${player.id}_captions_none" ` +
							`value="none" checked disabled>` +
						`<label class="${t.options.classPrefix}captions-selector-label ` +
							`${t.options.classPrefix}captions-selected" ` +
							`for="${player.id}_captions_none">${i18n.t('mejs.none')}</label>` +
					`</li>` +
				`</ul>` +
			`</div>`;

		t.addControlElement(player.captionsButton, 'tracks');

		player.captionsButton.querySelector(`.${t.options.classPrefix}captions-selector-input`).disabled = false;

		player.chaptersButton = document.createElement('div');
		player.chaptersButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}chapters-button`;
		player.chaptersButton.innerHTML =
			`<button type="button" aria-controls="${t.id}" title="${chaptersTitle}" aria-label="${chaptersTitle}" tabindex="0"></button>` +
			`<div class="${t.options.classPrefix}chapters-selector ${t.options.classPrefix}offscreen">` +
				`<ul class="${t.options.classPrefix}chapters-selector-list"></ul>` +
			`</div>`;

		let subtitleCount = 0;

		for (let i = 0; i < total; i++) {
			const
				kind = player.tracks[i].kind,
				src = player.tracks[i].src
			;
			if (src.trim()) {
				if (kind === 'subtitles' || kind === 'captions') {
					subtitleCount++;
				} else if (kind === 'chapters' && !controls.querySelector(`.${t.options.classPrefix}chapter-selector`)) {
					player.captionsButton.parentNode.insertBefore(player.chaptersButton, player.captionsButton);
				}
			}
		}

		player.trackToLoad = -1;
		player.selectedTrack = null;
		player.isLoadingTrack = false;

		// add to list
		for (let i = 0; i < total; i++) {
			const kind = player.tracks[i].kind;
			if (player.tracks[i].src.trim() && (kind === 'subtitles' || kind === 'captions')) {
				player.addTrackButton(player.tracks[i].trackId, player.tracks[i].srclang, player.tracks[i].label);
			}
		}

		// start loading tracks
		player.loadNextTrack();

		const
			inEvents = ['mouseenter', 'focusin'],
			outEvents = ['mouseleave', 'focusout']
		;

		// if only one language then just make the button a toggle
		if (t.options.toggleCaptionsButtonWhenOnlyOne && subtitleCount === 1) {
			player.captionsButton.addEventListener('click', (e) => {
				let trackId = 'none';
				if (player.selectedTrack === null) {
					trackId = player.tracks[0].trackId;
				}
				const keyboard = e.keyCode || e.which;
				player.setTrack(trackId, (typeof keyboard !== 'undefined'));
			});
		} else {
			const
				labels = player.captionsButton.querySelectorAll(`.${t.options.classPrefix}captions-selector-label`),
				captions = player.captionsButton.querySelectorAll('input[type=radio]')
			;

			for (let i = 0, total = inEvents.length; i < total; i++) {
				player.captionsButton.addEventListener(inEvents[i], function () {
					removeClass(this.querySelector(`.${t.options.classPrefix}captions-selector`), `${t.options.classPrefix}offscreen`);
				});
			}

			for (let i = 0, total = outEvents.length; i < total; i++) {
				player.captionsButton.addEventListener(outEvents[i], function () {
					addClass(this.querySelector(`.${t.options.classPrefix}captions-selector`), `${t.options.classPrefix}offscreen`);
				});
			}

			for (let i = 0, total = captions.length; i < total; i++) {
				captions[i].addEventListener('click',  function (e) {
					// value is trackId, same as the actual id, and we're using it here
					// because the "none" checkbox doesn't have a trackId
					// to use, but we want to know when "none" is clicked
					const keyboard = e.keyCode || e.which;
					player.setTrack(this.value, (typeof keyboard !== 'undefined'));
				});
			}

			for (let i = 0, total = labels.length; i < total; i++) {
				labels[i].addEventListener('click',  function (e) {
					const
						radio = siblings(this, (el) => el.tagName === 'INPUT')[0],
						event = createEvent('click', radio)
					;
					radio.dispatchEvent(event);
					e.preventDefault();
				});
			}

			//Allow up/down arrow to change the selected radio without changing the volume.
			player.captionsButton.addEventListener('keydown', (e) => {
				e.stopPropagation();
			});
		}

		for (let i = 0, total = inEvents.length; i < total; i++) {
			player.chaptersButton.addEventListener(inEvents[i], function () {
				if (this.querySelector(`.${t.options.classPrefix}chapters-selector-list`).children.length) {
					removeClass(this.querySelector(`.${t.options.classPrefix}chapters-selector`), `${t.options.classPrefix}offscreen`);
				}
			});
		}

		for (let i = 0, total = outEvents.length; i < total; i++) {
			player.chaptersButton.addEventListener(outEvents[i], function () {
				addClass(this.querySelector(`.${t.options.classPrefix}chapters-selector`), `${t.options.classPrefix}offscreen`);
			});
		}

		//Allow up/down arrow to change the selected radio without changing the volume.
		player.chaptersButton.addEventListener('keydown', (e) => {
			e.stopPropagation();
		});

		if (!player.options.alwaysShowControls) {
			// move with controls
			player.getElement(player.container).addEventListener('controlsshown', () => {
				// push captions above controls
				addClass(player.getElement(player.container).querySelector(`.${t.options.classPrefix}captions-position`), `${t.options.classPrefix}captions-position-hover`);
			});

			player.getElement(player.container).addEventListener('controlshidden', () => {
				if (!media.paused) {
					// move back to normal place
					removeClass(player.getElement(player.container).querySelector(`.${t.options.classPrefix}captions-position`), `${t.options.classPrefix}captions-position-hover`);
				}
			});
		} else {
			addClass(player.getElement(player.container).querySelector(`.${t.options.classPrefix}captions-position`), `${t.options.classPrefix}captions-position-hover`);
		}

		media.addEventListener('timeupdate', () => {
			player.displayCaptions();
		});

		if (player.options.slidesSelector !== '') {
			player.slidesContainer = document.querySelectorAll(player.options.slidesSelector);

			media.addEventListener('timeupdate', () => {
				player.displaySlides();
			});
		}
	},

	/**
	 * Feature destructor.
	 *
	 * Always has to be prefixed with `clean` and the name that was used in MepDefaults.features list
	 * @param {MediaElementPlayer} player
	 */
	cleartracks (player) {
		if (player) {
			if (player.captions) {
				player.captions.remove();
			}
			if (player.chapters) {
				player.chapters.remove();
			}
			if (player.captionsText) {
				player.captionsText.remove();
			}
			if (player.captionsButton) {
				player.captionsButton.remove();
			}
			if (player.chaptersButton) {
				player.chaptersButton.remove();
			}
		}
	},

	rebuildtracks () {
		const t = this;
		t.findTracks();
		t.buildtracks(t, t.getElement(t.controls), t.getElement(t.layers), t.media);
	},

	findTracks () {
		const
			t = this,
			tracktags = t.trackFiles === null ? t.node.querySelectorAll('track') : t.trackFiles,
			total = tracktags.length
		;

		// store for use by plugins
		t.tracks = [];
		for (let i = 0; i < total; i++) {
			const
				track = tracktags[i],
				srclang = track.getAttribute('srclang').toLowerCase() || '',
				trackId = `${t.id}_track_${i}_${track.getAttribute('kind')}_${srclang}`
			;
			t.tracks.push({
				trackId: trackId,
				srclang: srclang,
				src: track.getAttribute('src'),
				kind: track.getAttribute('kind'),
				label: track.getAttribute('label') || '',
				entries: [],
				isLoaded: false
			});
		}
	},

	/**
	 *
	 * @param {String} trackId, or "none" to disable captions
	 * @param {Boolean} setByKeyboard
	 */
	setTrack (trackId, setByKeyboard) {

		const
			t = this,
			radios = t.captionsButton.querySelectorAll('input[type="radio"]'),
			captions = t.captionsButton.querySelectorAll(`.${t.options.classPrefix}captions-selected`),
			track = t.captionsButton.querySelector(`input[value="${trackId}"]`)
		;

		for (let i = 0, total = radios.length; i < total; i++) {
			radios[i].checked = false;
		}

		for (let i = 0, total = captions.length; i < total; i++) {
			removeClass(captions[i], `${t.options.classPrefix}captions-selected`);
		}

		track.checked = true;
		const labels = siblings(track, (el) => hasClass(el, `${t.options.classPrefix}captions-selector-label`));
		for (let i = 0, total = labels.length; i < total; i++) {
			addClass(labels[i], `${t.options.classPrefix}captions-selected`)
		}

		if (trackId === 'none') {
			t.selectedTrack = null;
			removeClass(t.captionsButton, `${t.options.classPrefix}captions-enabled`);
		} else {
			for (let i = 0, total = t.tracks.length; i < total; i++) {
				const track = t.tracks[i];
				if (track.trackId === trackId) {
					if (t.selectedTrack === null) {
						addClass(t.captionsButton, `${t.options.classPrefix}captions-enabled`);
					}
					t.selectedTrack = track;
					t.captions.setAttribute('lang', t.selectedTrack.srclang);
					t.displayCaptions();
					break;
				}
			}
		}

		const event = createEvent('captionschange', t.media);
		event.detail.caption = t.selectedTrack;
		t.media.dispatchEvent(event);

		if (!setByKeyboard) {
			setTimeout(function() {
				t.getElement(t.container).focus();
			}, 500);
		}
	},

	/**
	 *
	 */
	loadNextTrack () {
		const t = this;

		t.trackToLoad++;
		if (t.trackToLoad < t.tracks.length) {
			t.isLoadingTrack = true;
			t.loadTrack(t.trackToLoad);
		} else {
			// add done?
			t.isLoadingTrack = false;
			t.checkForTracks();
		}
	},

	/**
	 *
	 * @param index
	 */
	loadTrack (index) {
		const
			t = this,
			track = t.tracks[index]
		;

		if (track !== undefined && (track.src !== undefined || track.src !== "")) {
			ajax(track.src, 'text', (d) => {
				track.entries = typeof d === 'string' && (/<tt\s+xml/ig).exec(d) ?
					mejs.TrackFormatParser.dfxp.parse(d) : mejs.TrackFormatParser.webvtt.parse(d);

				track.isLoaded = true;
				t.enableTrackButton(track);
				t.loadNextTrack();

				if (track.kind === 'slides') {
					t.setupSlides(track);
				}
				// Load by default the first track with `chapters` kind
				else if (track.kind === 'chapters' && !t.hasChapters) {
					t.drawChapters(track);
					t.hasChapters = true;
				}
			}, () => {
				t.removeTrackButton(track.trackId);
				t.loadNextTrack();
			});
		}
	},

	/**
	 *
	 * @param {String} track - The language code
	 */
	enableTrackButton (track) {
		const
			t = this,
			lang = track.srclang,
			target = document.getElementById(`${track.trackId}`)
		;

		if (!target) {
			return;
		}

		let label = track.label;

		if (label === '') {
			label = i18n.t(mejs.language.codes[lang]) || lang;
		}
		target.disabled = false;
		const targetSiblings = siblings(target, (el) => hasClass(el, `${t.options.classPrefix}captions-selector-label`));
		for (let i = 0, total = targetSiblings.length; i < total; i++) {
			targetSiblings[i].innerHTML = label;
		}

		// auto select
		if (t.options.startLanguage === lang) {
			target.checked = true;
			const event = createEvent('click', target);
			target.dispatchEvent(event);
		}
	},

	/**
	 *
	 * @param {String} trackId
	 */
	removeTrackButton (trackId) {
		const element = document.getElementById(`${trackId}`);
		if (element) {
			const button = element.closest('li');
			if (button) {
				button.remove();
			}
		}
	},

	/**
	 *
	 * @param {String} trackId
	 * @param {String} lang - The language code
	 * @param {String} label
	 */
	addTrackButton (trackId, lang, label) {
		const t = this;
		if (label === '') {
			label = i18n.t(mejs.language.codes[lang]) || lang;
		}

		// trackId is used in the value, too, because the "none"
		// caption option doesn't have a trackId but we need to be able
		// to set it, too
		t.captionsButton.querySelector('ul').innerHTML += `<li class="${t.options.classPrefix}captions-selector-list-item">` +
			`<input type="radio" class="${t.options.classPrefix}captions-selector-input" ` +
				`name="${t.id}_captions" id="${trackId}" value="${trackId}" disabled>` +
			`<label class="${t.options.classPrefix}captions-selector-label"` +
				`for="${trackId}">${label} (loading)</label>` +
		`</li>`;
	},

	/**
	 *
	 */
	checkForTracks () {
		const t = this;

		let hasSubtitles = false;

		// check if any subtitles
		if (t.options.hideCaptionsButtonWhenEmpty) {
			for (let i = 0, total = t.tracks.length; i < total; i++) {
				const kind = t.tracks[i].kind;
				if ((kind === 'subtitles' || kind === 'captions') && t.tracks[i].isLoaded) {
					hasSubtitles = true;
					break;
				}
			}

			t.captionsButton.style.display = hasSubtitles ? '' : 'none';
			t.setControlsSize();
		}
	},

	/**
	 *
	 */
	displayCaptions () {
		if (this.tracks === undefined) {
			return;
		}

		const
			t = this,
			track = t.selectedTrack,
			sanitize = (html) => {
				const div = document.createElement('div');
				div.innerHTML = html;

				// Remove all `<script>` tags first
				const scripts = div.getElementsByTagName('script');
				let i = scripts.length;
				while (i--) {
					scripts[i].remove();
				}

				// Loop the elements and remove anything that contains value="javascript:" or an `on*` attribute
				// (`onerror`, `onclick`, etc.)
				const allElements = div.getElementsByTagName('*');
				for (let i = 0, n = allElements.length; i < n; i++) {
					const
						attributesObj = allElements[i].attributes,
						attributes = Array.prototype.slice.call(attributesObj)
					;

					for (let j = 0, total = attributes.length; j < total; j++) {
						if (attributes[j].name.startsWith('on') || attributes[j].value.startsWith('javascript')) {
							allElements[i].remove();
						} else if (attributes[j].name === 'style') {
							allElements[i].removeAttribute(attributes[j].name);
						}
					}

				}
				return div.innerHTML;
			}
		;

		if (track !== null && track.isLoaded) {
			let i = t.searchTrackPosition(track.entries, t.media.currentTime);
			if (i > -1) {
				// Set the line before the timecode as a class so the cue can be targeted if needed
				t.captionsText.innerHTML = sanitize(track.entries[i].text);
				t.captionsText.className = `${t.options.classPrefix}captions-text ${(track.entries[i].identifier || '')}`;
				t.captions.style.display = '';
				t.captions.style.height = '0px';
				return; // exit out if one is visible;
			}
			t.captions.style.display = 'none';
		} else {
			t.captions.style.display = 'none';
		}
	},

	/**
	 *
	 * @param {HTMLElement} track
	 */
	setupSlides (track) {
		const t = this;
		t.slides = track;
		t.slides.entries.imgs = [t.slides.entries.length];
		t.showSlide(0);
	},

	/**
	 *
	 * @param {Number} index
	 */
	showSlide (index) {
		const t = this;

		if (t.tracks === undefined || t.slidesContainer === undefined) {
			return;
		}

		const url = t.slides.entries[index].text;

		let img = t.slides.entries[index].imgs;

		if (img === undefined || img.fadeIn === undefined) {
			const image = document.createElement('img');
			image.src = url;
			image.addEventListener('load', () => {
				const
					self = this,
					visible = siblings(self, (el) => visible(el))
				;
				self.style.display = 'none';
				t.slidesContainer.innerHTML += self.innerHTML;
				fadeIn(t.slidesContainer.querySelector(image));
				for (let i = 0, total = visible.length; i < total; i++) {
					fadeOut(visible[i], 400);
				}

			});
			t.slides.entries[index].imgs = img = image;

		} else if (!visible(img)) {
			const visible = siblings(self, (el) => visible(el));
			fadeIn(t.slidesContainer.querySelector(img));
			for (let i = 0, total = visible.length; i < total; i++) {
				fadeOut(visible[i]);
			}
		}

	},

	/**
	 *
	 */
	displaySlides () {
		const t = this;

		if (this.slides === undefined) {
			return;
		}

		const
			slides = t.slides,
			i = t.searchTrackPosition(slides.entries, t.media.currentTime)
		;

		if (i > -1) {
			t.showSlide(i);
		}
	},

	/**
	 *
	 * @param {Object} chapters
	 */
	drawChapters (chapters) {
		const
			t = this,
			total = chapters.entries.length
		;

		if (!total) {
			return;
		}

		t.chaptersButton.querySelector('ul').innerHTML = '';

		for (let i = 0; i < total; i++) {
			t.chaptersButton.querySelector('ul').innerHTML += `<li class="${t.options.classPrefix}chapters-selector-list-item" ` +
				`role="menuitemcheckbox" aria-live="polite" aria-disabled="false" aria-checked="false">` +
				`<input type="radio" class="${t.options.classPrefix}captions-selector-input" ` +
					`name="${t.id}_chapters" id="${t.id}_chapters_${i}" value="${chapters.entries[i].start}" disabled>` +
				`<label class="${t.options.classPrefix}chapters-selector-label"`+
					`for="${t.id}_chapters_${i}">${chapters.entries[i].text}</label>` +
			`</li>`;
		}

		const
			radios = t.chaptersButton.querySelectorAll('input[type="radio"]'),
			labels = t.chaptersButton.querySelectorAll(`.${t.options.classPrefix}chapters-selector-label`)
		;

		for (let i = 0, total = radios.length; i < total; i++) {
			radios[i].disabled = false;
			radios[i].checked = false;
			radios[i].addEventListener('click',  function (e) {
				const
					self = this,
					listItems = t.chaptersButton.querySelectorAll('li'),
					label = siblings(self, (el) => hasClass(el, `${t.options.classPrefix}chapters-selector-label`))[0]
				;

				self.checked = true;
				self.parentNode.setAttribute('aria-checked', true);
				addClass(label, `${t.options.classPrefix}chapters-selected`);
				removeClass(t.chaptersButton.querySelector(`.${t.options.classPrefix}chapters-selected`), `${t.options.classPrefix}chapters-selected`);

				for (let i = 0, total = listItems.length; i < total; i++) {
					listItems[i].setAttribute('aria-checked', false);
				}

				const keyboard = e.keyCode || e.which;
				if (typeof keyboard === 'undefined') {
					setTimeout(function() {
						t.getElement(t.container).focus();
					}, 500);
				}

				t.media.setCurrentTime(parseFloat(self.value));
				if (t.media.paused) {
					t.media.play();
				}
			});
		}

		for (let i = 0, total = labels.length; i < total; i++) {
			labels[i].addEventListener('click',  function (e) {
				const
					radio = siblings(this, (el) => el.tagName === 'INPUT')[0],
					event = createEvent('click', radio)
				;
				radio.dispatchEvent(event);
				e.preventDefault();
			});
		}
	},
	/**
	 * Perform binary search to look for proper track index
	 *
	 * @param {Object[]} tracks
	 * @param {Number} currentTime
	 * @return {Number}
	 */
	searchTrackPosition (tracks, currentTime) {
		let
			lo = 0,
			hi = tracks.length - 1,
			mid,
			start,
			stop
		;

		while (lo <= hi) {
			mid = ((lo + hi) >> 1);
			start = tracks[mid].start;
			stop = tracks[mid].stop;

			if (currentTime >= start && currentTime < stop) {
				return mid;
			} else if (start < currentTime) {
				lo = mid + 1;
			} else if (start > currentTime) {
				hi = mid - 1;
			}
		}

		return -1;
	}
});

/**
 * Map all possible languages with their respective code
 *
 * @constructor
 */
mejs.language = {
	codes: {
		af: 'mejs.afrikaans',
		sq: 'mejs.albanian',
		ar: 'mejs.arabic',
		be: 'mejs.belarusian',
		bg: 'mejs.bulgarian',
		ca: 'mejs.catalan',
		zh: 'mejs.chinese',
		'zh-cn': 'mejs.chinese-simplified',
		'zh-tw': 'mejs.chines-traditional',
		hr: 'mejs.croatian',
		cs: 'mejs.czech',
		da: 'mejs.danish',
		nl: 'mejs.dutch',
		en: 'mejs.english',
		et: 'mejs.estonian',
		fl: 'mejs.filipino',
		fi: 'mejs.finnish',
		fr: 'mejs.french',
		gl: 'mejs.galician',
		de: 'mejs.german',
		el: 'mejs.greek',
		ht: 'mejs.haitian-creole',
		iw: 'mejs.hebrew',
		hi: 'mejs.hindi',
		hu: 'mejs.hungarian',
		is: 'mejs.icelandic',
		id: 'mejs.indonesian',
		ga: 'mejs.irish',
		it: 'mejs.italian',
		ja: 'mejs.japanese',
		ko: 'mejs.korean',
		lv: 'mejs.latvian',
		lt: 'mejs.lithuanian',
		mk: 'mejs.macedonian',
		ms: 'mejs.malay',
		mt: 'mejs.maltese',
		no: 'mejs.norwegian',
		fa: 'mejs.persian',
		pl: 'mejs.polish',
		pt: 'mejs.portuguese',
		ro: 'mejs.romanian',
		ru: 'mejs.russian',
		sr: 'mejs.serbian',
		sk: 'mejs.slovak',
		sl: 'mejs.slovenian',
		es: 'mejs.spanish',
		sw: 'mejs.swahili',
		sv: 'mejs.swedish',
		tl: 'mejs.tagalog',
		th: 'mejs.thai',
		tr: 'mejs.turkish',
		uk: 'mejs.ukrainian',
		vi: 'mejs.vietnamese',
		cy: 'mejs.welsh',
		yi: 'mejs.yiddish'
	}
};

/*
 Parses WebVTT format which should be formatted as
 ================================
 WEBVTT

 1
 00:00:01,1 --> 00:00:05,000
 A line of text

 2
 00:01:15,1 --> 00:02:05,000
 A second line of text

 ===============================

 Adapted from: http://www.delphiki.com/html5/playr
 */
mejs.TrackFormatParser = {
	webvtt: {
		/**
		 * @type {String}
		 */
		pattern: /^((?:[0-9]{1,2}:)?[0-9]{2}:[0-9]{2}([,.][0-9]{1,3})?) --\> ((?:[0-9]{1,2}:)?[0-9]{2}:[0-9]{2}([,.][0-9]{3})?)(.*)$/,

		/**
		 *
		 * @param {String} trackText
		 * @returns {{text: Array, times: Array}}
		 */
		parse (trackText) {
			const
				lines = trackText.split(/\r?\n/),
				entries = []
			;

			let
				timecode,
				text,
				identifier
			;

			for (let i = 0, total = lines.length; i < total; i++) {
				timecode = this.pattern.exec(lines[i]);

				if (timecode && i < lines.length) {
					if ((i - 1) >= 0 && lines[i - 1] !== '') {
						identifier = lines[i - 1];
					}
					i++;
					// grab all the (possibly multi-line) text that follows
					text = lines[i];
					i++;
					while (lines[i] !== '' && i < lines.length) {
						text = `${text}\n${lines[i]}`;
						i++;
					}
					text = text.trim().replace(/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, "<a href='$1' target='_blank'>$1</a>");
					entries.push({
						identifier: identifier,
						start: (convertSMPTEtoSeconds(timecode[1]) === 0) ? 0.200 : convertSMPTEtoSeconds(timecode[1]),
						stop: convertSMPTEtoSeconds(timecode[3]),
						text: text,
						settings: timecode[5]
					});
				}
				identifier = '';
			}
			return entries;
		}
	},
	// Thanks to Justin Capella: https://github.com/johndyer/mediaelement/pull/420
	dfxp: {
		/**
		 *
		 * @param {String} trackText
		 * @returns {{text: Array, times: Array}}
		 */
		parse (trackText) {
			trackText = $(trackText).filter('tt');
			const
				container = trackText.firstChild,
				lines = container.querySelectorAll('p'),
				styleNode = trackText.getElementById(`${container.attr('style')}`),
				entries = []
			;

			let styles;

			if (styleNode.length) {
				styleNode.removeAttribute('id');
				const attributes = styleNode.attributes;
				if (attributes.length) {
					styles = {};
					for (let i = 0, total = attributes.length; i < total; i++) {
						styles[attributes[i].name.split(":")[1]] = attributes[i].value;
					}
				}
			}

			for (let i = 0, total = lines.length; i < total; i++) {
				let
					style,
					_temp = {
						start: null,
						stop: null,
						style: null,
						text: null
					}
				;

				if (lines.eq(i).attr('begin')) {
					_temp.start = convertSMPTEtoSeconds(lines.eq(i).attr('begin'));
				}
				if (!_temp.start && lines.eq(i - 1).attr('end')) {
					_temp.start = convertSMPTEtoSeconds(lines.eq(i - 1).attr('end'));
				}
				if (lines.eq(i).attr('end')) {
					_temp.stop = convertSMPTEtoSeconds(lines.eq(i).attr('end'));
				}
				if (!_temp.stop && lines.eq(i + 1).attr('begin')) {
					_temp.stop = convertSMPTEtoSeconds(lines.eq(i + 1).attr('begin'));
				}

				if (styles) {
					style = '';
					for (let _style in styles) {
						style += `${_style}:${styles[_style]};`;
					}
				}
				if (style) {
					_temp.style = style;
				}
				if (_temp.start === 0) {
					_temp.start = 0.200;
				}
				_temp.text = lines.eq(i).innerHTML.trim().replace(/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, "<a href='$1' target='_blank'>$1</a>");
				entries.push(_temp);
			}
			return entries;
		}
	}
};