diff options
Diffstat (limited to 'src/js/plugins')
-rw-r--r-- | src/js/plugins/ads.js | 46 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 197 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 334 |
3 files changed, 305 insertions, 272 deletions
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 0246e221..856772d5 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -7,7 +7,12 @@ /* global google */ import i18n from '../i18n'; -import utils from '../utils'; +import { createElement } from './../utils/elements'; +import { triggerEvent } from './../utils/events'; +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(); }) @@ -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); }; @@ -212,14 +222,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, }); @@ -266,7 +276,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 +403,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 +540,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 +556,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 +587,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 0ceb89e5..09339229 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -5,20 +5,57 @@ import captions from './../captions'; import controls from './../controls'; import ui from './../ui'; -import utils from './../utils'; +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; + 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); }) @@ -33,8 +70,8 @@ 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(':'); + const padding = 100 / x * y; this.elements.wrapper.style.paddingBottom = `${padding}%`; if (this.supported.ui) { @@ -62,34 +99,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; } @@ -99,11 +139,8 @@ const vimeo = { // Get original image url.pathname = `${url.pathname.split('_')[0]}.jpg`; - // Set attribute - player.media.setAttribute('poster', url.href); - - // Update - ui.setPoster.call(player); + // Set and show poster + ui.setPoster.call(player, url.href).catch(() => {}); }); // Setup instance @@ -123,15 +160,13 @@ const vimeo = { // Create a faux HTML5 API using the Vimeo API player.media.play = () => { - player.embed.play().then(() => { - player.media.paused = false; - }); + assurePlaybackState.call(player, true); + return player.embed.play(); }; player.media.pause = () => { - player.embed.pause().then(() => { - player.media.paused = true; - }); + assurePlaybackState.call(player, false); + return player.embed.pause(); }; player.media.stop = () => { @@ -146,25 +181,27 @@ const vimeo = { return currentTime; }, set(time) { - // Get current paused state - // Vimeo will automatically play on seek - const { paused } = player.media; - - // Set seeking flag - player.media.seeking = true; - - // Trigger seeking - utils.dispatchEvent.call(player, player.media, 'seeking'); - - // Seek after events - player.embed.setCurrentTime(time).catch(() => { - // Do nothing - }); - - // Restore pause state - if (paused) { - player.pause(); - } + // Vimeo will automatically play on seek if the video hasn't been played before + + // 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; + triggerEvent.call(player, media, 'seeking'); + + // If paused, mute until seek is complete + Promise.resolve(restorePause && embed.setVolume(0)) + // Seek + .then(() => embed.setCurrentTime(time)) + // Restore paused + .then(() => restorePause && embed.pause()) + // Restore volume + .then(() => restorePause && embed.setVolume(volume)) + .catch(() => { + // Do nothing + }); }, }); @@ -179,7 +216,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) @@ -199,7 +236,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'); }); }, }); @@ -211,11 +248,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'); }); }, }); @@ -227,7 +264,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; @@ -260,11 +297,8 @@ 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]); + Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { + const ratio = getAspectRatio(dimensions[0], dimensions[1]); vimeo.setAspectRatio.call(this, ratio); }); @@ -282,13 +316,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 @@ -297,18 +331,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 @@ -318,32 +355,27 @@ 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, 'playing'); + assurePlaybackState.call(player, true); + triggerEvent.call(player, player.media, 'playing'); }); player.embed.on('pause', () => { - player.media.paused = true; - utils.dispatchEvent.call(player, player.media, 'pause'); + assurePlaybackState.call(player, false); }); 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 @@ -351,25 +383,24 @@ 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'); - utils.dispatchEvent.call(player, player.media, 'play'); + 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'); }); // Rebuild UI diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 4fde9319..94ab6dfa 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -4,80 +4,81 @@ import controls from './../controls'; import ui from './../ui'; -import utils from './../utils'; +import { dedupe } from './../utils/arrays'; +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; + } + + const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; +} // 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'; + const qualities = { + hd2160: 2160, + hd1440: 1440, + hd1080: 1080, + hd720: 720, + large: 480, + medium: 360, + small: 240, + tiny: 144, + }; + + const entry = Object.entries(qualities).find(entry => entry.includes(input)); + + if (entry) { + // Get the match corresponding to the input + return entry.find(value => value !== input); } + + return 'default'; } function mapQualityUnits(levels) { - if (utils.is.empty(levels)) { + if (is.empty(levels)) { return levels; } - return utils.dedupe(levels.map(level => mapQualityUnit(level))); + return dedupe(levels.map(level => mapQualityUnit(level))); +} + +// 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; + 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); }); @@ -104,10 +105,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; @@ -116,13 +117,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); } @@ -143,7 +143,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; } @@ -151,18 +151,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); - - // Set poster image - player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId)); + 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); + + // 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) + 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 @@ -188,49 +206,26 @@ 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, { + triggerEvent.call(player, player.media, 'qualitychange', false, { quality: player.media.quality, }); }, @@ -241,7 +236,7 @@ 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) { // Get the instance @@ -252,10 +247,12 @@ const youtube = { // Create a faux HTML5 API using the YouTube API player.media.play = () => { + assurePlaybackState.call(player, true); instance.playVideo(); }; player.media.pause = () => { + assurePlaybackState.call(player, false); instance.pauseVideo(); }; @@ -273,22 +270,17 @@ const youtube = { return Number(instance.getCurrentTime()); }, set(time) { - // Vimeo will automatically play on seek - const { paused } = player.media; + // 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 flag + // Set seeking state and trigger event player.media.seeking = true; - - // Trigger seeking - utils.dispatchEvent.call(player, player.media, 'seeking'); + triggerEvent.call(player, player.media, 'seeking'); // Seek after events sent instance.seekTo(time); - - // Restore pause state - if (paused) { - player.pause(); - } }, }); @@ -308,15 +300,7 @@ const youtube = { 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, - }); + instance.setPlaybackQuality(mapQualityUnit(input)); }, }); @@ -329,7 +313,7 @@ const youtube = { set(input) { volume = input; instance.setVolume(volume * 100); - utils.dispatchEvent.call(player, player.media, 'volumechange'); + triggerEvent.call(player, player.media, 'volumechange'); }, }); @@ -340,10 +324,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'); }, }); @@ -369,8 +353,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); @@ -382,7 +366,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 @@ -393,7 +377,7 @@ const youtube = { clearInterval(player.timers.buffering); // Trigger event - utils.dispatchEvent.call(player, player.media, 'canplaythrough'); + triggerEvent.call(player, player.media, 'canplaythrough'); } }, 200); @@ -407,6 +391,14 @@ const youtube = { // Reset timer clearInterval(player.timers.playing); + const seeked = player.media.seeking && [1, 2].includes(event.data); + + if (seeked) { + // Unset seeking and fire seeked event + player.media.seeking = false; + triggerEvent.call(player, player.media, 'seeked'); + } + // Handle events // -1 Unstarted // 0 Ended @@ -417,16 +409,16 @@ 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; case 0: - player.media.paused = true; + assurePlaybackState.call(player, false); // YouTube doesn't support loop for a single video, so mimick it. if (player.media.loop) { @@ -434,48 +426,48 @@ const youtube = { instance.stopVideo(); instance.playVideo(); } else { - utils.dispatchEvent.call(player, player.media, 'ended'); + triggerEvent.call(player, player.media, 'ended'); } break; case 1: - // 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, 'playing'); - - // Poll to get playback progress - player.timers.playing = setInterval(() => { - utils.dispatchEvent.call(player, player.media, 'timeupdate'); - }, 50); - - // Check duration again due to YouTube bug - // https://github.com/sampotts/plyr/issues/374 - // 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'); + // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) + if (player.media.paused && !player.embed.hasPlayed) { + player.media.pause(); + } else { + assurePlaybackState.call(player, true); + + triggerEvent.call(player, player.media, 'playing'); + + // Poll to get playback progress + player.timers.playing = setInterval(() => { + triggerEvent.call(player, player.media, 'timeupdate'); + }, 50); + + // Check duration again due to YouTube bug + // https://github.com/sampotts/plyr/issues/374 + // https://code.google.com/p/gdata-issues/issues/detail?id=8690 + if (player.media.duration !== instance.getDuration()) { + player.media.duration = instance.getDuration(); + triggerEvent.call(player, player.media, 'durationchange'); + } + + // Get quality + controls.setQualityMenu.call( + player, + mapQualityUnits(instance.getAvailableQualityLevels()), + ); } - // Get quality - controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); - break; case 2: - player.media.paused = true; - - utils.dispatchEvent.call(player, player.media, 'pause'); + // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) + if (!player.muted) { + player.embed.unMute(); + } + assurePlaybackState.call(player, false); break; @@ -483,7 +475,7 @@ const youtube = { break; } - utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { + triggerEvent.call(player, player.elements.container, 'statechange', false, { code: event.data, }); }, |