Handle no audio, more docs in code, fix for playing getter

This commit is contained in:
Sam Potts 2017-11-21 20:14:57 +11:00
parent f33bc5a5c6
commit d3b31e595a
12 changed files with 209 additions and 66 deletions

2
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -329,6 +329,7 @@ Property | Getter | Setter | Description
`duration` | ✔ | - | Returns the duration for the current media. `duration` | ✔ | - | Returns the duration for the current media.
`volume` | ✔ | ✔ | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. `volume` | ✔ | ✔ | Gets or sets the volume for the player. The setter accepts a float between 0 and 1.
`muted` | ✔ | ✔ | Gets or sets the muted state of the player. The setter accepts a boolean. `muted` | ✔ | ✔ | Gets or sets the muted state of the player. The setter accepts a boolean.
`hasAudio` | ✔ | - | Returns a boolean indicating if the current media has an audio track.
`speed` | ✔ | ✔ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. `speed` | ✔ | ✔ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5.
`quality`¹ | ✔ | ✔ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. `quality`¹ | ✔ | ✔ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config.
`loop` | ✔ | ✔ | Gets or sets the current loop state of the player. The setter accepts a boolean. `loop` | ✔ | ✔ | Gets or sets the current loop state of the player. The setter accepts a boolean.

27
src/js/controls.js vendored
View File

@ -429,21 +429,8 @@ const controls = {
const tab = this.elements.settings.tabs[setting]; const tab = this.elements.settings.tabs[setting];
const pane = this.elements.settings.panes[setting]; const pane = this.elements.settings.panes[setting];
if (utils.is.htmlElement(tab)) { utils.toggleHidden(tab, !toggle);
if (toggle) { utils.toggleHidden(pane, !toggle);
tab.removeAttribute('hidden');
} else {
tab.setAttribute('hidden', '');
}
}
if (utils.is.htmlElement(pane)) {
if (toggle) {
pane.removeAttribute('hidden');
} else {
pane.setAttribute('hidden', '');
}
}
}, },
// Set the YouTube quality menu // Set the YouTube quality menu
@ -621,8 +608,8 @@ const controls = {
const list = this.elements.settings.panes.loop.querySelector('ul'); const list = this.elements.settings.panes.loop.querySelector('ul');
// Show the pane and tab // Show the pane and tab
this.elements.settings.tabs.loop.removeAttribute('hidden'); utils.toggleHidden(this.elements.settings.tabs.loop, false);
this.elements.settings.panes.loop.removeAttribute('hidden'); utils.toggleHidden(this.elements.settings.panes.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !utils.is.empty(this.loop.options); const toggle = !utils.is.empty(this.loop.options);
@ -746,8 +733,8 @@ const controls = {
const list = this.elements.settings.panes.speed.querySelector('ul'); const list = this.elements.settings.panes.speed.querySelector('ul');
// Show the pane and tab // Show the pane and tab
this.elements.settings.tabs.speed.removeAttribute('hidden'); utils.toggleHidden(this.elements.settings.tabs.speed, false);
this.elements.settings.panes.speed.removeAttribute('hidden'); utils.toggleHidden(this.elements.settings.panes.speed, false);
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
@ -1015,6 +1002,8 @@ const controls = {
volume.appendChild(range.label); volume.appendChild(range.label);
volume.appendChild(range.input); volume.appendChild(range.input);
this.elements.volume = volume;
container.appendChild(volume); container.appendChild(volume);
} }

View File

@ -140,7 +140,6 @@ const defaults = {
unmute: 'Unmute', unmute: 'Unmute',
enableCaptions: 'Enable captions', enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions', disableCaptions: 'Disable captions',
fullscreen: 'Fullscreen',
enterFullscreen: 'Enter fullscreen', enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen', exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}', frameTitle: 'Player for {title}',

View File

@ -232,6 +232,13 @@ const listeners = {
// Display duration // Display duration
utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event)); 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 // Handle the media finishing
utils.on(this.media, 'ended', () => { utils.on(this.media, 'ended', () => {
// Show poster on end // Show poster on end
@ -251,10 +258,10 @@ const listeners = {
utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event)); utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event));
// Handle native play/pause // 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 // 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 // Click video
if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') { if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') {

View File

@ -78,7 +78,7 @@ const media = {
default: default:
break; break;
} }
} else { } else if (this.isHTML5) {
ui.setTitle.call(this); ui.setTitle.call(this);
} }
}, },

View File

@ -258,8 +258,11 @@ const vimeo = {
}); });
player.embed.on('play', () => { 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; player.media.paused = false;
utils.dispatchEvent.call(player, player.media, 'play');
utils.dispatchEvent.call(player, player.media, 'playing'); utils.dispatchEvent.call(player, player.media, 'playing');
}); });

View File

@ -352,15 +352,18 @@ const youtube = {
break; break;
case 1: case 1:
player.media.paused = false;
player.media.seeking = false;
// If we were seeking, fire seeked event // If we were seeking, fire seeked event
if (player.media.seeking) { if (player.media.seeking) {
utils.dispatchEvent.call(player, player.media, 'seeked'); 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'); utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress // Poll to get playback progress

View File

@ -279,14 +279,30 @@ class Plyr {
return this; return this;
} }
/**
* Get paused state
*/
get paused() { get paused() {
return this.media.paused; return this.media.paused;
} }
/**
* Get playing state
*/
get playing() { 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() { get ended() {
return this.media.ended; return this.media.ended;
} }
@ -362,10 +378,16 @@ class Plyr {
this.console.log(`Seeking to ${this.currentTime} seconds`); this.console.log(`Seeking to ${this.currentTime} seconds`);
} }
/**
* Get current time
*/
get currentTime() { get currentTime() {
return Number(this.media.currentTime); return Number(this.media.currentTime);
} }
/**
* Get seeking status
*/
get seeking() { get seeking() {
return this.media.seeking; return this.media.seeking;
} }
@ -435,21 +457,30 @@ class Plyr {
return this.media.volume; return this.media.volume;
} }
// Increase volume /**
* Increase volume
* @param {boolean} step - How much to decrease by (between 0 and 1)
*/
increaseVolume(step) { increaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume; const volume = this.media.muted ? 0 : this.volume;
this.volume = volume + utils.is.number(step) ? step : 1; this.volume = volume + utils.is.number(step) ? step : 1;
return this; return this;
} }
// Decrease volume /**
* Decrease volume
* @param {boolean} step - How much to decrease by (between 0 and 1)
*/
decreaseVolume(step) { decreaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume; const volume = this.media.muted ? 0 : this.volume;
this.volume = volume - utils.is.number(step) ? step : 1; this.volume = volume - utils.is.number(step) ? step : 1;
return this; return this;
} }
// Toggle mute /**
* Set muted state
* @param {boolean} mute
*/
set muted(mute) { set muted(mute) {
let toggle = mute; let toggle = mute;
@ -470,11 +501,34 @@ class Plyr {
this.media.muted = toggle; this.media.muted = toggle;
} }
/**
* Get current muted state
*/
get muted() { get muted() {
return this.media.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) { set speed(input) {
let speed = null; let speed = null;
@ -506,17 +560,24 @@ class Plyr {
this.media.playbackRate = speed; this.media.playbackRate = speed;
} }
/**
* Get current playback speed
*/
get speed() { get speed() {
return this.media.playbackRate; return this.media.playbackRate;
} }
// Set playback quality /**
* Set playback quality
* Currently YouTube only
* @param {string} input - Quality level
*/
set quality(input) { set quality(input) {
let quality = null; let quality = null;
if (utils.is.string(input)) { if (utils.is.string(input)) {
quality = 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)); ({ quality } = storage.get.call(this));
} else { } else {
quality = this.config.quality.selected; quality = this.config.quality.selected;
@ -534,12 +595,18 @@ class Plyr {
this.media.quality = quality; this.media.quality = quality;
} }
/**
* Get current quality level
*/
get quality() { get quality() {
return this.media.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) { set loop(input) {
const toggle = utils.is.boolean(input) ? input : this.config.loop.active; const toggle = utils.is.boolean(input) ? input : this.config.loop.active;
this.config.loop.active = toggle; this.config.loop.active = toggle;
@ -589,22 +656,34 @@ class Plyr {
} */ } */
} }
/**
* Get current loop state
*/
get loop() { get loop() {
return this.media.loop; return this.media.loop;
} }
// Media source /**
* Set new media source
* @param {object} input - The new source object (see docs)
*/
set source(input) { set source(input) {
source.change.call(this, input); source.change.call(this, input);
} }
/**
* Get current source
*/
get source() { get source() {
return this.media.currentSrc; 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) { set poster(input) {
if (this.type !== 'video') { if (!this.isHTML5 || this.type !== 'video') {
this.console.warn('Poster can only be set on HTML5 video'); this.console.warn('Poster can only be set on HTML5 video');
return; return;
} }
@ -614,25 +693,37 @@ class Plyr {
} }
} }
/**
* Get the current poster image
*/
get poster() { get poster() {
if (this.type !== 'video') { if (!this.isHTML5 || this.type !== 'video') {
return null; return null;
} }
return this.media.getAttribute('poster'); return this.media.getAttribute('poster');
} }
// Autoplay /**
get autoplay() { * Set the autoplay state
return this.config.autoplay; * @param {boolean} input - Whether to autoplay or not
} */
set autoplay(input) { set autoplay(input) {
const toggle = utils.is.boolean(input) ? input : this.config.autoplay; const toggle = utils.is.boolean(input) ? input : this.config.autoplay;
this.config.autoplay = toggle; 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) { toggleCaptions(input) {
// If there's no full support, or there's no caption toggle // If there's no full support, or there's no caption toggle
if (!this.supported.ui || !utils.is.htmlElement(this.elements.buttons.captions)) { if (!this.supported.ui || !utils.is.htmlElement(this.elements.buttons.captions)) {
@ -665,7 +756,10 @@ class Plyr {
return this; return this;
} }
// Caption language /**
* Set the captions language
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/
set language(input) { set language(input) {
// Nothing specified // Nothing specified
if (!utils.is.string(input)) { if (!utils.is.string(input)) {
@ -701,12 +795,18 @@ class Plyr {
utils.dispatchEvent.call(this, this.media, 'languagechange'); utils.dispatchEvent.call(this, this.media, 'languagechange');
} }
/**
* Get the current captions language
*/
get language() { get language() {
return this.captions.language; return this.captions.language;
} }
// Toggle fullscreen /**
// Requires user input event * Toggle fullscreen playback
* Requires user input event
* @param {event} event
*/
toggleFullscreen(event) { toggleFullscreen(event) {
// Check for native support // Check for native support
if (fullscreen.enabled) { if (fullscreen.enabled) {
@ -759,9 +859,11 @@ class Plyr {
return this; return this;
} }
// Toggle picture-in-picture /**
// TODO: update player with state, support, enabled * Toggle picture-in-picture playback on WebKit/MacOS
// TODO: detect outside changes * TODO: update player with state, support, enabled
* TODO: detect outside changes
*/
set pip(input) { set pip(input) {
const states = { const states = {
pip: 'picture-in-picture', pip: 'picture-in-picture',
@ -780,6 +882,9 @@ class Plyr {
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
} }
/**
* Get the current picture-in-picture state
*/
get pip() { get pip() {
if (!support.pip) { if (!support.pip) {
return null; return null;
@ -788,8 +893,10 @@ class Plyr {
return this.media.webkitPresentationMode; 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() { airplay() {
// Bail if no support // Bail if no support
if (!support.airplay) { if (!support.airplay) {
@ -802,7 +909,10 @@ class Plyr {
return this; return this;
} }
// Show the player controls in fullscreen mode /**
* Toggle the player controls
* @param {boolean} toggle - Whether to show the controls
*/
toggleControls(toggle) { toggleControls(toggle) {
// We need controls of course... // We need controls of course...
if (!utils.is.htmlElement(this.elements.controls)) { if (!utils.is.htmlElement(this.elements.controls)) {
@ -897,25 +1007,41 @@ class Plyr {
return this; return this;
} }
// Event listeners /**
* Add event listeners
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
on(event, callback) { on(event, callback) {
utils.on(this.elements.container, event, callback); utils.on(this.elements.container, event, callback);
return this; return this;
} }
/**
* Remove event listeners
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
off(event, callback) { off(event, callback) {
utils.off(this.elements.container, event, callback); utils.off(this.elements.container, event, callback);
return this; return this;
} }
// Check for support /**
* Check for support for a mime type (HTML5 only)
* @param {string} type - Mime type
*/
supports(type) { supports(type) {
return support.mime.call(this, type); return support.mime.call(this, type);
} }
// Destroy an instance /**
// Event listeners are removed when elements are removed * Destroy an instance
// http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory * 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) { destroy(callback, soft = false) {
const done = () => { const done = () => {
// Reset overflow (incase destroyed while in fullscreen) // Reset overflow (incase destroyed while in fullscreen)

View File

@ -144,7 +144,9 @@ const ui = {
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused); utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
// Set aria state // 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 // Toggle controls
this.toggleControls(!this.playing); this.toggleControls(!this.playing);
@ -153,7 +155,7 @@ const ui = {
// Check if media is loading // Check if media is loading
checkLoading(event) { checkLoading(event) {
this.loading = event.type === 'waiting'; this.loading = ['stalled', 'waiting'].includes(event.type);
// Clear timer // Clear timer
clearTimeout(this.timers.loading); clearTimeout(this.timers.loading);

View File

@ -118,7 +118,7 @@ const utils = {
if (!hasId || !document.querySelectorAll(`#${id}`).length) { if (!hasId || !document.querySelectorAll(`#${id}`).length) {
// Create container // Create container
const container = document.createElement('div'); const container = document.createElement('div');
container.setAttribute('hidden', ''); utils.toggleHidden(container, true);
if (hasId) { if (hasId) {
container.setAttribute('id', id); container.setAttribute('id', id);
@ -337,6 +337,19 @@ const utils = {
return utils.is.htmlElement(element) && element.classList.contains(className); 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 // Element matches selector
matches(element, selector) { matches(element, selector) {
const prototype = { Element }; const prototype = { Element };