aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/controls.js27
-rw-r--r--src/js/defaults.js1
-rw-r--r--src/js/listeners.js11
-rw-r--r--src/js/media.js2
-rw-r--r--src/js/plugins/vimeo.js5
-rw-r--r--src/js/plugins/youtube.js11
-rw-r--r--src/js/plyr.js192
-rw-r--r--src/js/ui.js6
-rw-r--r--src/js/utils.js15
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 };