mediaelement
Version:
One file. Any browser. Same UI.
315 lines (277 loc) • 9.13 kB
JavaScript
;
import window from 'global/window';
import mejs from '../core/mejs';
import {renderer} from '../core/renderer';
import {createEvent} from '../utils/general';
import {HAS_MSE} from '../utils/constants';
import {typeChecks} from '../utils/media';
import {loadScript} from '../utils/dom';
/**
* Native HLS renderer
*
* Uses DailyMotion's hls.js, which is a JavaScript library which implements an HTTP Live Streaming client.
* It relies on HTML5 video and MediaSource Extensions for playback.
* @see https://github.com/video-dev/hls.js
*
*/
const NativeHls = {
promise: null,
/**
* Create a queue to prepare the loading of an HLS source
*
* @param {Object} settings - an object with settings needed to load an HLS player instance
*/
load: (settings) => {
if (typeof Hls !== 'undefined') {
NativeHls.promise = new Promise((resolve) => {
resolve();
}).then(() => {
NativeHls._createPlayer(settings);
});
} else {
settings.options.path = typeof settings.options.path === 'string' ?
settings.options.path : 'https://cdn.jsdelivr.net/npm/hls.js@latest';
NativeHls.promise = NativeHls.promise || loadScript(settings.options.path);
NativeHls.promise.then(() => {
NativeHls._createPlayer(settings);
});
}
return NativeHls.promise;
},
/**
* Create a new instance of HLS player and trigger a custom event to initialize it
*
* @param {Object} settings - an object with settings needed to instantiate HLS object
* @return {Hls}
*/
_createPlayer: (settings) => {
const player = new Hls(settings.options);
window[`__ready__${settings.id}`](player);
return player;
}
};
const HlsNativeRenderer = {
name: 'native_hls',
options: {
prefix: 'native_hls',
hls: {
// Special config: used to set the local path/URL of hls.js library
path: 'https://cdn.jsdelivr.net/npm/hls.js@latest',
// To modify more elements from hls.js,
// see https://github.com/video-dev/hls.js/blob/master/doc/API.md#fine-tuning
autoStartLoad: false,
debug: false
}
},
/**
* Determine if a specific element type can be played with this render
*
* @param {String} type
* @return {Boolean}
*/
canPlayType: (type) => HAS_MSE && ['application/x-mpegurl', 'application/vnd.apple.mpegurl', 'audio/mpegurl', 'audio/hls',
'video/hls'].indexOf(type.toLowerCase()) > -1,
/**
* Create the player instance and add all native events/methods/properties as possible
*
* @param {MediaElement} mediaElement Instance of mejs.MediaElement already created
* @param {Object} options All the player configuration options passed through constructor
* @param {Object[]} mediaFiles List of sources with format: {src: url, type: x/y-z}
* @return {Object}
*/
create: (mediaElement, options, mediaFiles) => {
const
originalNode = mediaElement.originalNode,
id = mediaElement.id + '_' + options.prefix,
preload = originalNode.getAttribute('preload'),
autoplay = originalNode.autoplay
;
let
hlsPlayer = null,
node = null,
index = 0,
total = mediaFiles.length
;
node = originalNode.cloneNode(true);
options = Object.assign(options, mediaElement.options);
options.hls.autoStartLoad = ((preload && preload !== 'none') || autoplay);
const
props = mejs.html5media.properties,
events = mejs.html5media.events.concat(['click', 'mouseover', 'mouseout']).filter(e => e !== 'error'),
attachNativeEvents = (e) => {
const event = createEvent(e.type, mediaElement);
mediaElement.dispatchEvent(event);
},
assignGettersSetters = (propName) => {
const capName = `${propName.substring(0, 1).toUpperCase()}${propName.substring(1)}`;
node[`get${capName}`] = () => hlsPlayer !== null ? node[propName] : null;
node[`set${capName}`] = (value) => {
if (mejs.html5media.readOnlyProperties.indexOf(propName) === -1) {
if (propName === 'src') {
node[propName] = typeof value === 'object' && value.src ? value.src : value;
if (hlsPlayer !== null) {
hlsPlayer.destroy();
for (let i = 0, total = events.length; i < total; i++) {
node.removeEventListener(events[i], attachNativeEvents);
}
hlsPlayer = NativeHls._createPlayer({
options: options.hls,
id: id
});
hlsPlayer.loadSource(value);
hlsPlayer.attachMedia(node);
}
} else {
node[propName] = value;
}
}
};
}
;
for (let i = 0, total = props.length; i < total; i++) {
assignGettersSetters(props[i]);
}
window['__ready__' + id] = (_hlsPlayer) => {
mediaElement.hlsPlayer = hlsPlayer = _hlsPlayer;
const
hlsEvents = Hls.Events,
assignEvents = (eventName) => {
if (eventName === 'loadedmetadata') {
const url = mediaElement.originalNode.src;
hlsPlayer.detachMedia();
hlsPlayer.loadSource(url);
hlsPlayer.attachMedia(node);
}
node.addEventListener(eventName, attachNativeEvents);
}
;
for (let i = 0, total = events.length; i < total; i++) {
assignEvents(events[i]);
}
/**
* Custom HLS events
*
* These events can be attached to the original node using addEventListener and the name of the event,
* not using Hls.Events object
* @see https://github.com/video-dev/hls.js/blob/master/src/events.js
* @see https://github.com/video-dev/hls.js/blob/master/src/errors.js
* @see https://github.com/video-dev/hls.js/blob/master/doc/API.md#runtime-events
* @see https://github.com/video-dev/hls.js/blob/master/doc/API.md#errors
*/
let recoverDecodingErrorDate, recoverSwapAudioCodecDate;
const assignHlsEvents = function (name, data) {
if (name === 'hlsError') {
console.warn(data);
data = data[1];
// borrowed from https://video-dev.github.io/hls.js/demo
if (data.fatal) {
switch (data.type) {
case 'mediaError':
const now = new Date().getTime();
if (!recoverDecodingErrorDate || (now - recoverDecodingErrorDate) > 3000) {
recoverDecodingErrorDate = new Date().getTime();
hlsPlayer.recoverMediaError();
} else if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
recoverSwapAudioCodecDate = new Date().getTime();
console.warn('Attempting to swap Audio Codec and recover from media error');
hlsPlayer.swapAudioCodec();
hlsPlayer.recoverMediaError();
} else {
const message = 'Cannot recover, last media error recovery failed';
mediaElement.generateError(message, node.src);
console.error(message);
}
break;
case 'networkError':
if (data.details === 'manifestLoadError') {
if (index < total && mediaFiles[(index + 1)] !== undefined) {
node.setSrc(mediaFiles[index++].src);
node.load();
node.play();
} else {
const message = 'Network error';
mediaElement.generateError(message, mediaFiles);
console.error(message);
}
} else {
const message = 'Network error';
mediaElement.generateError(message, mediaFiles);
console.error(message);
}
break;
default:
hlsPlayer.destroy();
break;
}
return;
}
}
const event = createEvent(name, mediaElement);
event.data = data;
mediaElement.dispatchEvent(event);
};
for (const eventType in hlsEvents) {
if (hlsEvents.hasOwnProperty(eventType)) {
hlsPlayer.on(hlsEvents[eventType], (...args) => assignHlsEvents(hlsEvents[eventType], args));
}
}
};
if (total > 0) {
for (; index < total; index++) {
if (renderer.renderers[options.prefix].canPlayType(mediaFiles[index].type)) {
node.setAttribute('src', mediaFiles[index].src);
break;
}
}
}
if (preload !== 'auto' && !autoplay) {
node.addEventListener('play', () => {
if (hlsPlayer !== null) {
hlsPlayer.startLoad();
}
});
node.addEventListener('pause', () => {
if (hlsPlayer !== null) {
hlsPlayer.stopLoad();
}
});
}
node.setAttribute('id', id);
originalNode.parentNode.insertBefore(node, originalNode);
originalNode.autoplay = false;
originalNode.style.display = 'none';
node.setSize = (width, height) => {
node.style.width = `${width}px`;
node.style.height = `${height}px`;
return node;
};
node.hide = () => {
node.pause();
node.style.display = 'none';
return node;
};
node.show = () => {
node.style.display = '';
return node;
};
node.destroy = () => {
if (hlsPlayer !== null) {
hlsPlayer.stopLoad();
hlsPlayer.destroy();
}
};
const event = createEvent('rendererready', node, false);
mediaElement.originalNode.dispatchEvent(event);
mediaElement.promises.push(NativeHls.load({
options: options.hls,
id: id
}));
return node;
}
};
/**
* Register Native HLS type based on URL structure
*
*/
typeChecks.push((url) => ~(url.toLowerCase()).indexOf('.m3u8') ? 'application/x-mpegURL' : null);
renderer.add(HlsNativeRenderer);