mediaelement
Version:
One file. Any browser. Same UI.
991 lines (875 loc) • 28 kB
JavaScript
;
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';
import {generateControlButton} from '../utils/generate';
/**
* 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 =
generateControlButton(t.id, tracksTitle, tracksTitle, `${t.media.options.iconSprite}`, ['icon-captions'], `${t.options.classPrefix}`) +
`<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 =
generateControlButton(t.id, chaptersTitle, chaptersTitle, `${t.media.options.iconSprite}`, ['icon-chapters'], `${t.options.classPrefix}`) +
`<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
var text = track.entries[i].text;
if (typeof t.options.captionTextPreprocessor === 'function')
text = t.options.captionTextPreprocessor(text);
t.captionsText.innerHTML = sanitize(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 === null ? '' : 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) {
const trackElem = document.adoptNode(new DOMParser().parseFromString(trackText, 'application/xml').documentElement),
container = trackElem.querySelector('div'),
lines = container.querySelectorAll('p'),
styleNode = document.getElementById(container.getAttribute('style')),
entries = []
;
let styles;
if (styleNode) {
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[i].getAttribute('begin')) {
_temp.start = convertSMPTEtoSeconds(lines[i].getAttribute('begin'));
}
if (!_temp.start && lines[i - 1].getAttribute('end')) {
_temp.start = convertSMPTEtoSeconds(lines[i - 1].getAttribute('end'));
}
if (lines[i].getAttribute('end')) {
_temp.stop = convertSMPTEtoSeconds(lines[i].getAttribute('end'));
}
if (!_temp.stop && lines[i + 1].getAttribute('begin')) {
_temp.stop = convertSMPTEtoSeconds(lines[i + 1].getAttribute('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[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;
}
}
};