/*! yt-player. MIT License. Feross Aboukhadijeh */ var EventEmitter = function () { this.events = {}; }; EventEmitter.prototype.on = function (event, listener) { if (typeof this.events[event] !== 'object') { this.events[event] = []; } this.events[event].push(listener); }; EventEmitter.prototype.removeListener = function (event, listener) { var idx; if (typeof this.events[event] === 'object') { idx = this.indexOf(this.events[event], listener); if (idx > -1) { this.events[event].splice(idx, 1); } } }; EventEmitter.prototype.emit = function (event) { var i, listeners, length, args = [].slice.call(arguments, 1); if (typeof this.events[event] === 'object') { listeners = this.events[event].slice(); length = listeners.length; for (i = 0; i < length; i++) { listeners[i].apply(this, args); } } }; EventEmitter.prototype.once = function (event, listener) { this.on(event, function g () { this.removeListener(event, g); listener.apply(this, arguments); }); }; var loadScript = function (src, attrs, parentNode) { return new Promise((resolve, reject) => { var script = document.createElement('script') script.async = true script.src = src for (var [k, v] of Object.entries(attrs || {})) { script.setAttribute(k, v) } script.onload = () => { script.onerror = script.onload = null resolve(script) } script.onerror = () => { script.onerror = script.onload = null reject(new Error(`Failed to load ${src}`)) } var node = parentNode || document.head || document.getElementsByTagName('head')[0] node.appendChild(script) }) } var YOUTUBE_IFRAME_API_SRC = 'https://www.youtube.com/iframe_api' var YOUTUBE_STATES = { '-1': 'unstarted', 0: 'ended', 1: 'playing', 2: 'paused', 3: 'buffering', 5: 'cued' } var YOUTUBE_ERROR = { // The request contains an invalid parameter value. For example, this error // occurs if you specify a videoId that does not have 11 characters, or if the // videoId contains invalid characters, such as exclamation points or asterisks. INVALID_PARAM: 2, // The requested content cannot be played in an HTML5 player or another error // related to the HTML5 player has occurred. HTML5_ERROR: 5, // The video requested was not found. This error occurs when a video has been // removed (for any reason) or has been marked as private. NOT_FOUND: 100, // The owner of the requested video does not allow it to be played in embedded // players. UNPLAYABLE_1: 101, // This error is the same as 101. It's just a 101 error in disguise! UNPLAYABLE_2: 150 } var loadIframeAPICallbacks = [] /** * YouTube Player. Exposes a better API, with nicer events. * @param {HTMLElement|selector} element */ YouTubePlayer = class YouTubePlayer extends EventEmitter { constructor (element, opts) { super() var elem = typeof element === 'string' ? document.querySelector(element) : element if (elem.id) { this._id = elem.id // use existing element id } else { this._id = elem.id = 'ytplayer-' + Math.random().toString(16).slice(2, 8) } this._opts = Object.assign({ width: 640, height: 360, autoplay: false, captions: undefined, controls: true, keyboard: true, fullscreen: true, annotations: true, modestBranding: false, related: true, timeupdateFrequency: 1000, playsInline: true, start: 0 }, opts) this.videoId = null this.destroyed = false this._api = null this._autoplay = false // autoplay the first video? this._player = null this._ready = false // is player ready? this._queue = [] this.replayInterval = [] this._interval = null // Setup listeners for 'timeupdate' events. The YouTube Player does not fire // 'timeupdate' events, so they are simulated using a setInterval(). this._startInterval = this._startInterval.bind(this) this._stopInterval = this._stopInterval.bind(this) this.on('playing', this._startInterval) this.on('unstarted', this._stopInterval) this.on('ended', this._stopInterval) this.on('paused', this._stopInterval) this.on('buffering', this._stopInterval) this._loadIframeAPI((err, api) => { if (err) return this._destroy(new Error('YouTube Iframe API failed to load')) this._api = api // If load(videoId, [autoplay, [size]]) was called before Iframe API // loaded, ensure it gets called again now if (this.videoId) this.load(this.videoId, this._autoplay, this._start) }) } indexOf (haystack, needle) { var i = 0, length = haystack.length, idx = -1, found = false; while (i < length && !found) { if (haystack[i] === needle) { idx = i; found = true; } i++; } return idx; } load (videoId, autoplay = false, start = 0) { if (this.destroyed) return this._startOptimizeDisplayEvent() this._optimizeDisplayHandler('center, center') this.videoId = videoId this._autoplay = autoplay this._start = start // If the Iframe API is not ready yet, do nothing. Once the Iframe API is // ready, `load(this.videoId)` will be called. if (!this._api) return // If there is no player instance, create one. if (!this._player) { this._createPlayer(videoId) return } // If the player instance is not ready yet, do nothing. Once the player // instance is ready, `load(this.videoId)` will be called. This ensures that // the last call to `load()` is the one that takes effect. if (!this._ready) return // If the player instance is ready, load the given `videoId`. if (autoplay) { this._player.loadVideoById(videoId, start) } else { this._player.cueVideoById(videoId, start) } } play () { if (this._ready) this._player.playVideo() else this._queueCommand('play') } replayFrom(num) { const find = this.replayInterval.find((obj) => { return obj.iframeParent === this._player.i.parentNode }) if (find || !num) return this.replayInterval.push({ iframeParent: this._player.i.parentNode, interval: setInterval(() => { if (this._player.getCurrentTime() >= this._player.getDuration() - Number(num)) { this.seek(0); for (const [key, val] of this.replayInterval.entries()) { if (Object.hasOwnProperty.call(this.replayInterval, key)) { clearInterval(this.replayInterval[key].interval) this.replayInterval.splice(key, 1) } } } }, Number(num) * 1000) }) } pause () { if (this._ready) this._player.pauseVideo() else this._queueCommand('pause') } stop () { if (this._ready) this._player.stopVideo() else this._queueCommand('stop') } seek (seconds) { if (this._ready) this._player.seekTo(seconds, true) else this._queueCommand('seek', seconds) } _optimizeDisplayHandler(anchor) { if (!this._player) return const YTPlayer = this._player.i const YTPAlign = anchor.split(","); if (YTPlayer) { const win = {}, el = YTPlayer.parentElement; if (el) { const computedStyle = window.getComputedStyle(el), outerHeight = el.clientHeight + parseFloat(computedStyle.marginTop, 10) + parseFloat(computedStyle.marginBottom, 10) + parseFloat(computedStyle.borderTopWidth, 10) + parseFloat(computedStyle.borderBottomWidth, 10), outerWidth = el.clientWidth + parseFloat(computedStyle.marginLeft, 10) + parseFloat(computedStyle.marginRight, 10) + parseFloat(computedStyle.borderLeftWidth, 10) + parseFloat(computedStyle.borderRightWidth, 10), ratio = 1.7, vid = YTPlayer; win.width = outerWidth; win.height = outerHeight + 80; vid.style.width = win.width + 'px'; vid.style.height = Math.ceil(parseFloat(vid.style.width, 10) / ratio) + 'px'; vid.style.marginTop = Math.ceil(-((parseFloat(vid.style.height, 10) - win.height) / 2)) + 'px'; vid.style.marginLeft = 0; const lowest = parseFloat(vid.style.height, 10) < win.height; if (lowest) { vid.style.height = win.height + 'px', vid.style.width = Math.ceil(parseFloat(vid.style.height, 10) * ratio) + 'px', vid.style.marginTop = 0, vid.style.marginLeft = Math.ceil(-((parseFloat(vid.style.width, 10) - win.width) / 2)) + 'px' } for (const align in YTPAlign) if (YTPAlign.hasOwnProperty(align)) { const al = YTPAlign[align].replace(/ /g, ""); switch (al) { case "top": vid.style.marginTop = lowest ? -((parseFloat(vid.style.height, 10) - win.height) / 2) + 'px' : 0; break; case "bottom": vid.style.marginTop = lowest ? 0 : -(parseFloat(vid.style.height, 10) - win.height) + 'px'; break; case "left": vid.style.marginLeft = 0; break; case "right": vid.style.marginLeft = lowest ? -(parseFloat(vid.style.width, 10) - win.width) : 0 + 'px'; break; default: parseFloat(vid.style.width, 10) > win.width && (vid.style.marginLeft = -((parseFloat(vid.style.width, 10) - win.width) / 2) + 'px') } } } } } stopResize () { window.removeEventListener('resize', this._resizeListener) this._resizeListener = null } stopReplay (iframeParent) { for (const [key, val] of this.replayInterval.entries()) { if (Object.hasOwnProperty.call(this.replayInterval, key)) { if (iframeParent === this.replayInterval[key].iframeParent) { clearInterval(this.replayInterval[key].interval); this.replayInterval.splice(key, 1) } } } } setVolume (volume) { if (this._ready) this._player.setVolume(volume) else this._queueCommand('setVolume', volume) } loadPlaylist () { if (this._ready) this._player.loadPlaylist(this.videoId) else this._queueCommand('loadPlaylist', this.videoId) } setLoop (bool) { if (this._ready) this._player.setLoop(bool) else this._queueCommand('setLoop', bool) } getVolume () { return (this._ready && this._player.getVolume()) || 0 } mute () { if (this._ready) this._player.mute() else this._queueCommand('mute') } unMute () { if (this._ready) this._player.unMute() else this._queueCommand('unMute') } isMuted () { return (this._ready && this._player.isMuted()) || false } setSize (width, height) { if (this._ready) this._player.setSize(width, height) else this._queueCommand('setSize', width, height) } setPlaybackRate (rate) { if (this._ready) this._player.setPlaybackRate(rate) else this._queueCommand('setPlaybackRate', rate) } setPlaybackQuality (suggestedQuality) { if (this._ready) this._player.setPlaybackQuality(suggestedQuality) else this._queueCommand('setPlaybackQuality', suggestedQuality) } getPlaybackRate () { return (this._ready && this._player.getPlaybackRate()) || 1 } getAvailablePlaybackRates () { return (this._ready && this._player.getAvailablePlaybackRates()) || [1] } getDuration () { return (this._ready && this._player.getDuration()) || 0 } getProgress () { return (this._ready && this._player.getVideoLoadedFraction()) || 0 } getState () { return (this._ready && YOUTUBE_STATES[this._player.getPlayerState()]) || 'unstarted' } getCurrentTime () { return (this._ready && this._player.getCurrentTime()) || 0 } destroy () { this._destroy() } _destroy (err) { if (this.destroyed) return this.destroyed = true if (this._player) { this._player.stopVideo && this._player.stopVideo() this._player.destroy() } this.videoId = null this._id = null this._opts = null this._api = null this._player = null this._ready = false this._queue = null this._stopInterval() this.removeListener('playing', this._startInterval) this.removeListener('paused', this._stopInterval) this.removeListener('buffering', this._stopInterval) this.removeListener('unstarted', this._stopInterval) this.removeListener('ended', this._stopInterval) if (err) this.emit('error', err) } _queueCommand (command, ...args) { if (this.destroyed) return this._queue.push([command, args]) } _flushQueue () { while (this._queue.length) { var command = this._queue.shift() this[command[0]].apply(this, command[1]) } } _loadIframeAPI (cb) { // If API is loaded, there is nothing else to do if (window.YT && typeof window.YT.Player === 'function') { return cb(null, window.YT) } // Otherwise, queue callback until API is loaded loadIframeAPICallbacks.push(cb) var scripts = Array.from(document.getElementsByTagName('script')) var isLoading = scripts.some(script => script.src === YOUTUBE_IFRAME_API_SRC) // If API