diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/index.js b/index.js index 52f457c..b253755 100644 --- a/index.js +++ b/index.js @@ -1,38 +1,37 @@ -/*! yt-player. MIT License. Feross Aboukhadijeh */ -const EventEmitter = require('events').EventEmitter -const loadScript = require('load-script2') +import EventEmitter from 'events' +import loadScript from 'load-script2' const YOUTUBE_IFRAME_API_SRC = 'https://www.youtube.com/iframe_api' const YOUTUBE_STATES = { - '-1': 'unstarted', - 0: 'ended', - 1: 'playing', - 2: 'paused', - 3: 'buffering', - 5: 'cued' + '-1': 'unstarted', + 0: 'ended', + 1: 'playing', + 2: 'paused', + 3: 'buffering', + 5: 'cued' } const 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 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 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 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, + // 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 + // This error is the same as 101. It's just a 101 error in disguise! + UNPLAYABLE_2: 150 } const loadIframeAPICallbacks = [] @@ -42,496 +41,496 @@ const loadIframeAPICallbacks = [] * @param {HTMLElement|selector} element */ class YouTubePlayer extends EventEmitter { - constructor (element, opts) { - super() - - const 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._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) - }) - } - - load (videoId, autoplay = false, start = 0) { - if (this.destroyed) return - - 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') - } - - 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) - } - - setVolume (volume) { - if (this._ready) this._player.setVolume(volume) - else this._queueCommand('setVolume', volume) - } - - 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) { - const 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) + constructor(element, opts) { + super() + + const 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._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) + }) + } + + load(videoId, autoplay = false, start = 0) { + if (this.destroyed) return + + 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') + } + + 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) + } + + setVolume(volume) { + if (this._ready) this._player.setVolume(volume) + else this._queueCommand('setVolume', volume) + } + + 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' } - // Otherwise, queue callback until API is loaded - loadIframeAPICallbacks.push(cb) - - const scripts = Array.from(document.getElementsByTagName('script')) - const isLoading = scripts.some(script => script.src === YOUTUBE_IFRAME_API_SRC) + 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) + } - // If API