UNPKG

mediaelement

Version:
450 lines (400 loc) 11.8 kB
'use strict'; /** * Twitch renderer * * Uses <iframe> approach and uses Twitch API to manipulate it. Collections are not supported by default. * @see https://github.com/justintv/Twitch-API/blob/master/embed-video.md */ const TwitchApi = { promise: null, /** * Create a queue to prepare the creation of <iframe> * * @param {Object} settings - an object with settings needed to create <iframe> */ load: (settings) => { if (typeof Twitch !== 'undefined') { TwitchApi.promise = new Promise((resolve) => { resolve(); }).then(() => { TwitchApi._createPlayer(settings); }); } else { TwitchApi.promise = TwitchApi.promise || mejs.Utils.loadScript('https://player.twitch.tv/js/embed/v1.js'); TwitchApi.promise.then(() => { TwitchApi._createPlayer(settings); }); } }, /** * Create a new instance of Twitch API player and trigger a custom event to initialize it * * @param {Object} settings - an object with settings needed to create <iframe> */ _createPlayer: (settings) => { const player = new Twitch.Player(settings.id, settings); window[`__ready__${settings.id}`](player); }, /** * Extract ID from Twitch to be loaded through API * Valid URL format(s): * - https://player.twitch.tv/?video=40464143 * - https://www.twitch.tv/40464143 * - https://player.twitch.tv/?channel=monserrat * - https://www.twitch.tv/monserrat * * @param {String} url - Twitch full URL to grab the number Id of the source * @return {int} */ getTwitchId: (url) => { let twitchId = ''; if (url.indexOf('?') > 0) { twitchId = TwitchApi.getTwitchIdFromParam(url); if (twitchId === '') { twitchId = TwitchApi.getTwitchIdFromUrl(url); } } else { twitchId = TwitchApi.getTwitchIdFromUrl(url); } return twitchId; }, /** * Get ID from URL with format: * - https://player.twitch.tv/?channel=monserrat * - https://player.twitch.tv/?video=40464143 * * @param {String} url * @returns {string} */ getTwitchIdFromParam: (url) => { if (url === undefined || url === null || !url.trim().length) { return null; } const parts = url.split('?'), parameters = parts[1].split('&') ; let twitchId = ''; for (let i = 0, total = parameters.length; i < total; i++) { const paramParts = parameters[i].split('='); if (~paramParts[0].indexOf('channel')) { twitchId = paramParts[1]; break; } else if (~paramParts[0].indexOf('video')) { twitchId = `v${paramParts[1]}`; break; } } return twitchId; }, /** * Get ID from URL with formats: * - https://www.twitch.tv/40464143 * - https://www.twitch.tv/monserrat * * @param {String} url * @return {?String} */ getTwitchIdFromUrl: (url) => { if (url === undefined || url === null || !url.trim().length) { return null; } const parts = url.split('?'); url = parts[0]; const id = url.substring(url.lastIndexOf('/') + 1); return /^\d+$/i.test(id) ? `v${id}` : id; }, /** * Determine whether media is channel or video based on Twitch ID * * @see getTwitchId() * @param {String} id * @returns {String} */ getTwitchType: (id) => /^v\d+/i.test(id) ? 'video' : 'channel' }; const TwitchIframeRenderer = { name: 'twitch_iframe', options: { prefix: 'twitch_iframe', }, /** * Determine if a specific element type can be played with this render * * @param {String} type * @return {Boolean} */ canPlayType: (type) => ~['video/twitch', 'video/x-twitch'].indexOf(type.toLowerCase()), /** * 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) => { // API objects const twitch = {}, apiStack = [], readyState = 4, twitchId = TwitchApi.getTwitchId(mediaFiles[0].src) ; let twitchPlayer = null, paused = true, ended = false, hasStartedPlaying = false, volume = 1, duration = Infinity, time = 0 ; twitch.options = options; twitch.id = mediaElement.id + '_' + options.prefix; twitch.mediaElement = mediaElement; // wrappers for get/set const props = mejs.html5media.properties, assignGettersSetters = (propName) => { const capName = `${propName.substring(0, 1).toUpperCase()}${propName.substring(1)}`; twitch[`get${capName}`] = () => { if (twitchPlayer !== null) { const value = null; // figure out how to get Twitch dta here switch (propName) { case 'currentTime': time = twitchPlayer.getCurrentTime(); return time; case 'duration': duration = twitchPlayer.getDuration(); return duration; case 'volume': volume = twitchPlayer.getVolume(); return volume; case 'paused': paused = twitchPlayer.isPaused(); return paused; case 'ended': ended = twitchPlayer.getEnded(); return ended; case 'muted': return twitchPlayer.getMuted(); case 'buffered': return { start: () => { return 0; }, end: () => { return 0; }, length: 1 }; case 'src': return (TwitchApi.getTwitchType(twitchId) === 'channel') ? twitchPlayer.getChannel() : twitchPlayer.getVideo(); case 'readyState': return readyState; } return value; } else { return null; } }; twitch[`set${capName}`] = (value) => { if (twitchPlayer !== null) { switch (propName) { case 'src': const url = typeof value === 'string' ? value : value[0].src, videoId = TwitchApi.getTwitchId(url) ; if (TwitchApi.getTwitchType(twitchId) === 'channel') { twitchPlayer.setChannel(videoId); } else { twitchPlayer.setVideo(videoId); } break; case 'currentTime': twitchPlayer.seek(value); setTimeout(() => { const event = mejs.Utils.createEvent('timeupdate', twitch); mediaElement.dispatchEvent(event); }, 50); break; case 'muted': twitchPlayer.setMuted(value); setTimeout(() => { const event = mejs.Utils.createEvent('volumechange', twitch); mediaElement.dispatchEvent(event); }, 50); break; case 'volume': volume = value; twitchPlayer.setVolume(value); setTimeout(() => { const event = mejs.Utils.createEvent('volumechange', twitch); mediaElement.dispatchEvent(event); }, 50); break; case 'readyState': const event = mejs.Utils.createEvent('canplay', twitch); mediaElement.dispatchEvent(event); break; default: console.log('Twitch ' + twitch.id, propName, 'UNSUPPORTED property'); break; } } else { // store for after "READY" event fires apiStack.push({type: 'set', propName: propName, value: value}); } }; } ; for (let i = 0, total = props.length; i < total; i++) { assignGettersSetters(props[i]); } // add wrappers for native methods const methods = mejs.html5media.methods, assignMethods = (methodName) => { twitch[methodName] = () => { if (twitchPlayer !== null) { switch (methodName) { case 'play': paused = false; return twitchPlayer.play(); case 'pause': paused = true; return twitchPlayer.pause(); case 'load': return null; } } else { apiStack.push({type: 'call', methodName: methodName}); } }; } ; for (let i = 0, total = methods.length; i < total; i++) { assignMethods(methods[i]); } /** * Dispatch a list of events * * @private * @param {Array} events */ function sendEvents (events) { for (let i = 0, total = events.length; i < total; i++) { const event = mejs.Utils.createEvent(events[i], twitch); mediaElement.dispatchEvent(event); } } // Initial method to register all Twitch events when initializing <iframe> window['__ready__' + twitch.id] = (_twitchPlayer) => { mediaElement.twitchPlayer = twitchPlayer = _twitchPlayer; if (apiStack.length) { for (let i = 0, total = apiStack.length; i < total; i++) { const stackItem = apiStack[i]; if (stackItem.type === 'set') { const propName = stackItem.propName, capName = `${propName.substring(0, 1).toUpperCase()}${propName.substring(1)}`; twitch[`set${capName}`](stackItem.value); } else if (stackItem.type === 'call') { twitch[stackItem.methodName](); } } } const twitchIframe = document.getElementById(twitch.id).firstChild; twitchIframe.style.width = '100%'; twitchIframe.style.height = '100%'; const events = ['mouseover', 'mouseout'], assignEvents = (e) => { const event = mejs.Utils.createEvent(e.type, twitch); mediaElement.dispatchEvent(event); } ; for (let i = 0, total = events.length; i < total; i++) { twitchIframe.addEventListener(events[i], assignEvents, false); } let timer; // Twitch events twitchPlayer.addEventListener(Twitch.Player.READY, () => { paused = false; ended = false; sendEvents(['rendererready', 'loadedmetadata', 'loadeddata', 'canplay']); }); twitchPlayer.addEventListener(Twitch.Player.PLAY, () => { if (!hasStartedPlaying) { hasStartedPlaying = true; } paused = false; ended = false; sendEvents(['play', 'playing', 'progress']); // Workaround to update progress bar timer = setInterval(() => { twitchPlayer.getCurrentTime(); sendEvents(['timeupdate']); }, 250); }); twitchPlayer.addEventListener(Twitch.Player.PAUSE, () => { paused = true; ended = false; if (!twitchPlayer.getEnded()) { sendEvents(['pause']); } }); twitchPlayer.addEventListener(Twitch.Player.ENDED, () => { paused = true; ended = true; sendEvents(['ended']); clearInterval(timer); hasStartedPlaying = false; timer = null; }); }; const height = mediaElement.originalNode.height, width = mediaElement.originalNode.width, twitchContainer = document.createElement('div'), type = TwitchApi.getTwitchType(twitchId), twitchSettings = { id: twitch.id, width: width, height: height, playsinline: false, autoplay: mediaElement.originalNode.autoplay, muted: mediaElement.originalNode.muted, } ; twitchSettings[type] = twitchId; twitchContainer.id = twitch.id; twitchContainer.style.width = '100%'; twitchContainer.style.height = '100%'; mediaElement.originalNode.parentNode.insertBefore(twitchContainer, mediaElement.originalNode); mediaElement.originalNode.style.display = 'none'; mediaElement.originalNode.autoplay = false; twitch.setSize = (width, height) => { if (TwitchApi !== null && !isNaN(width) && !isNaN(height)) { twitchContainer.setAttribute('width', width); twitchContainer.setAttribute('height', height); } }; twitch.hide = () => { twitch.pause(); twitchContainer.style.display = 'none'; }; twitch.show = () => { twitchContainer.style.display = ''; }; twitch.destroy = () => {}; // send it off for async loading and creation TwitchApi.load(twitchSettings); return twitch; } }; mejs.Utils.typeChecks.push((url) => /\/\/(www|player).twitch.tv/i.test(url) ? 'video/x-twitch' : null); mejs.Renderers.add(TwitchIframeRenderer);