aboutsummaryrefslogtreecommitdiffstats
path: root/src/js/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/plugins')
-rw-r--r--src/js/plugins/ads.js59
-rw-r--r--src/js/plugins/vimeo.js161
-rw-r--r--src/js/plugins/youtube.js256
3 files changed, 228 insertions, 248 deletions
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js
index 0246e221..375fdc13 100644
--- a/src/js/plugins/ads.js
+++ b/src/js/plugins/ads.js
@@ -6,8 +6,13 @@
/* global google */
-import i18n from '../i18n';
-import utils from '../utils';
+import { createElement } from '../utils/elements';
+import { triggerEvent } from '../utils/events';
+import i18n from '../utils/i18n';
+import is from '../utils/is';
+import loadScript from '../utils/loadScript';
+import { formatTime } from '../utils/time';
+import { buildUrlParams } from '../utils/urls';
class Ads {
/**
@@ -44,7 +49,9 @@ class Ads {
}
get enabled() {
- return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
+ return (
+ this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId)
+ );
}
/**
@@ -53,9 +60,8 @@ class Ads {
load() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
- if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
- utils
- .loadScript(this.player.config.urls.googleIMA.sdk)
+ if (!is.object(window.google) || !is.object(window.google.ima)) {
+ loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@@ -94,7 +100,7 @@ class Ads {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
- AV_URL: location.hostname,
+ AV_URL: window.location.hostname,
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
@@ -103,7 +109,7 @@ class Ads {
const base = 'https://go.aniview.com/api/adserver6/vast/';
- return `${base}?${utils.buildUrlParams(params)}`;
+ return `${base}?${buildUrlParams(params)}`;
}
/**
@@ -116,7 +122,7 @@ class Ads {
*/
setupIMA() {
// Create the container for our advertisements
- this.elements.container = utils.createElement('div', {
+ this.elements.container = createElement('div', {
class: this.player.config.classNames.ads,
});
this.player.elements.container.appendChild(this.elements.container);
@@ -146,7 +152,11 @@ class Ads {
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events
- this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);
+ this.loader.addEventListener(
+ google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
+ event => this.onAdsManagerLoaded(event),
+ false,
+ );
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads
@@ -184,7 +194,7 @@ class Ads {
}
const update = () => {
- const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
+ const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label);
};
@@ -197,6 +207,11 @@ class Ads {
* @param {Event} adsManagerLoadedEvent
*/
onAdsManagerLoaded(event) {
+ // Load could occur after a source change (race condition)
+ if (!this.enabled) {
+ return;
+ }
+
// Get the ads manager
const settings = new google.ima.AdsRenderingSettings();
@@ -212,14 +227,14 @@ class Ads {
this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available
- if (!utils.is.empty(this.cuePoints)) {
+ if (!is.empty(this.cuePoints)) {
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
- if (utils.is.element(seekElement)) {
+ if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint;
- const cue = utils.createElement('span', {
+ const cue = createElement('span', {
class: this.player.config.classNames.cues,
});
@@ -230,10 +245,6 @@ class Ads {
});
}
- // Get skippable state
- // TODO: Skip button
- // this.player.debug.warn(this.manager.getAdSkippableState());
-
// Set volume to match player
this.manager.setVolume(this.player.volume);
@@ -266,7 +277,7 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
- utils.dispatchEvent.call(this.player, this.player.media, event);
+ triggerEvent.call(this.player, this.player.media, event);
};
switch (event.type) {
@@ -393,7 +404,7 @@ class Ads {
this.player.on('seeked', () => {
const seekedTime = this.player.currentTime;
- if (utils.is.empty(this.cuePoints)) {
+ if (is.empty(this.cuePoints)) {
return;
}
@@ -530,9 +541,9 @@ class Ads {
trigger(event, ...args) {
const handlers = this.events[event];
- if (utils.is.array(handlers)) {
+ if (is.array(handlers)) {
handlers.forEach(handler => {
- if (utils.is.function(handler)) {
+ if (is.function(handler)) {
handler.apply(this, args);
}
});
@@ -546,7 +557,7 @@ class Ads {
* @return {Ads}
*/
on(event, callback) {
- if (!utils.is.array(this.events[event])) {
+ if (!is.array(this.events[event])) {
this.events[event] = [];
}
@@ -577,7 +588,7 @@ class Ads {
* @param {string} from
*/
clearSafetyTimer(from) {
- if (!utils.is.nullOrUndefined(this.safetyTimer)) {
+ if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer);
diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js
index 46d4f3f9..2d9ba6e2 100644
--- a/src/js/plugins/vimeo.js
+++ b/src/js/plugins/vimeo.js
@@ -2,31 +2,60 @@
// Vimeo plugin
// ==========================================================================
-import captions from './../captions';
-import controls from './../controls';
-import ui from './../ui';
-import utils from './../utils';
+import captions from '../captions';
+import controls from '../controls';
+import ui from '../ui';
+import { createElement, replaceElement, toggleClass } from '../utils/elements';
+import { triggerEvent } from '../utils/events';
+import fetch from '../utils/fetch';
+import is from '../utils/is';
+import loadScript from '../utils/loadScript';
+import { format, stripHTML } from '../utils/strings';
+import { buildUrlParams } from '../utils/urls';
+
+// Parse Vimeo ID from URL
+function parseId(url) {
+ if (is.empty(url)) {
+ return null;
+ }
+
+ if (is.number(Number(url))) {
+ return url;
+ }
+
+ const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
+ return url.match(regex) ? RegExp.$2 : url;
+}
+
+// Get aspect ratio for dimensions
+function getAspectRatio(width, height) {
+ const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
+ const ratio = getRatio(width, height);
+ return `${width / ratio}:${height / ratio}`;
+}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
+ if (play && !this.embed.hasPlayed) {
+ this.embed.hasPlayed = true;
+ }
if (this.media.paused === play) {
this.media.paused = !play;
- utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = {
setup() {
// Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set intial ratio
vimeo.setAspectRatio.call(this);
// Load the API if not already
- if (!utils.is.object(window.Vimeo)) {
- utils
- .loadScript(this.config.urls.vimeo.sdk)
+ if (!is.object(window.Vimeo)) {
+ loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@@ -41,8 +70,9 @@ const vimeo = {
// Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
setAspectRatio(input) {
- const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
- const padding = 100 / ratio[0] * ratio[1];
+ const [x, y] = (is.string(input) ? input : this.config.ratio).split(':').map(Number);
+ const padding = (100 / x) * y;
+ vimeo.padding = padding;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
if (this.supported.ui) {
@@ -70,34 +100,37 @@ const vimeo = {
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
- const params = utils.buildUrlParams(options);
+ const params = buildUrlParams(options);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
- if (utils.is.empty(source)) {
+ if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
- const id = utils.parseVimeoId(source);
+ const id = parseId(source);
// Build an iframe
- const iframe = utils.createElement('iframe');
- const src = utils.format(player.config.urls.vimeo.iframe, id, params);
+ const iframe = createElement('iframe');
+ const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
+ // Get poster, if already set
+ const { poster } = player;
+
// Inject the package
- const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
+ const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
- player.media = utils.replaceElement(wrapper, player.media);
+ player.media = replaceElement(wrapper, player.media);
// Get poster image
- utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
- if (utils.is.empty(response)) {
+ fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
+ if (is.empty(response)) {
return;
}
@@ -108,7 +141,7 @@ const vimeo = {
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster
- ui.setPoster.call(player, url.href);
+ ui.setPoster.call(player, url.href).catch(() => {});
});
// Setup instance
@@ -153,19 +186,20 @@ const vimeo = {
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
+ const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event
media.seeking = true;
- utils.dispatchEvent.call(player, media, 'seeking');
+ triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
- Promise.resolve(paused && embed.setVolume(0))
+ Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
- .then(() => paused && embed.pause())
+ .then(() => restorePause && embed.pause())
// Restore volume
- .then(() => paused && embed.setVolume(volume))
+ .then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
@@ -183,7 +217,7 @@ const vimeo = {
.setPlaybackRate(input)
.then(() => {
speed = input;
- utils.dispatchEvent.call(player, player.media, 'ratechange');
+ triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
@@ -203,7 +237,7 @@ const vimeo = {
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
- utils.dispatchEvent.call(player, player.media, 'volumechange');
+ triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@@ -215,11 +249,11 @@ const vimeo = {
return muted;
},
set(input) {
- const toggle = utils.is.boolean(input) ? input : false;
+ const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
- utils.dispatchEvent.call(player, player.media, 'volumechange');
+ triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@@ -231,7 +265,7 @@ const vimeo = {
return loop;
},
set(input) {
- const toggle = utils.is.boolean(input) ? input : player.config.loop.active;
+ const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
@@ -245,6 +279,7 @@ const vimeo = {
.getVideoUrl()
.then(value => {
currentSrc = value;
+ controls.setDownloadLink.call(player);
})
.catch(error => {
this.debug.warn(error);
@@ -264,12 +299,9 @@ const vimeo = {
});
// Set aspect ratio based on video size
- Promise.all([
- player.embed.getVideoWidth(),
- player.embed.getVideoHeight(),
- ]).then(dimensions => {
- const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
- vimeo.setAspectRatio.call(this, ratio);
+ Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
+ vimeo.ratio = getAspectRatio(dimensions[0], dimensions[1]);
+ vimeo.setAspectRatio.call(this, vimeo.ratio);
});
// Set autopause
@@ -286,13 +318,13 @@ const vimeo = {
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'timeupdate');
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
- utils.dispatchEvent.call(player, player.media, 'durationchange');
+ triggerEvent.call(player, player.media, 'durationchange');
});
// Get captions
@@ -301,18 +333,21 @@ const vimeo = {
captions.setup.call(player);
});
- player.embed.on('cuechange', data => {
- let cue = null;
-
- if (data.cues.length) {
- cue = utils.stripHTML(data.cues[0].text);
- }
-
- captions.setText.call(player, cue);
+ player.embed.on('cuechange', ({ cues = [] }) => {
+ const strippedCues = cues.map(cue => stripHTML(cue.text));
+ captions.updateCues.call(player, strippedCues);
});
player.embed.on('loaded', () => {
- if (utils.is.element(player.embed.element) && player.supported.ui) {
+ // Assure state and events are updated on autoplay
+ player.embed.getPaused().then(paused => {
+ assurePlaybackState.call(player, !paused);
+ if (!paused) {
+ triggerEvent.call(player, player.media, 'playing');
+ }
+ });
+
+ if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
// Fix keyboard focus issues
@@ -323,7 +358,7 @@ const vimeo = {
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
- utils.dispatchEvent.call(player, player.media, 'playing');
+ triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => {
@@ -333,16 +368,16 @@ const vimeo = {
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => {
player.media.buffered = data.percent;
- utils.dispatchEvent.call(player, player.media, 'progress');
+ triggerEvent.call(player, player.media, 'progress');
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
- utils.dispatchEvent.call(player, player.media, 'canplaythrough');
+ triggerEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
@@ -350,24 +385,40 @@ const vimeo = {
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
- utils.dispatchEvent.call(player, player.media, 'durationchange');
+ triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
- utils.dispatchEvent.call(player, player.media, 'seeked');
+ triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
- utils.dispatchEvent.call(player, player.media, 'ended');
+ triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
- utils.dispatchEvent.call(player, player.media, 'error');
+ triggerEvent.call(player, player.media, 'error');
+ });
+
+ // Set height/width on fullscreen
+ player.on('enterfullscreen exitfullscreen', event => {
+ const { target } = player.fullscreen;
+
+ // Ignore for iOS native
+ if (target !== player.elements.container) {
+ return;
+ }
+
+ const toggle = event.type === 'enterfullscreen';
+ const [x, y] = vimeo.ratio.split(':').map(Number);
+ const dimension = x > y ? 'width' : 'height';
+
+ target.style[dimension] = toggle ? `${vimeo.padding}%` : null;
});
// Rebuild UI
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index 67b8093e..73175c14 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -2,90 +2,50 @@
// YouTube plugin
// ==========================================================================
-import controls from './../controls';
-import ui from './../ui';
-import utils from './../utils';
-
-// Standardise YouTube quality unit
-function mapQualityUnit(input) {
- switch (input) {
- case 'hd2160':
- return 2160;
-
- case 2160:
- return 'hd2160';
-
- case 'hd1440':
- return 1440;
-
- case 1440:
- return 'hd1440';
-
- case 'hd1080':
- return 1080;
-
- case 1080:
- return 'hd1080';
-
- case 'hd720':
- return 720;
-
- case 720:
- return 'hd720';
-
- case 'large':
- return 480;
-
- case 480:
- return 'large';
-
- case 'medium':
- return 360;
-
- case 360:
- return 'medium';
-
- case 'small':
- return 240;
-
- case 240:
- return 'small';
-
- default:
- return 'default';
- }
-}
-
-function mapQualityUnits(levels) {
- if (utils.is.empty(levels)) {
- return levels;
+import ui from '../ui';
+import { createElement, replaceElement, toggleClass } from '../utils/elements';
+import { triggerEvent } from '../utils/events';
+import fetch from '../utils/fetch';
+import is from '../utils/is';
+import loadImage from '../utils/loadImage';
+import loadScript from '../utils/loadScript';
+import { format, generateId } from '../utils/strings';
+
+// Parse YouTube ID from URL
+function parseId(url) {
+ if (is.empty(url)) {
+ return null;
}
- return utils.dedupe(levels.map(level => mapQualityUnit(level)));
+ const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
+ return url.match(regex) ? RegExp.$2 : url;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
+ if (play && !this.embed.hasPlayed) {
+ this.embed.hasPlayed = true;
+ }
if (this.media.paused === play) {
this.media.paused = !play;
- utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const youtube = {
setup() {
// Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
youtube.setAspectRatio.call(this);
// Setup API
- if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
+ if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
- utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
+ loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@@ -112,10 +72,10 @@ const youtube = {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
- if (utils.is.function(this.embed.getVideoData)) {
+ if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
- if (utils.is.empty(title)) {
+ if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
@@ -124,13 +84,12 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
- if (utils.is.string(key) && !utils.is.empty(key)) {
- const url = utils.format(this.config.urls.youtube.api, videoId, key);
+ if (is.string(key) && !is.empty(key)) {
+ const url = format(this.config.urls.youtube.api, videoId, key);
- utils
- .fetch(url)
+ fetch(url)
.then(result => {
- if (utils.is.object(result)) {
+ if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@@ -151,7 +110,7 @@ const youtube = {
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
- if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
+ if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@@ -159,30 +118,36 @@ const youtube = {
let source = player.media.getAttribute('src');
// Get from <div> if needed
- if (utils.is.empty(source)) {
+ if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
- const videoId = utils.parseYouTubeId(source);
- const id = utils.generateId(player.provider);
- const container = utils.createElement('div', { id });
- player.media = utils.replaceElement(container, player.media);
+ const videoId = parseId(source);
+ const id = generateId(player.provider);
+
+ // Get poster, if already set
+ const { poster } = player;
+
+ // Replace media element
+ const container = createElement('div', { id, poster });
+ player.media = replaceElement(container, player.media);
- // Set poster image
+ // Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
- utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
- .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
- .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
+ loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
+ .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
+ .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
- });
+ })
+ .catch(() => {});
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
@@ -190,6 +155,7 @@ const youtube = {
videoId,
playerVars: {
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
+ hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
rel: 0, // No related vids
showinfo: 0, // Hide info
@@ -208,51 +174,23 @@ const youtube = {
},
events: {
onError(event) {
- // If we've already fired an error, don't do it again
- // YouTube fires onError twice
- if (utils.is.object(player.media.error)) {
- return;
+ // YouTube may fire onError twice, so only handle it once
+ if (!player.media.error) {
+ const code = event.data;
+ // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
+ const message =
+ {
+ 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
+ 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
+ 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.',
+ 101: 'The owner of the requested video does not allow it to be played in embedded players.',
+ 150: 'The owner of the requested video does not allow it to be played in embedded players.',
+ }[code] || 'An unknown error occured';
+
+ player.media.error = { code, message };
+
+ triggerEvent.call(player, player.media, 'error');
}
-
- const detail = {
- code: event.data,
- };
-
- // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
- switch (event.data) {
- case 2:
- detail.message =
- 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
- break;
-
- case 5:
- detail.message =
- 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
- break;
-
- case 100:
- detail.message =
- 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
- break;
-
- case 101:
- case 150:
- detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
- break;
-
- default:
- detail.message = 'An unknown error occured';
- break;
- }
-
- player.media.error = detail;
-
- utils.dispatchEvent.call(player, player.media, 'error');
- },
- onPlaybackQualityChange() {
- utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
- quality: player.media.quality,
- });
},
onPlaybackRateChange(event) {
// Get the instance
@@ -261,9 +199,13 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
- utils.dispatchEvent.call(player, player.media, 'ratechange');
+ triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
+ // Bail if onReady has already been called. See issue #1108
+ if (is.function(player.media.play)) {
+ return;
+ }
// Get the instance
const instance = event.target;
@@ -295,14 +237,14 @@ const youtube = {
return Number(instance.getCurrentTime());
},
set(time) {
- // If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
- if (player.paused) {
+ // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
+ if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
- utils.dispatchEvent.call(player, player.media, 'seeking');
+ triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
@@ -319,24 +261,6 @@ const youtube = {
},
});
- // Quality
- Object.defineProperty(player.media, 'quality', {
- get() {
- return mapQualityUnit(instance.getPlaybackQuality());
- },
- set(input) {
- const quality = input;
-
- // Set via API
- instance.setPlaybackQuality(mapQualityUnit(quality));
-
- // Trigger request event
- utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
- quality,
- });
- },
- });
-
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
@@ -346,7 +270,7 @@ const youtube = {
set(input) {
volume = input;
instance.setVolume(volume * 100);
- utils.dispatchEvent.call(player, player.media, 'volumechange');
+ triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -357,10 +281,10 @@ const youtube = {
return muted;
},
set(input) {
- const toggle = utils.is.boolean(input) ? input : muted;
+ const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
- utils.dispatchEvent.call(player, player.media, 'volumechange');
+ triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -386,8 +310,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1);
}
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- utils.dispatchEvent.call(player, player.media, 'durationchange');
+ triggerEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
@@ -399,7 +323,7 @@ const youtube = {
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
- utils.dispatchEvent.call(player, player.media, 'progress');
+ triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
@@ -410,7 +334,7 @@ const youtube = {
clearInterval(player.timers.buffering);
// Trigger event
- utils.dispatchEvent.call(player, player.media, 'canplaythrough');
+ triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
@@ -424,15 +348,12 @@ const youtube = {
// Reset timer
clearInterval(player.timers.playing);
- const seeked = player.media.seeking && [
- 1,
- 2,
- ].includes(event.data);
+ const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
- utils.dispatchEvent.call(player, player.media, 'seeked');
+ triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
@@ -445,11 +366,11 @@ const youtube = {
switch (event.data) {
case -1:
// Update scrubber
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
- utils.dispatchEvent.call(player, player.media, 'progress');
+ triggerEvent.call(player, player.media, 'progress');
break;
@@ -462,23 +383,23 @@ const youtube = {
instance.stopVideo();
instance.playVideo();
} else {
- utils.dispatchEvent.call(player, player.media, 'ended');
+ triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
- if (player.media.paused) {
+ if (player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
- utils.dispatchEvent.call(player, player.media, 'playing');
+ triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
@@ -486,11 +407,8 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
- utils.dispatchEvent.call(player, player.media, 'durationchange');
+ triggerEvent.call(player, player.media, 'durationchange');
}
-
- // Get quality
- controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
}
break;
@@ -508,7 +426,7 @@ const youtube = {
break;
}
- utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
+ triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},