diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/controls.js | 27 | ||||
-rw-r--r-- | src/js/defaults.js | 1 | ||||
-rw-r--r-- | src/js/listeners.js | 11 | ||||
-rw-r--r-- | src/js/media.js | 2 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 5 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 11 | ||||
-rw-r--r-- | src/js/plyr.js | 192 | ||||
-rw-r--r-- | src/js/ui.js | 6 | ||||
-rw-r--r-- | src/js/utils.js | 15 |
9 files changed, 206 insertions, 64 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index 24c8f72d..fd3e5c29 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -429,21 +429,8 @@ const controls = { const tab = this.elements.settings.tabs[setting]; const pane = this.elements.settings.panes[setting]; - if (utils.is.htmlElement(tab)) { - if (toggle) { - tab.removeAttribute('hidden'); - } else { - tab.setAttribute('hidden', ''); - } - } - - if (utils.is.htmlElement(pane)) { - if (toggle) { - pane.removeAttribute('hidden'); - } else { - pane.setAttribute('hidden', ''); - } - } + utils.toggleHidden(tab, !toggle); + utils.toggleHidden(pane, !toggle); }, // Set the YouTube quality menu @@ -621,8 +608,8 @@ const controls = { const list = this.elements.settings.panes.loop.querySelector('ul'); // Show the pane and tab - this.elements.settings.tabs.loop.removeAttribute('hidden'); - this.elements.settings.panes.loop.removeAttribute('hidden'); + utils.toggleHidden(this.elements.settings.tabs.loop, false); + utils.toggleHidden(this.elements.settings.panes.loop, false); // Toggle the pane and tab const toggle = !utils.is.empty(this.loop.options); @@ -746,8 +733,8 @@ const controls = { const list = this.elements.settings.panes.speed.querySelector('ul'); // Show the pane and tab - this.elements.settings.tabs.speed.removeAttribute('hidden'); - this.elements.settings.panes.speed.removeAttribute('hidden'); + utils.toggleHidden(this.elements.settings.tabs.speed, false); + utils.toggleHidden(this.elements.settings.panes.speed, false); // Empty the menu utils.emptyElement(list); @@ -1015,6 +1002,8 @@ const controls = { volume.appendChild(range.label); volume.appendChild(range.input); + this.elements.volume = volume; + container.appendChild(volume); } diff --git a/src/js/defaults.js b/src/js/defaults.js index d7259bcc..152e0661 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -140,7 +140,6 @@ const defaults = { unmute: 'Unmute', enableCaptions: 'Enable captions', disableCaptions: 'Disable captions', - fullscreen: 'Fullscreen', enterFullscreen: 'Enter fullscreen', exitFullscreen: 'Exit fullscreen', frameTitle: 'Player for {title}', diff --git a/src/js/listeners.js b/src/js/listeners.js index 69b32814..46bce967 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -232,6 +232,13 @@ const listeners = { // Display duration utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event)); + // Check for audio tracks on load + // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point + utils.on(this.media, 'loadeddata', () => { + utils.toggleHidden(this.elements.volume, !this.hasAudio); + utils.toggleHidden(this.elements.buttons.mute, !this.hasAudio); + }); + // Handle the media finishing utils.on(this.media, 'ended', () => { // Show poster on end @@ -251,10 +258,10 @@ const listeners = { utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event)); // Handle native play/pause - utils.on(this.media, 'play pause ended', event => ui.checkPlaying.call(this, event)); + utils.on(this.media, 'playing play pause ended', event => ui.checkPlaying.call(this, event)); // Loading - utils.on(this.media, 'waiting canplay seeked', event => ui.checkLoading.call(this, event)); + utils.on(this.media, 'stalled waiting canplay seeked playing', event => ui.checkLoading.call(this, event)); // Click video if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') { diff --git a/src/js/media.js b/src/js/media.js index 2f2146a2..85a06021 100644 --- a/src/js/media.js +++ b/src/js/media.js @@ -78,7 +78,7 @@ const media = { default: break; } - } else { + } else if (this.isHTML5) { ui.setTitle.call(this); } }, diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 9b62e844..5c34a7ca 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -258,8 +258,11 @@ const vimeo = { }); player.embed.on('play', () => { + // Only fire play if paused before + if (player.media.paused) { + utils.dispatchEvent.call(player, player.media, 'play'); + } player.media.paused = false; - utils.dispatchEvent.call(player, player.media, 'play'); utils.dispatchEvent.call(player, player.media, 'playing'); }); diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index c39b1785..9e02bd37 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -352,15 +352,18 @@ const youtube = { break; case 1: - player.media.paused = false; - player.media.seeking = false; - // If we were seeking, fire seeked event if (player.media.seeking) { utils.dispatchEvent.call(player, player.media, 'seeked'); } + player.media.seeking = false; + + // Only fire play if paused before + if (player.media.paused) { + utils.dispatchEvent.call(player, player.media, 'play'); + } + player.media.paused = false; - utils.dispatchEvent.call(player, player.media, 'play'); utils.dispatchEvent.call(player, player.media, 'playing'); // Poll to get playback progress diff --git a/src/js/plyr.js b/src/js/plyr.js index 83c1b7b8..52255b16 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -279,14 +279,30 @@ class Plyr { return this; } + /** + * Get paused state + */ get paused() { return this.media.paused; } + /** + * Get playing state + */ get playing() { - return this.currentTime > 0 && !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true); + // Because the third party players don't fire timeupdate as frequently as HTML5, + // we can't use the check for currentTime > 0 for those players which is a shame + // readystate also does not exist for the embedded players + if (this.isHTML5) { + return !this.paused && !this.ended && this.currentTime > 0 && this.media.readyState > 2; + } + + return !this.paused && !this.ended; } + /** + * Get ended state + */ get ended() { return this.media.ended; } @@ -362,10 +378,16 @@ class Plyr { this.console.log(`Seeking to ${this.currentTime} seconds`); } + /** + * Get current time + */ get currentTime() { return Number(this.media.currentTime); } + /** + * Get seeking status + */ get seeking() { return this.media.seeking; } @@ -435,21 +457,30 @@ class Plyr { return this.media.volume; } - // Increase volume + /** + * Increase volume + * @param {boolean} step - How much to decrease by (between 0 and 1) + */ increaseVolume(step) { const volume = this.media.muted ? 0 : this.volume; this.volume = volume + utils.is.number(step) ? step : 1; return this; } - // Decrease volume + /** + * Decrease volume + * @param {boolean} step - How much to decrease by (between 0 and 1) + */ decreaseVolume(step) { const volume = this.media.muted ? 0 : this.volume; this.volume = volume - utils.is.number(step) ? step : 1; return this; } - // Toggle mute + /** + * Set muted state + * @param {boolean} mute + */ set muted(mute) { let toggle = mute; @@ -470,11 +501,34 @@ class Plyr { this.media.muted = toggle; } + /** + * Get current muted state + */ get muted() { return this.media.muted; } - // Playback speed + /** + * Check if the media has audio + */ + get hasAudio() { + // Assume yes for all non HTML5 (as we can't tell...) + if (!this.isHTML5) { + return true; + } + + // Get audio tracks + return ( + this.media.mozHasAudio || + Boolean(this.media.webkitAudioDecodedByteCount) || + Boolean(this.media.audioTracks && this.media.audioTracks.length) + ); + } + + /** + * Set playback speed + * @param {decimal} speed - the speed of playback (0.5-2.0) + */ set speed(input) { let speed = null; @@ -506,17 +560,24 @@ class Plyr { this.media.playbackRate = speed; } + /** + * Get current playback speed + */ get speed() { return this.media.playbackRate; } - // Set playback quality + /** + * Set playback quality + * Currently YouTube only + * @param {string} input - Quality level + */ set quality(input) { let quality = null; if (utils.is.string(input)) { quality = input; - } else if (utils.is.number(storage.get.call(this).speed)) { + } else if (utils.is.number(storage.get.call(this).quality)) { ({ quality } = storage.get.call(this)); } else { quality = this.config.quality.selected; @@ -534,12 +595,18 @@ class Plyr { this.media.quality = quality; } + /** + * Get current quality level + */ get quality() { return this.media.quality; } - // Toggle loop - // TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config + /** + * Toggle loop + * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config + * @param {boolean} input - Whether to loop or not + */ set loop(input) { const toggle = utils.is.boolean(input) ? input : this.config.loop.active; this.config.loop.active = toggle; @@ -589,22 +656,34 @@ class Plyr { } */ } + /** + * Get current loop state + */ get loop() { return this.media.loop; } - // Media source + /** + * Set new media source + * @param {object} input - The new source object (see docs) + */ set source(input) { source.change.call(this, input); } + /** + * Get current source + */ get source() { return this.media.currentSrc; } - // Poster image + /** + * Set the poster image for a HTML5 video + * @param {input} - the URL for the new poster image + */ set poster(input) { - if (this.type !== 'video') { + if (!this.isHTML5 || this.type !== 'video') { this.console.warn('Poster can only be set on HTML5 video'); return; } @@ -614,25 +693,37 @@ class Plyr { } } + /** + * Get the current poster image + */ get poster() { - if (this.type !== 'video') { + if (!this.isHTML5 || this.type !== 'video') { return null; } return this.media.getAttribute('poster'); } - // Autoplay - get autoplay() { - return this.config.autoplay; - } - + /** + * Set the autoplay state + * @param {boolean} input - Whether to autoplay or not + */ set autoplay(input) { const toggle = utils.is.boolean(input) ? input : this.config.autoplay; this.config.autoplay = toggle; } - // Toggle captions + /** + * Get the current autoplay state + */ + get autoplay() { + return this.config.autoplay; + } + + /** + * Toggle captions + * @param {boolean} input - Whether to enable captions + */ toggleCaptions(input) { // If there's no full support, or there's no caption toggle if (!this.supported.ui || !utils.is.htmlElement(this.elements.buttons.captions)) { @@ -665,7 +756,10 @@ class Plyr { return this; } - // Caption language + /** + * Set the captions language + * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) + */ set language(input) { // Nothing specified if (!utils.is.string(input)) { @@ -701,12 +795,18 @@ class Plyr { utils.dispatchEvent.call(this, this.media, 'languagechange'); } + /** + * Get the current captions language + */ get language() { return this.captions.language; } - // Toggle fullscreen - // Requires user input event + /** + * Toggle fullscreen playback + * Requires user input event + * @param {event} event + */ toggleFullscreen(event) { // Check for native support if (fullscreen.enabled) { @@ -759,9 +859,11 @@ class Plyr { return this; } - // Toggle picture-in-picture - // TODO: update player with state, support, enabled - // TODO: detect outside changes + /** + * Toggle picture-in-picture playback on WebKit/MacOS + * TODO: update player with state, support, enabled + * TODO: detect outside changes + */ set pip(input) { const states = { pip: 'picture-in-picture', @@ -780,6 +882,9 @@ class Plyr { this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); } + /** + * Get the current picture-in-picture state + */ get pip() { if (!support.pip) { return null; @@ -788,8 +893,10 @@ class Plyr { return this.media.webkitPresentationMode; } - // Trigger airplay - // TODO: update player with state, support, enabled + /** + * Trigger the airplay dialog + * TODO: update player with state, support, enabled + */ airplay() { // Bail if no support if (!support.airplay) { @@ -802,7 +909,10 @@ class Plyr { return this; } - // Show the player controls in fullscreen mode + /** + * Toggle the player controls + * @param {boolean} toggle - Whether to show the controls + */ toggleControls(toggle) { // We need controls of course... if (!utils.is.htmlElement(this.elements.controls)) { @@ -897,25 +1007,41 @@ class Plyr { return this; } - // Event listeners + /** + * Add event listeners + * @param {string} event - Event type + * @param {function} callback - Callback for when event occurs + */ on(event, callback) { utils.on(this.elements.container, event, callback); return this; } + /** + * Remove event listeners + * @param {string} event - Event type + * @param {function} callback - Callback for when event occurs + */ off(event, callback) { utils.off(this.elements.container, event, callback); return this; } - // Check for support + /** + * Check for support for a mime type (HTML5 only) + * @param {string} type - Mime type + */ supports(type) { return support.mime.call(this, type); } - // Destroy an instance - // Event listeners are removed when elements are removed - // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory + /** + * Destroy an instance + * Event listeners are removed when elements are removed + * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory + * @param {function} callback - Callback for when destroy is complete + * @param {boolean} soft - Whether it's a soft destroy (for source changes etc) + */ destroy(callback, soft = false) { const done = () => { // Reset overflow (incase destroyed while in fullscreen) diff --git a/src/js/ui.js b/src/js/ui.js index 6246a71f..062331dc 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -144,7 +144,9 @@ const ui = { utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused); // Set aria state - Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing)); + if (utils.is.array(this.elements.buttons.play)) { + Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing)); + } // Toggle controls this.toggleControls(!this.playing); @@ -153,7 +155,7 @@ const ui = { // Check if media is loading checkLoading(event) { - this.loading = event.type === 'waiting'; + this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer clearTimeout(this.timers.loading); diff --git a/src/js/utils.js b/src/js/utils.js index 36fdaa6e..eb0a9650 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -118,7 +118,7 @@ const utils = { if (!hasId || !document.querySelectorAll(`#${id}`).length) { // Create container const container = document.createElement('div'); - container.setAttribute('hidden', ''); + utils.toggleHidden(container, true); if (hasId) { container.setAttribute('id', id); @@ -337,6 +337,19 @@ const utils = { return utils.is.htmlElement(element) && element.classList.contains(className); }, + // Toggle hidden attribute on an element + toggleHidden(element, toggle) { + if (!utils.is.htmlElement(element)) { + return; + } + + if (toggle) { + element.setAttribute('hidden', ''); + } else { + element.removeAttribute('hidden'); + } + }, + // Element matches selector matches(element, selector) { const prototype = { Element }; |