From 0694e586502f7ce85ce7858b67d515403b48c87e Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 25 Apr 2019 12:14:48 +1000 Subject: v3.5.4 --- src/js/config/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 6ba6d323..808fa0ae 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -61,7 +61,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.5.2/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.5.4/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index e81e073e..7124c6ba 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.5.3 +// plyr.js v3.5.4 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index ce41151b..95e14797 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.5.3 +// plyr.js v3.5.4 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From 80aa6ffe435b170466838f977d52438a95e22186 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Tue, 30 Apr 2019 23:44:05 +1000 Subject: Linting changes --- src/js/captions.js | 2 -- src/js/controls.js | 2 +- src/js/html5.js | 1 - src/js/listeners.js | 7 ------- src/js/plugins/ads.js | 1 - src/js/plugins/previewThumbnails.js | 4 ---- src/js/plugins/vimeo.js | 2 -- src/js/plugins/youtube.js | 3 --- src/js/plyr.js | 4 ---- src/js/plyr.polyfilled.js | 1 + src/js/utils/elements.js | 3 --- src/js/utils/events.js | 1 - src/js/utils/loadSprite.js | 2 -- src/js/utils/style.js | 1 - src/js/utils/time.js | 1 - 15 files changed, 2 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/js/captions.js b/src/js/captions.js index b326d85e..a8bb4d77 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -85,7 +85,6 @@ const captions = { const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); - let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); // Use first browser language when language is 'auto' @@ -166,7 +165,6 @@ const captions = { const { toggled } = this.captions; // Current state const activeClass = this.config.classNames.captions.active; - // Get the next state // If the method is called without parameter, toggle based on current value const active = is.nullOrUndefined(input) ? !toggled : input; diff --git a/src/js/controls.js b/src/js/controls.js index 9a960b38..d3e20a57 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -4,6 +4,7 @@ // ========================================================================== import RangeTouch from 'rangetouch'; + import captions from './captions'; import html5 from './html5'; import support from './support'; @@ -92,7 +93,6 @@ const controls = { const namespace = 'http://www.w3.org/2000/svg'; const iconUrl = controls.getIconUrl.call(this); const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; - // Create const icon = document.createElementNS(namespace, 'svg'); setAttributes( diff --git a/src/js/html5.js b/src/js/html5.js index 34f0c391..0d9a64ea 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -60,7 +60,6 @@ const html5 = { set(input) { // Get sources const sources = html5.getSources.call(player); - // Get first match for requested size const source = sources.find(source => Number(source.getAttribute('size')) === input); diff --git a/src/js/listeners.js b/src/js/listeners.js index d4d7bb32..ca527f1e 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -285,7 +285,6 @@ class Listeners { // Show, then hide after a timeout unless another control event occurs const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); - let delay = 0; if (show) { @@ -351,7 +350,6 @@ class Listeners { } const isEnter = event.type === 'enterfullscreen'; - // Set the player size when entering fullscreen to viewport size const { padding, ratio } = setPlayerSize(isEnter); @@ -542,7 +540,6 @@ class Listeners { controls() { const { player } = this; const { elements } = player; - // IE doesn't support input event, so we fallback to change const inputEvent = browser.isIE ? 'change' : 'input'; @@ -678,7 +675,6 @@ class Listeners { // Was playing before? const play = seek.hasAttribute(attribute); - // Done seeking const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); @@ -706,7 +702,6 @@ class Listeners { inputEvent, event => { const seek = event.currentTarget; - // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) let seekTo = seek.getAttribute('seek-value'); @@ -837,10 +832,8 @@ class Listeners { // Detect "natural" scroll - suppored on OS X Safari only // Other browsers on OS X will be inverted until support improves const inverted = event.webkitDirectionInvertedFromDevice; - // Get delta from event. Invert if `inverted` is true const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value)); - // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 2b083285..2acfaed9 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -303,7 +303,6 @@ class Ads { */ onAdEvent(event) { const { container } = this.player.elements; - // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) // don't have ad object associated const ad = event.getAd(); diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/previewThumbnails.js index 3e4b17a3..f03abe69 100644 --- a/src/js/plugins/previewThumbnails.js +++ b/src/js/plugins/previewThumbnails.js @@ -121,7 +121,6 @@ class PreviewThumbnails { // If string, convert into single-element list const urls = is.string(src) ? [src] : src; - // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails const promises = urls.map(u => this.getThumbnail(u)); @@ -467,7 +466,6 @@ class PreviewThumbnails { const { urlPrefix } = this.thumbnails[0]; const thumbURL = urlPrefix + newThumbFilename; - const previewImage = new Image(); previewImage.src = thumbURL; previewImage.onload = () => { @@ -601,11 +599,9 @@ class PreviewThumbnails { const seekbarRect = this.player.elements.progress.getBoundingClientRect(); const plyrRect = this.player.elements.container.getBoundingClientRect(); const { container } = this.elements.thumb; - // Find the lowest and highest desired left-position, so we don't slide out the side of the video container const minVal = plyrRect.left - seekbarRect.left + 10; const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10; - // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2; diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 8d920eea..bef48708 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -91,7 +91,6 @@ const vimeo = { } const id = parseId(source); - // Build an iframe const iframe = createElement('iframe'); const src = format(player.config.urls.vimeo.iframe, id, params); @@ -102,7 +101,6 @@ const vimeo = { // Get poster, if already set const { poster } = player; - // Inject the package const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); wrapper.appendChild(iframe); diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 7abc05fe..a5b1dafd 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -107,7 +107,6 @@ const youtube = { // API ready ready() { const player = this; - // Ignore already setup (race condition) const currentId = player.media.getAttribute('id'); if (!is.empty(currentId) && currentId.startsWith('youtube-')) { @@ -125,10 +124,8 @@ const youtube = { // Replace the
due to YouTube API issues 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); diff --git a/src/js/plyr.js b/src/js/plyr.js index 7124c6ba..5ec5bf2e 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -151,7 +151,6 @@ class Plyr { // Set media type based on tag or data attribute // Supported: video, audio, vimeo, youtube const type = this.media.tagName.toLowerCase(); - // Embed properties let iframe = null; let url = null; @@ -514,7 +513,6 @@ class Plyr { get duration() { // Faux duration set via config const fauxDuration = parseFloat(this.config.duration); - // Media duration can be NaN or Infinity before the media has loaded const realDuration = (this.media || {}).duration; const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; @@ -1045,10 +1043,8 @@ class Plyr { if (this.supported.ui && !this.isAudio) { // Get state before change const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); - // Negate the argument if not undefined since adding the class to hides the controls const force = typeof toggle === 'undefined' ? undefined : !toggle; - // Apply and get updated state const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 95e14797..c87bbd8c 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -7,6 +7,7 @@ import 'custom-event-polyfill'; import 'url-polyfill'; + import Plyr from './plyr'; export default Plyr; diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 9c1ddebc..94744771 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -17,7 +17,6 @@ export function wrap(elements, wrapper) { .reverse() .forEach((element, index) => { const child = index > 0 ? wrapper.cloneNode(true) : wrapper; - // Cache the current parent and sibling. const parent = element.parentNode; const sibling = element.nextSibling; @@ -145,12 +144,10 @@ export function getAttributesFromSelector(sel, existingAttributes) { const selector = s.trim(); const className = selector.replace('.', ''); const stripped = selector.replace(/[[\]]/g, ''); - // Get the parts and value const parts = stripped.split('='); const [key] = parts; const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; - // Get the first character const start = selector.charAt(0); diff --git a/src/js/utils/events.js b/src/js/utils/events.js index d304c312..87c35d26 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -35,7 +35,6 @@ export function toggleListener(element, event, callback, toggle = false, passive // Allow multiple events const events = event.split(' '); - // Build options // Default to just the capture boolean for browsers with no passive listener support let options = capture; diff --git a/src/js/utils/loadSprite.js b/src/js/utils/loadSprite.js index 917bd6ac..092f9986 100644 --- a/src/js/utils/loadSprite.js +++ b/src/js/utils/loadSprite.js @@ -15,7 +15,6 @@ export default function loadSprite(url, id) { const prefix = 'cache'; const hasId = is.string(id); let isCached = false; - const exists = () => document.getElementById(id) !== null; const update = (container, data) => { @@ -33,7 +32,6 @@ export default function loadSprite(url, id) { // Only load once if ID set if (!hasId || !exists()) { const useStorage = Storage.supported; - // Create container const container = document.createElement('div'); container.setAttribute('hidden', ''); diff --git a/src/js/utils/style.js b/src/js/utils/style.js index e51892e5..6f3069c9 100644 --- a/src/js/utils/style.js +++ b/src/js/utils/style.js @@ -64,7 +64,6 @@ export function setAspectRatio(input) { } const ratio = getAspectRatio.call(this, input); - const [w, h] = is.array(ratio) ? ratio : [0, 0]; const padding = (100 / w) * h; diff --git a/src/js/utils/time.js b/src/js/utils/time.js index 2deccf65..ffca88b2 100644 --- a/src/js/utils/time.js +++ b/src/js/utils/time.js @@ -18,7 +18,6 @@ export function formatTime(time = 0, displayHours = false, inverted = false) { // Format time component to add leading zero const format = value => `0${value}`.slice(-2); - // Breakdown to hours, mins, secs let hours = getHours(time); const mins = getMinutes(time); -- cgit v1.2.3 From 15cbae8a19dec5745f57377e7c4197ccbaead7ee Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 2 Jun 2019 22:25:44 +1000 Subject: Removed commented out code for Edge --- src/js/listeners.js | 8 -------- 1 file changed, 8 deletions(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index f5c9cda8..fe8d7d3c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -301,14 +301,6 @@ class Listeners { }, ); - // Force edge to repaint on exit fullscreen - // TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen - /* if (browser.isEdge) { - on.call(player, elements.container, 'exitfullscreen', () => { - setTimeout(() => repaint(elements.container), 100); - }); - } */ - // Set a gutter for Vimeo const setGutter = (ratio, padding, toggle) => { if (!player.isVimeo) { -- cgit v1.2.3 From ac6e3dba5a7ed25a01097a9bd165160652febba9 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 3 Jun 2019 00:28:09 +1000 Subject: Fix for thumbnails in demo for audio --- src/js/plugins/previewThumbnails.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/previewThumbnails.js index b4714117..67367b95 100644 --- a/src/js/plugins/previewThumbnails.js +++ b/src/js/plugins/previewThumbnails.js @@ -101,6 +101,10 @@ class PreviewThumbnails { } this.getThumbnails().then(() => { + if (!this.enabled) { + return; + } + // Render DOM elements this.render(); -- cgit v1.2.3 From c94ab2a39fde5ce41a2473f7e9ac29f51886fe18 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 3 Jun 2019 20:12:21 +1000 Subject: Repaint clean up --- src/js/fullscreen.js | 3 --- src/js/utils/animation.js | 16 ++++++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index c86bf877..d4d15b23 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -73,9 +73,6 @@ function toggleFallback(toggle = false) { .filter(part => part.trim() !== property) .join(','); } - - // Force a repaint as sometimes Safari doesn't want to fill the screen - setTimeout(() => repaint(this.target), 100); } // Toggle button and fire events diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index 6b950b61..3f721b5a 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -2,7 +2,6 @@ // Animation utils // ========================================================================== -import { toggleHidden } from './elements'; import is from './is'; export const transitionEndEvent = (() => { @@ -21,14 +20,19 @@ export const transitionEndEvent = (() => { })(); // Force repaint of element -export function repaint(element) { +export function repaint(element, delay) { setTimeout(() => { try { - toggleHidden(element, true); - element.offsetHeight; // eslint-disable-line - toggleHidden(element, false); + // eslint-disable-next-line no-param-reassign + element.hidden = true; + + // eslint-disable-next-line no-unused-expressions + element.offsetHeight; + + // eslint-disable-next-line no-param-reassign + element.hidden = false; } catch (e) { // Do nothing } - }, 0); + }, delay); } -- cgit v1.2.3 From 0f14865d567c2ccc03b30d26231dbe84db636f68 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 3 Jun 2019 20:12:43 +1000 Subject: Add duration (commented out) in defaults --- src/js/config/defaults.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 6ba6d323..96e5f09c 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -128,6 +128,7 @@ const defaults = { // 'fast-forward', 'progress', 'current-time', + // 'duration', 'mute', 'volume', 'captions', -- cgit v1.2.3 From 97d9228bed639f0c20b1f21468dd3f181af6b262 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 3 Jun 2019 20:13:16 +1000 Subject: Aspect ratio tweaks --- src/js/html5.js | 6 ++++-- src/js/listeners.js | 9 +++++++-- src/js/utils/elements.js | 7 ++----- src/js/utils/style.js | 11 ++--------- src/sass/lib/mixins.scss | 3 ++- 5 files changed, 17 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/js/html5.js b/src/js/html5.js index e538e922..b03e9c26 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -44,8 +44,10 @@ const html5 = { const player = this; - // Set aspect ratio if set - setAspectRatio.call(player); + // Set aspect ratio if fixed + if (!is.empty(this.config.ratio)) { + setAspectRatio.call(player); + } // Quality Object.defineProperty(player.media, 'quality', { diff --git a/src/js/listeners.js b/src/js/listeners.js index fe8d7d3c..c5076ff3 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -336,8 +336,13 @@ class Listeners { on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => { const { target, usingNative } = player.fullscreen; - // Ignore for iOS native - if (!player.isEmbed || target !== elements.container) { + // Ignore events not from target + if (target !== elements.container) { + return; + } + + // If it's not an embed and no ratio specified + if (!player.isEmbed && is.empty(player.config.ratio)) { return; } diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 98b44f13..4f10938e 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -192,11 +192,8 @@ export function toggleHidden(element, hidden) { hide = !element.hidden; } - if (hide) { - element.setAttribute('hidden', ''); - } else { - element.removeAttribute('hidden'); - } + // eslint-disable-next-line no-param-reassign + element.hidden = hide; } // Mirror Element.classList.toggle, with IE compatibility for "force" argument diff --git a/src/js/utils/style.js b/src/js/utils/style.js index 6f3069c9..941db8f2 100644 --- a/src/js/utils/style.js +++ b/src/js/utils/style.js @@ -27,15 +27,8 @@ export function reduceAspectRatio(ratio) { } export function getAspectRatio(input) { - const parse = ratio => { - if (!validateRatio(ratio)) { - return null; - } - - return ratio.split(':').map(Number); - }; - - // Provided ratio + const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null); + // Try provided ratio let ratio = parse(input); // Get from config diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index 554c66a5..5a1ca753 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -62,12 +62,13 @@ .plyr__video-wrapper { height: 100%; - width: 100%; + position: static; } // Vimeo requires some different styling &.plyr--vimeo .plyr__video-wrapper { height: 0; + position: relative; top: 50%; transform: translateY(-50%); } -- cgit v1.2.3 From 2e40b91ec1aabf33e945cba66fbcdd9b7aa53ba7 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 20 Jun 2019 23:50:46 +1000 Subject: Styling tweaks for demo --- src/js/config/defaults.js | 2 +- src/sass/components/control.scss | 5 ++--- src/sass/plugins/ads.scss | 2 +- src/sass/plugins/previewThumbnails.scss | 2 +- src/sass/settings/badges.scss | 2 +- src/sass/settings/colors.scss | 18 +++++++++++++----- src/sass/settings/controls.scss | 2 +- src/sass/settings/menus.scss | 4 ++-- src/sass/settings/progress.scss | 4 ++-- src/sass/settings/sliders.scss | 2 +- src/sass/settings/tooltips.scss | 2 +- 11 files changed, 26 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 96e5f09c..087500b7 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -196,7 +196,7 @@ const defaults = { }, youtube: { sdk: 'https://www.youtube.com/iframe_api', - api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', // 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title),fileDetails)&part=snippet', + api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', }, googleIMA: { sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index 1c9aab2b..f849d135 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -63,9 +63,9 @@ a.plyr__control { // Video control .plyr--video .plyr__control { - svg { + /* svg { filter: drop-shadow(0 1px 1px rgba(#000, 0.15)); - } + } */ // Hover and tab focus &.plyr__tab-focus, @@ -81,7 +81,6 @@ a.plyr__control { background: rgba($plyr-video-control-bg-hover, 0.8); border: 0; border-radius: 100%; - box-shadow: 0 1px 1px rgba(#000, 0.15); color: $plyr-video-control-color; display: none; left: 50%; diff --git a/src/sass/plugins/ads.scss b/src/sass/plugins/ads.scss index c5acef75..44ec5351 100644 --- a/src/sass/plugins/ads.scss +++ b/src/sass/plugins/ads.scss @@ -23,7 +23,7 @@ // The countdown label &::after { - background: rgba($plyr-color-gunmetal, 0.8); + background: rgba($plyr-color-gray-9, 0.8); border-radius: 2px; bottom: $plyr-control-spacing; color: #fff; diff --git a/src/sass/plugins/previewThumbnails.scss b/src/sass/plugins/previewThumbnails.scss index 02a2f619..b2b272c1 100644 --- a/src/sass/plugins/previewThumbnails.scss +++ b/src/sass/plugins/previewThumbnails.scss @@ -7,7 +7,7 @@ $plyr-preview-bg: $plyr-tooltip-bg !default; $plyr-preview-radius: $plyr-tooltip-radius !default; $plyr-preview-shadow: $plyr-tooltip-shadow !default; $plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default; -$plyr-preview-image-bg: $plyr-color-heather !default; +$plyr-preview-image-bg: $plyr-color-gray-2 !default; $plyr-preview-time-font-size: $plyr-font-size-time !default; $plyr-preview-time-padding: 3px 6px !default; $plyr-preview-time-bg: rgba(0, 0, 0, 0.55); diff --git a/src/sass/settings/badges.scss b/src/sass/settings/badges.scss index 4f98c9a8..5fd0c138 100644 --- a/src/sass/settings/badges.scss +++ b/src/sass/settings/badges.scss @@ -2,5 +2,5 @@ // Badges // ========================================================================== -$plyr-badge-bg: $plyr-color-fiord !default; +$plyr-badge-bg: $plyr-color-gray-7 !default; $plyr-badge-color: #fff !default; diff --git a/src/sass/settings/colors.scss b/src/sass/settings/colors.scss index c9ea580c..e3883eef 100644 --- a/src/sass/settings/colors.scss +++ b/src/sass/settings/colors.scss @@ -2,8 +2,16 @@ // Colors // ========================================================================== -$plyr-color-main: #1aafff !default; -$plyr-color-gunmetal: #2f343d !default; -$plyr-color-fiord: #4f5b5f !default; -$plyr-color-lynch: #6b7d85 !default; -$plyr-color-heather: #b7c5cd !default; +$plyr-color-main: hsl(198, 100%, 50%) !default; + +// Grayscale +$plyr-color-gray-9: hsl(210, 15%, 16%); +$plyr-color-gray-8: lighten($plyr-color-gray-9, 9%); +$plyr-color-gray-7: lighten($plyr-color-gray-8, 9%); +$plyr-color-gray-6: lighten($plyr-color-gray-7, 9%); +$plyr-color-gray-5: lighten($plyr-color-gray-6, 9%); +$plyr-color-gray-4: lighten($plyr-color-gray-5, 9%); +$plyr-color-gray-3: lighten($plyr-color-gray-4, 9%); +$plyr-color-gray-2: lighten($plyr-color-gray-3, 9%); +$plyr-color-gray-1: lighten($plyr-color-gray-2, 9%); +$plyr-color-gray-0: lighten($plyr-color-gray-1, 9%); diff --git a/src/sass/settings/controls.scss b/src/sass/settings/controls.scss index d6d2c153..da9f4e58 100644 --- a/src/sass/settings/controls.scss +++ b/src/sass/settings/controls.scss @@ -13,6 +13,6 @@ $plyr-video-control-color-hover: #fff !default; $plyr-video-control-bg-hover: $plyr-color-main !default; $plyr-audio-controls-bg: #fff !default; -$plyr-audio-control-color: $plyr-color-fiord !default; +$plyr-audio-control-color: $plyr-color-gray-7 !default; $plyr-audio-control-color-hover: #fff !default; $plyr-audio-control-bg-hover: $plyr-color-main !default; diff --git a/src/sass/settings/menus.scss b/src/sass/settings/menus.scss index 64df9863..420ebb03 100644 --- a/src/sass/settings/menus.scss +++ b/src/sass/settings/menus.scss @@ -3,8 +3,8 @@ // ========================================================================== $plyr-menu-bg: rgba(#fff, 0.9) !default; -$plyr-menu-color: $plyr-color-fiord !default; +$plyr-menu-color: $plyr-color-gray-7 !default; $plyr-menu-arrow-size: 6px !default; -$plyr-menu-border-color: $plyr-color-heather !default; +$plyr-menu-border-color: $plyr-color-gray-2 !default; $plyr-menu-border-shadow-color: #fff !default; $plyr-menu-shadow: 0 1px 2px rgba(#000, 0.15) !default; diff --git a/src/sass/settings/progress.scss b/src/sass/settings/progress.scss index 074ee3c6..10b6ebb7 100644 --- a/src/sass/settings/progress.scss +++ b/src/sass/settings/progress.scss @@ -4,8 +4,8 @@ // Loading $plyr-progress-loading-size: 25px !default; -$plyr-progress-loading-bg: rgba($plyr-color-gunmetal, 0.6) !default; +$plyr-progress-loading-bg: rgba($plyr-color-gray-9, 0.6) !default; // Buffered $plyr-video-progress-buffered-bg: rgba(#fff, 0.25) !default; -$plyr-audio-progress-buffered-bg: rgba($plyr-color-heather, 0.66) !default; +$plyr-audio-progress-buffered-bg: rgba($plyr-color-gray-2, 0.66) !default; diff --git a/src/sass/settings/sliders.scss b/src/sass/settings/sliders.scss index 6ac053b0..c4d239ae 100644 --- a/src/sass/settings/sliders.scss +++ b/src/sass/settings/sliders.scss @@ -9,7 +9,7 @@ $plyr-range-thumb-active-shadow-width: 3px !default; $plyr-range-thumb-height: 13px !default; $plyr-range-thumb-bg: #fff !default; $plyr-range-thumb-border: 2px solid transparent !default; -$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gunmetal, 0.2) !default; +$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gray-9, 0.2) !default; // Track $plyr-range-track-height: 5px !default; diff --git a/src/sass/settings/tooltips.scss b/src/sass/settings/tooltips.scss index fd304d60..2d298ef8 100644 --- a/src/sass/settings/tooltips.scss +++ b/src/sass/settings/tooltips.scss @@ -3,7 +3,7 @@ // ========================================================================== $plyr-tooltip-bg: rgba(#fff, 0.9) !default; -$plyr-tooltip-color: $plyr-color-fiord !default; +$plyr-tooltip-color: $plyr-color-gray-7 !default; $plyr-tooltip-padding: ($plyr-control-spacing / 2) !default; $plyr-tooltip-arrow-size: 4px !default; $plyr-tooltip-radius: 3px !default; -- cgit v1.2.3 From c4b3e0672e86f2a2786f315bf8f54250cd1f7f78 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 21 Jun 2019 00:10:57 +1000 Subject: Clean up --- src/js/captions.js | 3 ++- src/sass/components/control.scss | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/js/captions.js b/src/js/captions.js index f7c24534..e33fd81a 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -133,7 +133,8 @@ const captions = { }); // Turn off native caption rendering to avoid double captions - Object.assign(track, { mode: 'hidden' }); + // eslint-disable-next-line no-param-reassign + track.mode = 'hidden'; // Add event listener for cue changes on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index f849d135..0022d17b 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -63,10 +63,6 @@ a.plyr__control { // Video control .plyr--video .plyr__control { - /* svg { - filter: drop-shadow(0 1px 1px rgba(#000, 0.15)); - } */ - // Hover and tab focus &.plyr__tab-focus, &:hover, -- cgit v1.2.3 From 8fc6c2ba526bf1ef8cdb9476f1644089281ce60d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 21 Jun 2019 00:19:37 +1000 Subject: File rename and clean up --- src/js/controls.js | 2 +- src/js/fullscreen.js | 1 - src/js/plugins/ads.js | 2 +- src/js/plugins/preview-thumbnails.js | 652 ++++++++++++++++++++++++++++++ src/js/plugins/previewThumbnails.js | 653 ------------------------------- src/js/plugins/vimeo.js | 2 +- src/js/plugins/youtube.js | 4 +- src/js/plyr.js | 4 +- src/js/ui.js | 2 +- src/js/utils/load-image.js | 19 + src/js/utils/load-script.js | 14 + src/js/utils/load-sprite.js | 75 ++++ src/js/utils/loadImage.js | 19 - src/js/utils/loadScript.js | 14 - src/js/utils/loadSprite.js | 75 ---- src/sass/plugins/preview-thumbnails.scss | 118 ++++++ src/sass/plugins/previewThumbnails.scss | 118 ------ src/sass/plyr.scss | 2 +- 18 files changed, 887 insertions(+), 889 deletions(-) create mode 100644 src/js/plugins/preview-thumbnails.js delete mode 100644 src/js/plugins/previewThumbnails.js create mode 100644 src/js/utils/load-image.js create mode 100644 src/js/utils/load-script.js create mode 100644 src/js/utils/load-sprite.js delete mode 100644 src/js/utils/loadImage.js delete mode 100644 src/js/utils/loadScript.js delete mode 100644 src/js/utils/loadSprite.js create mode 100644 src/sass/plugins/preview-thumbnails.scss delete mode 100644 src/sass/plugins/previewThumbnails.scss (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 43a92140..7afcd2c0 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -28,7 +28,7 @@ import { import { off, on } from './utils/events'; import i18n from './utils/i18n'; import is from './utils/is'; -import loadSprite from './utils/loadSprite'; +import loadSprite from './utils/load-sprite'; import { extend } from './utils/objects'; import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings'; import { formatTime, getHours } from './utils/time'; diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index d4d15b23..4de8da88 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -4,7 +4,6 @@ // https://webkit.org/blog/7929/designing-websites-for-iphone-x/ // ========================================================================== -import { repaint } from './utils/animation'; import browser from './utils/browser'; import { hasClass, toggleClass, trapFocus } from './utils/elements'; import { on, triggerEvent } from './utils/events'; diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index e6fab967..db55e499 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -10,7 +10,7 @@ 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 loadScript from '../utils/load-script'; import { formatTime } from '../utils/time'; import { buildUrlParams } from '../utils/urls'; diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js new file mode 100644 index 00000000..61021d64 --- /dev/null +++ b/src/js/plugins/preview-thumbnails.js @@ -0,0 +1,652 @@ +import { createElement } from '../utils/elements'; +import { once } from '../utils/events'; +import fetch from '../utils/fetch'; +import is from '../utils/is'; +import { formatTime } from '../utils/time'; + +// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg" +const parseVtt = vttDataString => { + const processedList = []; + const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/); + + frames.forEach(frame => { + const result = {}; + const lines = frame.split(/\r\n|\n|\r/); + + lines.forEach(line => { + if (!is.number(result.startTime)) { + // The line with start and end times on it is the first line of interest + const matchTimes = line.match( + /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/, + ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT + + if (matchTimes) { + result.startTime = + Number(matchTimes[1] || 0) * 60 * 60 + + Number(matchTimes[2]) * 60 + + Number(matchTimes[3]) + + Number(`0.${matchTimes[4]}`); + result.endTime = + Number(matchTimes[6] || 0) * 60 * 60 + + Number(matchTimes[7]) * 60 + + Number(matchTimes[8]) + + Number(`0.${matchTimes[9]}`); + } + } else if (!is.empty(line.trim()) && is.empty(result.text)) { + // If we already have the startTime, then we're definitely up to the text line(s) + const lineSplit = line.trim().split('#xywh='); + [result.text] = lineSplit; + + // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image + if (lineSplit[1]) { + [result.x, result.y, result.w, result.h] = lineSplit[1].split(','); + } + } + }); + + if (result.text) { + processedList.push(result); + } + }); + + return processedList; +}; + +/** + * Preview thumbnails for seek hover and scrubbing + * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar + * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed + * + * Notes: + * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole + * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails + * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered + */ + +class PreviewThumbnails { + /** + * PreviewThumbnails constructor. + * @param {Plyr} player + * @return {PreviewThumbnails} + */ + constructor(player) { + this.player = player; + this.thumbnails = []; + this.loaded = false; + this.lastMouseMoveTime = Date.now(); + this.mouseDown = false; + this.loadedImages = []; + + this.elements = { + thumb: {}, + scrubbing: {}, + }; + + this.load(); + } + + get enabled() { + return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled; + } + + load() { + // Togglethe regular seek tooltip + if (this.player.elements.display.seekTooltip) { + this.player.elements.display.seekTooltip.hidden = this.enabled; + } + + if (!this.enabled) { + return; + } + + this.getThumbnails().then(() => { + if (!this.enabled) { + return; + } + + // Render DOM elements + this.render(); + + // Check to see if thumb container size was specified manually in CSS + this.determineContainerAutoSizing(); + + this.loaded = true; + }); + } + + // Download VTT files and parse them + getThumbnails() { + return new Promise(resolve => { + const { src } = this.player.config.previewThumbnails; + + if (is.empty(src)) { + throw new Error('Missing previewThumbnails.src config attribute'); + } + + // If string, convert into single-element list + const urls = is.string(src) ? [src] : src; + // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails + const promises = urls.map(u => this.getThumbnail(u)); + + Promise.all(promises).then(() => { + // Sort smallest to biggest (e.g., [120p, 480p, 1080p]) + this.thumbnails.sort((x, y) => x.height - y.height); + + this.player.debug.log('Preview thumbnails', this.thumbnails); + + resolve(); + }); + }); + } + + // Process individual VTT file + getThumbnail(url) { + return new Promise(resolve => { + fetch(url).then(response => { + const thumbnail = { + frames: parseVtt(response), + height: null, + urlPrefix: '', + }; + + // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file + // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank + // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file + if ( + !thumbnail.frames[0].text.startsWith('/') && + !thumbnail.frames[0].text.startsWith('http://') && + !thumbnail.frames[0].text.startsWith('https://') + ) { + thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1); + } + + // Download the first frame, so that we can determine/set the height of this thumbnailsDef + const tempImage = new Image(); + + tempImage.onload = () => { + thumbnail.height = tempImage.naturalHeight; + thumbnail.width = tempImage.naturalWidth; + + this.thumbnails.push(thumbnail); + + resolve(); + }; + + tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text; + }); + }); + } + + startMove(event) { + if (!this.loaded) { + return; + } + + if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) { + return; + } + + // Wait until media has a duration + if (!this.player.media.duration) { + return; + } + + if (event.type === 'touchmove') { + // Calculate seek hover position as approx video seconds + this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100); + } else { + // Calculate seek hover position as approx video seconds + const clientRect = this.player.elements.progress.getBoundingClientRect(); + const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left); + this.seekTime = this.player.media.duration * (percentage / 100); + + if (this.seekTime < 0) { + // The mousemove fires for 10+px out to the left + this.seekTime = 0; + } + + if (this.seekTime > this.player.media.duration - 1) { + // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video + this.seekTime = this.player.media.duration - 1; + } + + this.mousePosX = event.pageX; + + // Set time text inside image container + this.elements.thumb.time.innerText = formatTime(this.seekTime); + } + + // Download and show image + this.showImageAtCurrentTime(); + } + + endMove() { + this.toggleThumbContainer(false, true); + } + + startScrubbing(event) { + // Only act on left mouse button (0), or touch device (event.button is false) + if (event.button === false || event.button === 0) { + this.mouseDown = true; + + // Wait until media has a duration + if (this.player.media.duration) { + this.toggleScrubbingContainer(true); + this.toggleThumbContainer(false, true); + + // Download and show image + this.showImageAtCurrentTime(); + } + } + } + + endScrubbing() { + this.mouseDown = false; + + // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview + if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) { + // The video was already seeked/loaded at the chosen time - hide immediately + this.toggleScrubbingContainer(false); + } else { + // The video hasn't seeked yet. Wait for that + once.call(this.player, this.player.media, 'timeupdate', () => { + // Re-check mousedown - we might have already started scrubbing again + if (!this.mouseDown) { + this.toggleScrubbingContainer(false); + } + }); + } + } + + /** + * Setup hooks for Plyr and window events + */ + listeners() { + // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering + this.player.on('play', () => { + this.toggleThumbContainer(false, true); + }); + + this.player.on('seeked', () => { + this.toggleThumbContainer(false); + }); + + this.player.on('timeupdate', () => { + this.lastTime = this.player.media.currentTime; + }); + } + + /** + * Create HTML elements for image containers + */ + render() { + // Create HTML element: plyr__preview-thumbnail-container + this.elements.thumb.container = createElement('div', { + class: this.player.config.classNames.previewThumbnails.thumbContainer, + }); + + // Wrapper for the image for styling + this.elements.thumb.imageContainer = createElement('div', { + class: this.player.config.classNames.previewThumbnails.imageContainer, + }); + this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer); + + // Create HTML element, parent+span: time text (e.g., 01:32:00) + const timeContainer = createElement('div', { + class: this.player.config.classNames.previewThumbnails.timeContainer, + }); + + this.elements.thumb.time = createElement('span', {}, '00:00'); + timeContainer.appendChild(this.elements.thumb.time); + + this.elements.thumb.container.appendChild(timeContainer); + + // Inject the whole thumb + if (is.element(this.player.elements.progress)) { + this.player.elements.progress.appendChild(this.elements.thumb.container); + } + + // Create HTML element: plyr__preview-scrubbing-container + this.elements.scrubbing.container = createElement('div', { + class: this.player.config.classNames.previewThumbnails.scrubbingContainer, + }); + + this.player.elements.wrapper.appendChild(this.elements.scrubbing.container); + } + + showImageAtCurrentTime() { + if (this.mouseDown) { + this.setScrubbingContainerSize(); + } else { + this.setThumbContainerSizeAndPos(); + } + + // Find the desired thumbnail index + // TODO: Handle a video longer than the thumbs where thumbNum is null + const thumbNum = this.thumbnails[0].frames.findIndex( + frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime, + ); + const hasThumb = thumbNum >= 0; + let qualityIndex = 0; + + // Show the thumb container if we're not scrubbing + if (!this.mouseDown) { + this.toggleThumbContainer(hasThumb); + } + + // No matching thumb found + if (!hasThumb) { + return; + } + + // Check to see if we've already downloaded higher quality versions of this image + this.thumbnails.forEach((thumbnail, index) => { + if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) { + qualityIndex = index; + } + }); + + // Only proceed if either thumbnum or thumbfilename has changed + if (thumbNum !== this.showingThumb) { + this.showingThumb = thumbNum; + this.loadImage(qualityIndex); + } + } + + // Show the image that's currently specified in this.showingThumb + loadImage(qualityIndex = 0) { + const thumbNum = this.showingThumb; + const thumbnail = this.thumbnails[qualityIndex]; + const { urlPrefix } = thumbnail; + const frame = thumbnail.frames[thumbNum]; + const thumbFilename = thumbnail.frames[thumbNum].text; + const thumbUrl = urlPrefix + thumbFilename; + + if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) { + // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one + // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort + if (this.loadingImage && this.usingSprites) { + this.loadingImage.onload = null; + } + + // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image + // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background + // images causes a flicker. Putting a new image over the top does not + const previewImage = new Image(); + previewImage.src = thumbUrl; + previewImage.dataset.index = thumbNum; + previewImage.dataset.filename = thumbFilename; + this.showingThumbFilename = thumbFilename; + + this.player.debug.log(`Loading image: ${thumbUrl}`); + + // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function... + previewImage.onload = () => + this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true); + this.loadingImage = previewImage; + this.removeOldImages(previewImage); + } else { + // Update the existing image + this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false); + this.currentImageElement.dataset.index = thumbNum; + this.removeOldImages(this.currentImageElement); + } + } + + showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) { + this.player.debug.log( + `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`, + ); + this.setImageSizeAndOffset(previewImage, frame); + + if (newImage) { + this.currentImageContainer.appendChild(previewImage); + this.currentImageElement = previewImage; + + if (!this.loadedImages.includes(thumbFilename)) { + this.loadedImages.push(thumbFilename); + } + } + + // Preload images before and after the current one + // Show higher quality of the same frame + // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading + this.preloadNearby(thumbNum, true) + .then(this.preloadNearby(thumbNum, false)) + .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename)); + } + + // Remove all preview images that aren't the designated current image + removeOldImages(currentImage) { + // Get a list of all images, convert it from a DOM list to an array + Array.from(this.currentImageContainer.children).forEach(image => { + if (image.tagName.toLowerCase() !== 'img') { + return; + } + + const removeDelay = this.usingSprites ? 500 : 1000; + + if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) { + // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients + // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function + // eslint-disable-next-line no-param-reassign + image.dataset.deleting = true; + + // This has to be set before the timeout - to prevent issues switching between hover and scrub + const { currentImageContainer } = this; + + setTimeout(() => { + currentImageContainer.removeChild(image); + this.player.debug.log(`Removing thumb: ${image.dataset.filename}`); + }, removeDelay); + } + }); + } + + // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame + // This will only preload the lowest quality + preloadNearby(thumbNum, forward = true) { + return new Promise(resolve => { + setTimeout(() => { + const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text; + + if (this.showingThumbFilename === oldThumbFilename) { + // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away + let thumbnailsClone; + if (forward) { + thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum); + } else { + thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse(); + } + + let foundOne = false; + + thumbnailsClone.forEach(frame => { + const newThumbFilename = frame.text; + + if (newThumbFilename !== oldThumbFilename) { + // Found one with a different filename. Make sure it hasn't already been loaded on this page visit + if (!this.loadedImages.includes(newThumbFilename)) { + foundOne = true; + this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`); + + const { urlPrefix } = this.thumbnails[0]; + const thumbURL = urlPrefix + newThumbFilename; + const previewImage = new Image(); + previewImage.src = thumbURL; + previewImage.onload = () => { + this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`); + if (!this.loadedImages.includes(newThumbFilename)) + this.loadedImages.push(newThumbFilename); + + // We don't resolve until the thumb is loaded + resolve(); + }; + } + } + }); + + // If there are none to preload then we want to resolve immediately + if (!foundOne) { + resolve(); + } + } + }, 300); + }); + } + + // If user has been hovering current image for half a second, look for a higher quality one + getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) { + if (currentQualityIndex < this.thumbnails.length - 1) { + // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container + let previewImageHeight = previewImage.naturalHeight; + + if (this.usingSprites) { + previewImageHeight = frame.h; + } + + if (previewImageHeight < this.thumbContainerHeight) { + // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while + setTimeout(() => { + // Make sure the mouse hasn't already moved on and started hovering at another image + if (this.showingThumbFilename === thumbFilename) { + this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`); + this.loadImage(currentQualityIndex + 1); + } + }, 300); + } + } + } + + get currentImageContainer() { + if (this.mouseDown) { + return this.elements.scrubbing.container; + } + + return this.elements.thumb.imageContainer; + } + + get usingSprites() { + return Object.keys(this.thumbnails[0].frames[0]).includes('w'); + } + + get thumbAspectRatio() { + if (this.usingSprites) { + return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h; + } + + return this.thumbnails[0].width / this.thumbnails[0].height; + } + + get thumbContainerHeight() { + if (this.mouseDown) { + // Can't use media.clientHeight - HTML5 video goes big and does black bars above and below + return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio); + } + + return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4); + } + + get currentImageElement() { + if (this.mouseDown) { + return this.currentScrubbingImageElement; + } + + return this.currentThumbnailImageElement; + } + + set currentImageElement(element) { + if (this.mouseDown) { + this.currentScrubbingImageElement = element; + } else { + this.currentThumbnailImageElement = element; + } + } + + toggleThumbContainer(toggle = false, clearShowing = false) { + const className = this.player.config.classNames.previewThumbnails.thumbContainerShown; + this.elements.thumb.container.classList.toggle(className, toggle); + + if (!toggle && clearShowing) { + this.showingThumb = null; + this.showingThumbFilename = null; + } + } + + toggleScrubbingContainer(toggle = false) { + const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown; + this.elements.scrubbing.container.classList.toggle(className, toggle); + + if (!toggle) { + this.showingThumb = null; + this.showingThumbFilename = null; + } + } + + determineContainerAutoSizing() { + if (this.elements.thumb.imageContainer.clientHeight > 20) { + // This will prevent auto sizing in this.setThumbContainerSizeAndPos() + this.sizeSpecifiedInCSS = true; + } + } + + // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS + setThumbContainerSizeAndPos() { + if (!this.sizeSpecifiedInCSS) { + const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio); + this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`; + this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`; + } + + this.setThumbContainerPos(); + } + + setThumbContainerPos() { + const seekbarRect = this.player.elements.progress.getBoundingClientRect(); + const plyrRect = this.player.elements.container.getBoundingClientRect(); + const { container } = this.elements.thumb; + // Find the lowest and highest desired left-position, so we don't slide out the side of the video container + const minVal = plyrRect.left - seekbarRect.left + 10; + const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10; + // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth + let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2; + + if (previewPos < minVal) { + previewPos = minVal; + } + + if (previewPos > maxVal) { + previewPos = maxVal; + } + + container.style.left = `${previewPos}px`; + } + + // Can't use 100% width, in case the video is a different aspect ratio to the video container + setScrubbingContainerSize() { + this.elements.scrubbing.container.style.width = `${this.player.media.clientWidth}px`; + // Can't use media.clientHeight - html5 video goes big and does black bars above and below + this.elements.scrubbing.container.style.height = `${this.player.media.clientWidth / this.thumbAspectRatio}px`; + } + + // Sprites need to be offset to the correct location + setImageSizeAndOffset(previewImage, frame) { + if (!this.usingSprites) { + return; + } + + // Find difference between height and preview container height + const multiplier = this.thumbContainerHeight / frame.h; + + // eslint-disable-next-line no-param-reassign + previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`; + // eslint-disable-next-line no-param-reassign + previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`; + // eslint-disable-next-line no-param-reassign + previewImage.style.left = `-${frame.x * multiplier}px`; + // eslint-disable-next-line no-param-reassign + previewImage.style.top = `-${frame.y * multiplier}px`; + } +} + +export default PreviewThumbnails; diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/previewThumbnails.js deleted file mode 100644 index 67367b95..00000000 --- a/src/js/plugins/previewThumbnails.js +++ /dev/null @@ -1,653 +0,0 @@ -import { createElement } from '../utils/elements'; -import { once } from '../utils/events'; -import fetch from '../utils/fetch'; -import is from '../utils/is'; -import { extend } from '../utils/objects'; -import { formatTime } from '../utils/time'; - -// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg" -const parseVtt = vttDataString => { - const processedList = []; - const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/); - - frames.forEach(frame => { - const result = {}; - const lines = frame.split(/\r\n|\n|\r/); - - lines.forEach(line => { - if (!is.number(result.startTime)) { - // The line with start and end times on it is the first line of interest - const matchTimes = line.match( - /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/, - ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT - - if (matchTimes) { - result.startTime = - Number(matchTimes[1] || 0) * 60 * 60 + - Number(matchTimes[2]) * 60 + - Number(matchTimes[3]) + - Number(`0.${matchTimes[4]}`); - result.endTime = - Number(matchTimes[6] || 0) * 60 * 60 + - Number(matchTimes[7]) * 60 + - Number(matchTimes[8]) + - Number(`0.${matchTimes[9]}`); - } - } else if (!is.empty(line.trim()) && is.empty(result.text)) { - // If we already have the startTime, then we're definitely up to the text line(s) - const lineSplit = line.trim().split('#xywh='); - [result.text] = lineSplit; - - // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image - if (lineSplit[1]) { - [result.x, result.y, result.w, result.h] = lineSplit[1].split(','); - } - } - }); - - if (result.text) { - processedList.push(result); - } - }); - - return processedList; -}; - -/** - * Preview thumbnails for seek hover and scrubbing - * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar - * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed - * - * Notes: - * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole - * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails - * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered - */ - -class PreviewThumbnails { - /** - * PreviewThumbnails constructor. - * @param {Plyr} player - * @return {PreviewThumbnails} - */ - constructor(player) { - this.player = player; - this.thumbnails = []; - this.loaded = false; - this.lastMouseMoveTime = Date.now(); - this.mouseDown = false; - this.loadedImages = []; - - this.elements = { - thumb: {}, - scrubbing: {}, - }; - - this.load(); - } - - get enabled() { - return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled; - } - - load() { - // Togglethe regular seek tooltip - if (this.player.elements.display.seekTooltip) { - this.player.elements.display.seekTooltip.hidden = this.enabled; - } - - if (!this.enabled) { - return; - } - - this.getThumbnails().then(() => { - if (!this.enabled) { - return; - } - - // Render DOM elements - this.render(); - - // Check to see if thumb container size was specified manually in CSS - this.determineContainerAutoSizing(); - - this.loaded = true; - }); - } - - // Download VTT files and parse them - getThumbnails() { - return new Promise(resolve => { - const { src } = this.player.config.previewThumbnails; - - if (is.empty(src)) { - throw new Error('Missing previewThumbnails.src config attribute'); - } - - // If string, convert into single-element list - const urls = is.string(src) ? [src] : src; - // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails - const promises = urls.map(u => this.getThumbnail(u)); - - Promise.all(promises).then(() => { - // Sort smallest to biggest (e.g., [120p, 480p, 1080p]) - this.thumbnails.sort((x, y) => x.height - y.height); - - this.player.debug.log('Preview thumbnails', this.thumbnails); - - resolve(); - }); - }); - } - - // Process individual VTT file - getThumbnail(url) { - return new Promise(resolve => { - fetch(url).then(response => { - const thumbnail = { - frames: parseVtt(response), - height: null, - urlPrefix: '', - }; - - // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file - // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank - // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file - if ( - !thumbnail.frames[0].text.startsWith('/') && - !thumbnail.frames[0].text.startsWith('http://') && - !thumbnail.frames[0].text.startsWith('https://') - ) { - thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1); - } - - // Download the first frame, so that we can determine/set the height of this thumbnailsDef - const tempImage = new Image(); - - tempImage.onload = () => { - thumbnail.height = tempImage.naturalHeight; - thumbnail.width = tempImage.naturalWidth; - - this.thumbnails.push(thumbnail); - - resolve(); - }; - - tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text; - }); - }); - } - - startMove(event) { - if (!this.loaded) { - return; - } - - if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) { - return; - } - - // Wait until media has a duration - if (!this.player.media.duration) { - return; - } - - if (event.type === 'touchmove') { - // Calculate seek hover position as approx video seconds - this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100); - } else { - // Calculate seek hover position as approx video seconds - const clientRect = this.player.elements.progress.getBoundingClientRect(); - const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left); - this.seekTime = this.player.media.duration * (percentage / 100); - - if (this.seekTime < 0) { - // The mousemove fires for 10+px out to the left - this.seekTime = 0; - } - - if (this.seekTime > this.player.media.duration - 1) { - // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video - this.seekTime = this.player.media.duration - 1; - } - - this.mousePosX = event.pageX; - - // Set time text inside image container - this.elements.thumb.time.innerText = formatTime(this.seekTime); - } - - // Download and show image - this.showImageAtCurrentTime(); - } - - endMove() { - this.toggleThumbContainer(false, true); - } - - startScrubbing(event) { - // Only act on left mouse button (0), or touch device (event.button is false) - if (event.button === false || event.button === 0) { - this.mouseDown = true; - - // Wait until media has a duration - if (this.player.media.duration) { - this.toggleScrubbingContainer(true); - this.toggleThumbContainer(false, true); - - // Download and show image - this.showImageAtCurrentTime(); - } - } - } - - endScrubbing() { - this.mouseDown = false; - - // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview - if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) { - // The video was already seeked/loaded at the chosen time - hide immediately - this.toggleScrubbingContainer(false); - } else { - // The video hasn't seeked yet. Wait for that - once.call(this.player, this.player.media, 'timeupdate', () => { - // Re-check mousedown - we might have already started scrubbing again - if (!this.mouseDown) { - this.toggleScrubbingContainer(false); - } - }); - } - } - - /** - * Setup hooks for Plyr and window events - */ - listeners() { - // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering - this.player.on('play', () => { - this.toggleThumbContainer(false, true); - }); - - this.player.on('seeked', () => { - this.toggleThumbContainer(false); - }); - - this.player.on('timeupdate', () => { - this.lastTime = this.player.media.currentTime; - }); - } - - /** - * Create HTML elements for image containers - */ - render() { - // Create HTML element: plyr__preview-thumbnail-container - this.elements.thumb.container = createElement('div', { - class: this.player.config.classNames.previewThumbnails.thumbContainer, - }); - - // Wrapper for the image for styling - this.elements.thumb.imageContainer = createElement('div', { - class: this.player.config.classNames.previewThumbnails.imageContainer, - }); - this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer); - - // Create HTML element, parent+span: time text (e.g., 01:32:00) - const timeContainer = createElement('div', { - class: this.player.config.classNames.previewThumbnails.timeContainer, - }); - - this.elements.thumb.time = createElement('span', {}, '00:00'); - timeContainer.appendChild(this.elements.thumb.time); - - this.elements.thumb.container.appendChild(timeContainer); - - // Inject the whole thumb - if (is.element(this.player.elements.progress)) { - this.player.elements.progress.appendChild(this.elements.thumb.container); - } - - // Create HTML element: plyr__preview-scrubbing-container - this.elements.scrubbing.container = createElement('div', { - class: this.player.config.classNames.previewThumbnails.scrubbingContainer, - }); - - this.player.elements.wrapper.appendChild(this.elements.scrubbing.container); - } - - showImageAtCurrentTime() { - if (this.mouseDown) { - this.setScrubbingContainerSize(); - } else { - this.setThumbContainerSizeAndPos(); - } - - // Find the desired thumbnail index - // TODO: Handle a video longer than the thumbs where thumbNum is null - const thumbNum = this.thumbnails[0].frames.findIndex( - frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime, - ); - const hasThumb = thumbNum >= 0; - let qualityIndex = 0; - - // Show the thumb container if we're not scrubbing - if (!this.mouseDown) { - this.toggleThumbContainer(hasThumb); - } - - // No matching thumb found - if (!hasThumb) { - return; - } - - // Check to see if we've already downloaded higher quality versions of this image - this.thumbnails.forEach((thumbnail, index) => { - if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) { - qualityIndex = index; - } - }); - - // Only proceed if either thumbnum or thumbfilename has changed - if (thumbNum !== this.showingThumb) { - this.showingThumb = thumbNum; - this.loadImage(qualityIndex); - } - } - - // Show the image that's currently specified in this.showingThumb - loadImage(qualityIndex = 0) { - const thumbNum = this.showingThumb; - const thumbnail = this.thumbnails[qualityIndex]; - const { urlPrefix } = thumbnail; - const frame = thumbnail.frames[thumbNum]; - const thumbFilename = thumbnail.frames[thumbNum].text; - const thumbUrl = urlPrefix + thumbFilename; - - if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) { - // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one - // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort - if (this.loadingImage && this.usingSprites) { - this.loadingImage.onload = null; - } - - // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image - // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background - // images causes a flicker. Putting a new image over the top does not - const previewImage = new Image(); - previewImage.src = thumbUrl; - previewImage.dataset.index = thumbNum; - previewImage.dataset.filename = thumbFilename; - this.showingThumbFilename = thumbFilename; - - this.player.debug.log(`Loading image: ${thumbUrl}`); - - // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function... - previewImage.onload = () => - this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true); - this.loadingImage = previewImage; - this.removeOldImages(previewImage); - } else { - // Update the existing image - this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false); - this.currentImageElement.dataset.index = thumbNum; - this.removeOldImages(this.currentImageElement); - } - } - - showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) { - this.player.debug.log( - `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`, - ); - this.setImageSizeAndOffset(previewImage, frame); - - if (newImage) { - this.currentImageContainer.appendChild(previewImage); - this.currentImageElement = previewImage; - - if (!this.loadedImages.includes(thumbFilename)) { - this.loadedImages.push(thumbFilename); - } - } - - // Preload images before and after the current one - // Show higher quality of the same frame - // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading - this.preloadNearby(thumbNum, true) - .then(this.preloadNearby(thumbNum, false)) - .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename)); - } - - // Remove all preview images that aren't the designated current image - removeOldImages(currentImage) { - // Get a list of all images, convert it from a DOM list to an array - Array.from(this.currentImageContainer.children).forEach(image => { - if (image.tagName.toLowerCase() !== 'img') { - return; - } - - const removeDelay = this.usingSprites ? 500 : 1000; - - if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) { - // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients - // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function - // eslint-disable-next-line no-param-reassign - image.dataset.deleting = true; - - // This has to be set before the timeout - to prevent issues switching between hover and scrub - const { currentImageContainer } = this; - - setTimeout(() => { - currentImageContainer.removeChild(image); - this.player.debug.log(`Removing thumb: ${image.dataset.filename}`); - }, removeDelay); - } - }); - } - - // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame - // This will only preload the lowest quality - preloadNearby(thumbNum, forward = true) { - return new Promise(resolve => { - setTimeout(() => { - const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text; - - if (this.showingThumbFilename === oldThumbFilename) { - // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away - let thumbnailsClone; - if (forward) { - thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum); - } else { - thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse(); - } - - let foundOne = false; - - thumbnailsClone.forEach(frame => { - const newThumbFilename = frame.text; - - if (newThumbFilename !== oldThumbFilename) { - // Found one with a different filename. Make sure it hasn't already been loaded on this page visit - if (!this.loadedImages.includes(newThumbFilename)) { - foundOne = true; - this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`); - - const { urlPrefix } = this.thumbnails[0]; - const thumbURL = urlPrefix + newThumbFilename; - const previewImage = new Image(); - previewImage.src = thumbURL; - previewImage.onload = () => { - this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`); - if (!this.loadedImages.includes(newThumbFilename)) - this.loadedImages.push(newThumbFilename); - - // We don't resolve until the thumb is loaded - resolve(); - }; - } - } - }); - - // If there are none to preload then we want to resolve immediately - if (!foundOne) { - resolve(); - } - } - }, 300); - }); - } - - // If user has been hovering current image for half a second, look for a higher quality one - getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) { - if (currentQualityIndex < this.thumbnails.length - 1) { - // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container - let previewImageHeight = previewImage.naturalHeight; - - if (this.usingSprites) { - previewImageHeight = frame.h; - } - - if (previewImageHeight < this.thumbContainerHeight) { - // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while - setTimeout(() => { - // Make sure the mouse hasn't already moved on and started hovering at another image - if (this.showingThumbFilename === thumbFilename) { - this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`); - this.loadImage(currentQualityIndex + 1); - } - }, 300); - } - } - } - - get currentImageContainer() { - if (this.mouseDown) { - return this.elements.scrubbing.container; - } - - return this.elements.thumb.imageContainer; - } - - get usingSprites() { - return Object.keys(this.thumbnails[0].frames[0]).includes('w'); - } - - get thumbAspectRatio() { - if (this.usingSprites) { - return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h; - } - - return this.thumbnails[0].width / this.thumbnails[0].height; - } - - get thumbContainerHeight() { - if (this.mouseDown) { - // Can't use media.clientHeight - HTML5 video goes big and does black bars above and below - return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio); - } - - return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4); - } - - get currentImageElement() { - if (this.mouseDown) { - return this.currentScrubbingImageElement; - } - - return this.currentThumbnailImageElement; - } - - set currentImageElement(element) { - if (this.mouseDown) { - this.currentScrubbingImageElement = element; - } else { - this.currentThumbnailImageElement = element; - } - } - - toggleThumbContainer(toggle = false, clearShowing = false) { - const className = this.player.config.classNames.previewThumbnails.thumbContainerShown; - this.elements.thumb.container.classList.toggle(className, toggle); - - if (!toggle && clearShowing) { - this.showingThumb = null; - this.showingThumbFilename = null; - } - } - - toggleScrubbingContainer(toggle = false) { - const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown; - this.elements.scrubbing.container.classList.toggle(className, toggle); - - if (!toggle) { - this.showingThumb = null; - this.showingThumbFilename = null; - } - } - - determineContainerAutoSizing() { - if (this.elements.thumb.imageContainer.clientHeight > 20) { - // This will prevent auto sizing in this.setThumbContainerSizeAndPos() - this.sizeSpecifiedInCSS = true; - } - } - - // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS - setThumbContainerSizeAndPos() { - if (!this.sizeSpecifiedInCSS) { - const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio); - this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`; - this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`; - } - - this.setThumbContainerPos(); - } - - setThumbContainerPos() { - const seekbarRect = this.player.elements.progress.getBoundingClientRect(); - const plyrRect = this.player.elements.container.getBoundingClientRect(); - const { container } = this.elements.thumb; - // Find the lowest and highest desired left-position, so we don't slide out the side of the video container - const minVal = plyrRect.left - seekbarRect.left + 10; - const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10; - // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth - let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2; - - if (previewPos < minVal) { - previewPos = minVal; - } - - if (previewPos > maxVal) { - previewPos = maxVal; - } - - container.style.left = `${previewPos}px`; - } - - // Can't use 100% width, in case the video is a different aspect ratio to the video container - setScrubbingContainerSize() { - this.elements.scrubbing.container.style.width = `${this.player.media.clientWidth}px`; - // Can't use media.clientHeight - html5 video goes big and does black bars above and below - this.elements.scrubbing.container.style.height = `${this.player.media.clientWidth / this.thumbAspectRatio}px`; - } - - // Sprites need to be offset to the correct location - setImageSizeAndOffset(previewImage, frame) { - if (!this.usingSprites) { - return; - } - - // Find difference between height and preview container height - const multiplier = this.thumbContainerHeight / frame.h; - - // eslint-disable-next-line no-param-reassign - previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`; - // eslint-disable-next-line no-param-reassign - previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`; - // eslint-disable-next-line no-param-reassign - previewImage.style.left = `-${frame.x * multiplier}px`; - // eslint-disable-next-line no-param-reassign - previewImage.style.top = `-${frame.y * multiplier}px`; - } -} - -export default PreviewThumbnails; diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index bef48708..91019abf 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -9,7 +9,7 @@ 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 loadScript from '../utils/load-script'; import { extend } from '../utils/objects'; import { format, stripHTML } from '../utils/strings'; import { setAspectRatio } from '../utils/style'; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 34c5de7e..31d22bb4 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -7,8 +7,8 @@ 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 loadImage from '../utils/load-image'; +import loadScript from '../utils/load-script'; import { extend } from '../utils/objects'; import { format, generateId } from '../utils/strings'; import { setAspectRatio } from '../utils/style'; diff --git a/src/js/plyr.js b/src/js/plyr.js index 84fa87fa..3835cf94 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -15,7 +15,7 @@ import Fullscreen from './fullscreen'; import Listeners from './listeners'; import media from './media'; import Ads from './plugins/ads'; -import PreviewThumbnails from './plugins/previewThumbnails'; +import PreviewThumbnails from './plugins/preview-thumbnails'; import source from './source'; import Storage from './storage'; import support from './support'; @@ -24,7 +24,7 @@ import { closest } from './utils/arrays'; import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements'; import { off, on, once, triggerEvent, unbindListeners } from './utils/events'; import is from './utils/is'; -import loadSprite from './utils/loadSprite'; +import loadSprite from './utils/load-sprite'; import { clamp } from './utils/numbers'; import { cloneDeep, extend } from './utils/objects'; import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style'; diff --git a/src/js/ui.js b/src/js/ui.js index df52eb64..953ecba2 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -10,7 +10,7 @@ import { getElement, toggleClass } from './utils/elements'; import { ready, triggerEvent } from './utils/events'; import i18n from './utils/i18n'; import is from './utils/is'; -import loadImage from './utils/loadImage'; +import loadImage from './utils/load-image'; const ui = { addStyleHook() { diff --git a/src/js/utils/load-image.js b/src/js/utils/load-image.js new file mode 100644 index 00000000..8acd2496 --- /dev/null +++ b/src/js/utils/load-image.js @@ -0,0 +1,19 @@ +// ========================================================================== +// Load image avoiding xhr/fetch CORS issues +// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded +// By default it checks if it is at least 1px, but you can add a second argument to change this +// ========================================================================== + +export default function loadImage(src, minWidth = 1) { + return new Promise((resolve, reject) => { + const image = new Image(); + + const handler = () => { + delete image.onload; + delete image.onerror; + (image.naturalWidth >= minWidth ? resolve : reject)(image); + }; + + Object.assign(image, { onload: handler, onerror: handler, src }); + }); +} diff --git a/src/js/utils/load-script.js b/src/js/utils/load-script.js new file mode 100644 index 00000000..81ae36f4 --- /dev/null +++ b/src/js/utils/load-script.js @@ -0,0 +1,14 @@ +// ========================================================================== +// Load an external script +// ========================================================================== + +import loadjs from 'loadjs'; + +export default function loadScript(url) { + return new Promise((resolve, reject) => { + loadjs(url, { + success: resolve, + error: reject, + }); + }); +} diff --git a/src/js/utils/load-sprite.js b/src/js/utils/load-sprite.js new file mode 100644 index 00000000..fe4add00 --- /dev/null +++ b/src/js/utils/load-sprite.js @@ -0,0 +1,75 @@ +// ========================================================================== +// Sprite loader +// ========================================================================== + +import Storage from '../storage'; +import fetch from './fetch'; +import is from './is'; + +// Load an external SVG sprite +export default function loadSprite(url, id) { + if (!is.string(url)) { + return; + } + + const prefix = 'cache'; + const hasId = is.string(id); + let isCached = false; + const exists = () => document.getElementById(id) !== null; + + const update = (container, data) => { + // eslint-disable-next-line no-param-reassign + container.innerHTML = data; + + // Check again incase of race condition + if (hasId && exists()) { + return; + } + + // Inject the SVG to the body + document.body.insertAdjacentElement('afterbegin', container); + }; + + // Only load once if ID set + if (!hasId || !exists()) { + const useStorage = Storage.supported; + // Create container + const container = document.createElement('div'); + container.setAttribute('hidden', ''); + + if (hasId) { + container.setAttribute('id', id); + } + + // Check in cache + if (useStorage) { + const cached = window.localStorage.getItem(`${prefix}-${id}`); + isCached = cached !== null; + + if (isCached) { + const data = JSON.parse(cached); + update(container, data.content); + } + } + + // Get the sprite + fetch(url) + .then(result => { + if (is.empty(result)) { + return; + } + + if (useStorage) { + window.localStorage.setItem( + `${prefix}-${id}`, + JSON.stringify({ + content: result, + }), + ); + } + + update(container, result); + }) + .catch(() => {}); + } +} diff --git a/src/js/utils/loadImage.js b/src/js/utils/loadImage.js deleted file mode 100644 index 8acd2496..00000000 --- a/src/js/utils/loadImage.js +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Load image avoiding xhr/fetch CORS issues -// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded -// By default it checks if it is at least 1px, but you can add a second argument to change this -// ========================================================================== - -export default function loadImage(src, minWidth = 1) { - return new Promise((resolve, reject) => { - const image = new Image(); - - const handler = () => { - delete image.onload; - delete image.onerror; - (image.naturalWidth >= minWidth ? resolve : reject)(image); - }; - - Object.assign(image, { onload: handler, onerror: handler, src }); - }); -} diff --git a/src/js/utils/loadScript.js b/src/js/utils/loadScript.js deleted file mode 100644 index 81ae36f4..00000000 --- a/src/js/utils/loadScript.js +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Load an external script -// ========================================================================== - -import loadjs from 'loadjs'; - -export default function loadScript(url) { - return new Promise((resolve, reject) => { - loadjs(url, { - success: resolve, - error: reject, - }); - }); -} diff --git a/src/js/utils/loadSprite.js b/src/js/utils/loadSprite.js deleted file mode 100644 index fe4add00..00000000 --- a/src/js/utils/loadSprite.js +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Sprite loader -// ========================================================================== - -import Storage from '../storage'; -import fetch from './fetch'; -import is from './is'; - -// Load an external SVG sprite -export default function loadSprite(url, id) { - if (!is.string(url)) { - return; - } - - const prefix = 'cache'; - const hasId = is.string(id); - let isCached = false; - const exists = () => document.getElementById(id) !== null; - - const update = (container, data) => { - // eslint-disable-next-line no-param-reassign - container.innerHTML = data; - - // Check again incase of race condition - if (hasId && exists()) { - return; - } - - // Inject the SVG to the body - document.body.insertAdjacentElement('afterbegin', container); - }; - - // Only load once if ID set - if (!hasId || !exists()) { - const useStorage = Storage.supported; - // Create container - const container = document.createElement('div'); - container.setAttribute('hidden', ''); - - if (hasId) { - container.setAttribute('id', id); - } - - // Check in cache - if (useStorage) { - const cached = window.localStorage.getItem(`${prefix}-${id}`); - isCached = cached !== null; - - if (isCached) { - const data = JSON.parse(cached); - update(container, data.content); - } - } - - // Get the sprite - fetch(url) - .then(result => { - if (is.empty(result)) { - return; - } - - if (useStorage) { - window.localStorage.setItem( - `${prefix}-${id}`, - JSON.stringify({ - content: result, - }), - ); - } - - update(container, result); - }) - .catch(() => {}); - } -} diff --git a/src/sass/plugins/preview-thumbnails.scss b/src/sass/plugins/preview-thumbnails.scss new file mode 100644 index 00000000..b2b272c1 --- /dev/null +++ b/src/sass/plugins/preview-thumbnails.scss @@ -0,0 +1,118 @@ +// -------------------------------------------------------------- +// Preview Thumbnails +// -------------------------------------------------------------- + +$plyr-preview-padding: $plyr-tooltip-padding !default; +$plyr-preview-bg: $plyr-tooltip-bg !default; +$plyr-preview-radius: $plyr-tooltip-radius !default; +$plyr-preview-shadow: $plyr-tooltip-shadow !default; +$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default; +$plyr-preview-image-bg: $plyr-color-gray-2 !default; +$plyr-preview-time-font-size: $plyr-font-size-time !default; +$plyr-preview-time-padding: 3px 6px !default; +$plyr-preview-time-bg: rgba(0, 0, 0, 0.55); +$plyr-preview-time-color: #fff; +$plyr-preview-time-bottom-offset: 6px; + +.plyr__preview-thumb { + background-color: $plyr-preview-bg; + border-radius: 3px; + bottom: 100%; + box-shadow: $plyr-preview-shadow; + margin-bottom: $plyr-preview-padding * 2; + opacity: 0; + padding: $plyr-preview-radius; + pointer-events: none; + position: absolute; + transform: translate(0, 10px) scale(0.8); + transform-origin: 50% 100%; + transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease; + z-index: 2; + + &--is-shown { + opacity: 1; + transform: translate(0, 0) scale(1); + } + + // The background triangle + &::before { + border-left: $plyr-preview-arrow-size solid transparent; + border-right: $plyr-preview-arrow-size solid transparent; + border-top: $plyr-preview-arrow-size solid $plyr-preview-bg; + bottom: -$plyr-preview-arrow-size; + content: ''; + height: 0; + left: 50%; + position: absolute; + transform: translateX(-50%); + width: 0; + z-index: 2; + } + + &__image-container { + background: $plyr-preview-image-bg; + border-radius: ($plyr-preview-radius - 1px); + overflow: hidden; + position: relative; + z-index: 0; + + img { + height: 100%; // Non sprite images are 100%. Sprites will have their size applied by JavaScript + left: 0; + max-height: none; + max-width: none; + position: absolute; + top: 0; + width: 100%; + } + } + + // Seek time text + &__time-container { + bottom: $plyr-preview-time-bottom-offset; + left: 0; + position: absolute; + right: 0; + white-space: nowrap; + z-index: 3; + + span { + background-color: $plyr-preview-time-bg; + border-radius: ($plyr-preview-radius - 1px); + color: $plyr-preview-time-color; + font-size: $plyr-preview-time-font-size; + padding: $plyr-preview-time-padding; + } + } +} + +.plyr__preview-scrubbing { + bottom: 0; + filter: blur(1px); + height: 100%; + left: 0; + margin: auto; // Required when video is different dimensions to container (e.g. fullscreen) + opacity: 0; + overflow: hidden; + position: absolute; + right: 0; + top: 0; + transition: opacity 0.3s ease; + width: 100%; + z-index: 1; + + &--is-shown { + opacity: 1; + } + + img { + height: 100%; + left: 0; + max-height: none; + max-width: none; + object-fit: contain; + position: absolute; + top: 0; + width: 100%; + } +} diff --git a/src/sass/plugins/previewThumbnails.scss b/src/sass/plugins/previewThumbnails.scss deleted file mode 100644 index b2b272c1..00000000 --- a/src/sass/plugins/previewThumbnails.scss +++ /dev/null @@ -1,118 +0,0 @@ -// -------------------------------------------------------------- -// Preview Thumbnails -// -------------------------------------------------------------- - -$plyr-preview-padding: $plyr-tooltip-padding !default; -$plyr-preview-bg: $plyr-tooltip-bg !default; -$plyr-preview-radius: $plyr-tooltip-radius !default; -$plyr-preview-shadow: $plyr-tooltip-shadow !default; -$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default; -$plyr-preview-image-bg: $plyr-color-gray-2 !default; -$plyr-preview-time-font-size: $plyr-font-size-time !default; -$plyr-preview-time-padding: 3px 6px !default; -$plyr-preview-time-bg: rgba(0, 0, 0, 0.55); -$plyr-preview-time-color: #fff; -$plyr-preview-time-bottom-offset: 6px; - -.plyr__preview-thumb { - background-color: $plyr-preview-bg; - border-radius: 3px; - bottom: 100%; - box-shadow: $plyr-preview-shadow; - margin-bottom: $plyr-preview-padding * 2; - opacity: 0; - padding: $plyr-preview-radius; - pointer-events: none; - position: absolute; - transform: translate(0, 10px) scale(0.8); - transform-origin: 50% 100%; - transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease; - z-index: 2; - - &--is-shown { - opacity: 1; - transform: translate(0, 0) scale(1); - } - - // The background triangle - &::before { - border-left: $plyr-preview-arrow-size solid transparent; - border-right: $plyr-preview-arrow-size solid transparent; - border-top: $plyr-preview-arrow-size solid $plyr-preview-bg; - bottom: -$plyr-preview-arrow-size; - content: ''; - height: 0; - left: 50%; - position: absolute; - transform: translateX(-50%); - width: 0; - z-index: 2; - } - - &__image-container { - background: $plyr-preview-image-bg; - border-radius: ($plyr-preview-radius - 1px); - overflow: hidden; - position: relative; - z-index: 0; - - img { - height: 100%; // Non sprite images are 100%. Sprites will have their size applied by JavaScript - left: 0; - max-height: none; - max-width: none; - position: absolute; - top: 0; - width: 100%; - } - } - - // Seek time text - &__time-container { - bottom: $plyr-preview-time-bottom-offset; - left: 0; - position: absolute; - right: 0; - white-space: nowrap; - z-index: 3; - - span { - background-color: $plyr-preview-time-bg; - border-radius: ($plyr-preview-radius - 1px); - color: $plyr-preview-time-color; - font-size: $plyr-preview-time-font-size; - padding: $plyr-preview-time-padding; - } - } -} - -.plyr__preview-scrubbing { - bottom: 0; - filter: blur(1px); - height: 100%; - left: 0; - margin: auto; // Required when video is different dimensions to container (e.g. fullscreen) - opacity: 0; - overflow: hidden; - position: absolute; - right: 0; - top: 0; - transition: opacity 0.3s ease; - width: 100%; - z-index: 1; - - &--is-shown { - opacity: 1; - } - - img { - height: 100%; - left: 0; - max-height: none; - max-width: none; - object-fit: contain; - position: absolute; - top: 0; - width: 100%; - } -} diff --git a/src/sass/plyr.scss b/src/sass/plyr.scss index 2b6cfa20..144297f7 100644 --- a/src/sass/plyr.scss +++ b/src/sass/plyr.scss @@ -41,7 +41,7 @@ @import 'states/fullscreen'; @import 'plugins/ads'; -@import 'plugins/previewThumbnails'; +@import 'plugins/preview-thumbnails'; @import 'utils/animation'; @import 'utils/hidden'; -- cgit v1.2.3 From dfc09b8e04f6e4829c29a68106eb4af5be76a2ff Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 21 Jun 2019 00:24:28 +1000 Subject: v3.5.5 deployed --- src/js/config/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 4bb0e8d5..cdac3136 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -61,7 +61,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.5.4/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.5.5/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 3835cf94..980f26bc 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.5.4 +// plyr.js v3.5.5 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index c87bbd8c..ed0495ff 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.5.4 +// plyr.js v3.5.5 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From 2488299d7b91dd4face0fa0521b1af4bbde3ba52 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 21 Jun 2019 12:34:49 +1000 Subject: Edge fix --- src/sass/components/controls.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss index f4559bba..8abee204 100644 --- a/src/sass/components/controls.scss +++ b/src/sass/components/controls.scss @@ -12,11 +12,11 @@ align-items: center; display: flex; justify-content: flex-end; - min-width: 0; // Fix for Edge issue where content would overflow text-align: center; .plyr__progress__container { flex: 1; + min-width: 0; // Fix for Edge issue where content would overflow } // Spacing -- cgit v1.2.3 From 7c442c93571fb5d75f96bff710e310d00e1289e9 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 21 Jun 2019 12:35:47 +1000 Subject: 3.5.6 --- src/js/config/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index cdac3136..c50a8900 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -61,7 +61,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.5.5/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.5.6/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 980f26bc..f30d334a 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.5.5 +// plyr.js v3.5.6 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index ed0495ff..8a1cb7a1 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.5.5 +// plyr.js v3.5.6 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From 400fd77d0afae6410e8c78053288ae9fcf6349d2 Mon Sep 17 00:00:00 2001 From: Antony O'Neill Date: Thu, 4 Jul 2019 19:02:22 +0100 Subject: Prevent default on settings icon click --- src/js/listeners.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index c5076ff3..bcb3571e 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -603,12 +603,19 @@ class Listeners { this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle - this.bind(elements.buttons.settings, 'click', event => { - // Prevent the document click listener closing the menu - event.stopPropagation(); + this.bind( + elements.buttons.settings, + 'click', + event => { + // Prevent the document click listener closing the menu + event.stopPropagation(); + event.preventDefault(); - controls.toggleMenu.call(player, event); - }); + controls.toggleMenu.call(player, event); + }, + null, + false + ); // Can't be passive as we're preventing default // Settings menu - keyboard toggle // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus -- cgit v1.2.3 From b36b92b2478b5aac165be0d3d84cd1f011d4c8df Mon Sep 17 00:00:00 2001 From: nskazki Date: Fri, 19 Jul 2019 16:12:16 +0300 Subject: Detach event listeners on destroy if "on" receives "this", it attaches "eventListeners" array to "this" and stores "{ element, type, callback }" hash in there. later, during the destruction process, all the entries from this array will be processed by "unbindListeners" helper to detach all the event listeners. --- src/js/controls.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 7afcd2c0..b5b2d00a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -402,7 +402,8 @@ const controls = { // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 bindMenuItemShortcuts(menuItem, type) { // Navigate through menus via arrow keys and space - on( + on.call( + this, menuItem, 'keydown keyup', event => { @@ -452,7 +453,7 @@ const controls = { // Enter will fire a `click` event but we still need to manage focus // So we bind to keyup which fires after and set focus here - on(menuItem, 'keyup', event => { + on.call(this, menuItem, 'keyup', event => { if (event.which !== 13) { return; } @@ -1463,7 +1464,7 @@ const controls = { bindMenuItemShortcuts.call(this, menuItem, type); // Show menu on click - on(menuItem, 'click', () => { + on.call(this, menuItem, 'click', () => { showMenuPanel.call(this, type, false); }); @@ -1515,7 +1516,8 @@ const controls = { ); // Go back via keyboard - on( + on.call( + this, pane, 'keydown', event => { @@ -1535,7 +1537,7 @@ const controls = { ); // Go back via button click - on(backButton, 'click', () => { + on.call(this, backButton, 'click', () => { showMenuPanel.call(this, 'home', false); }); -- cgit v1.2.3 From d771da9abf4acde9b4b9b8fb5ee703c16e12785c Mon Sep 17 00:00:00 2001 From: Aziz Khambati Date: Tue, 30 Jul 2019 18:19:10 +0530 Subject: Toggle also returns promise --- src/js/plyr.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/js/plyr.js b/src/js/plyr.js index f30d334a..8d4ab203 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -371,7 +371,7 @@ class Plyr { return; } - this.media.pause(); + return this.media.pause(); } /** @@ -411,9 +411,9 @@ class Plyr { const toggle = is.boolean(input) ? input : !this.playing; if (toggle) { - this.play(); + return this.play(); } else { - this.pause(); + return this.pause(); } } -- cgit v1.2.3 From 800c8e0a17dd5c86f2fc38a7d13523c4ab747d2d Mon Sep 17 00:00:00 2001 From: ondratra Date: Wed, 7 Aug 2019 00:08:10 +0200 Subject: typescript typings --- src/js/plyr.d.ts | 560 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 src/js/plyr.d.ts (limited to 'src') diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts new file mode 100644 index 00000000..4f64898f --- /dev/null +++ b/src/js/plyr.d.ts @@ -0,0 +1,560 @@ +// Type definitions for plyr 3.5 +// Project: https://plyr.io +// Definitions by: ondratra +// TypeScript Version: 3.0 + +export = Plyr; +export as namespace Plyr; + + +declare class Plyr { + /** + * Setup a new instance + */ + static setup(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options): Plyr[]; + + /** + * Check for support + * @param mediaType + * @param provider + * @param playsInline Whether the player has the playsinline attribute (only applicable to iOS 10+) + */ + static supported(mediaType?: Plyr.MediaType, provider?: Plyr.Provider, playsInline?: boolean): Plyr.Support; + + constructor(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options); + + /** + * Indicates if the current player is HTML5. + */ + readonly isHTML5: boolean; + + /** + * Indicates if the current player is an embedded player. + */ + readonly isEmbed: boolean; + + /** + * Indicates if the current player is playing. + */ + readonly playing: boolean; + + /** + * Indicates if the current player is paused. + */ + readonly paused: boolean; + + /** + * Indicates if the current player is stopped. + */ + readonly stopped: boolean; + + /** + * Indicates if the current player has finished playback. + */ + readonly ended: boolean; + + /** + * Returns a float between 0 and 1 indicating how much of the media is buffered + */ + readonly buffered: number; + + /** + * Gets or sets the currentTime for the player. The setter accepts a float in seconds. + */ + currentTime: number; + + /** + * Indicates if the current player is seeking. + */ + readonly seeking: boolean; + + /** + * Returns the duration for the current media. + */ + readonly duration: number; + + /** + * Gets or sets the volume for the player. The setter accepts a float between 0 and 1. + */ + volume: number; + + /** + * Gets or sets the muted state of the player. The setter accepts a boolean. + */ + muted: boolean; + + /** + * Indicates if the current media has an audio track. + */ + readonly hasAudio: boolean; + + /** + * 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: number; + + /** + * Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. + * Remarks: YouTube only. HTML5 will follow. + */ + quality: string; + + /** + * Gets or sets the current loop state of the player. + */ + loop: boolean; + + /** + * Gets or sets the current source for the player. + */ + source: Plyr.SourceInfo; + + /** + * Gets or sets the current poster image URL for the player. + */ + poster: string; + + /** + * Gets or sets the autoplay state of the player. + */ + autoplay: boolean; + + /** + * Gets or sets the caption track by index. 1 means the track is missing or captions is not active + */ + currentTrack: number; + + /** + * Gets or sets the preferred captions language for the player. The setter accepts an ISO twoletter language code. Support for the languages is dependent on the captions you include. + * If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use currentTrack instead. + */ + language: string; + + /** + * Gets or sets the picture-in-picture state of the player. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. + */ + pip: boolean; + + readonly fullscreen: Plyr.FullscreenControl; + + /** + * Start playback. + * For HTML5 players, play() will return a Promise in some browsers - WebKit and Mozilla according to MDN at time of writing. + */ + play(): Promise | void; + + /** + * Pause playback. + */ + pause(): void; + + /** + * Toggle playback, if no parameters are passed, it will toggle based on current status. + */ + togglePlay(toggle?: boolean): boolean; + + /** + * Stop playback and reset to start. + */ + stop(): void; + + /** + * Restart playback. + */ + restart(): void; + + /** + * Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. + */ + rewind(seekTime?: number): void; + + /** + * Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. + */ + forward(seekTime?: number): void; + + /** + * Increase volume by the specified step. If no parameter is passed, the default step will be used. + */ + increaseVolume(step?: number): void; + + /** + * Increase volume by the specified step. If no parameter is passed, the default step will be used. + */ + decreaseVolume(step?: number): void; + + /** + * Toggle captions display. If no parameter is passed, it will toggle based on current status. + */ + toggleCaptions(toggle?: boolean): void; + + /** + * Trigger the airplay dialog on supported devices. + */ + airplay(): void; + + /** + * Toggle the controls (video only). Takes optional truthy value to force it on/off. + */ + toggleControls(toggle: boolean): void; + + /** + * Add an event listener for the specified event. + */ + on(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void; + + /** + * Add an event listener for the specified event once. + */ + once(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void; + + /** + * Remove an event listener for the specified event. + */ + off(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void; + + /** + * Check support for a mime type. + */ + supports(type: string): boolean; + + /** + * Destroy lib instance + */ + destroy(): void; +} + +declare namespace Plyr { + type MediaType = "audio" | "video"; + type Provider = "html5" | "youtube" | "vimeo"; + type StandardEvent = "progress" | "playing" | "play" | "pause" | "timeupdate" | "volumechange" | "seeking" | "seeked" | "ratechange" | "ended" | "enterfullscreen" | "exitfullscreen" + | "captionsenabled" | "captionsdisabled" | "languagechange" | "controlshidden" | "controlsshown" | "ready"; + type Html5Event = "loadstart" | "loadeddata" | "loadedmetadata" | "canplay" | "canplaythrough" | "stalled" | "waiting" | "emptied" | "cuechange" | "error"; + type YoutubeEvent = "statechange" | "qualitychange" | "qualityrequested"; + + interface FullscreenControl { + /** + * Indicates if the current player is in fullscreen mode. + */ + readonly active: boolean; + + /** + * Indicates if the current player has fullscreen enabled. + */ + readonly enabled: boolean; + + /** + * Enter fullscreen. If fullscreen is not supported, a fallback ""full window/viewport"" is used instead. + */ + enter(): void; + + /** + * Exit fullscreen. + */ + exit(): void; + + /** + * Toggle fullscreen. + */ + toggle(): void; + } + + interface Options { + /** + * Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. + */ + enabled?: boolean; + + /** + * Display debugging information in the console + */ + debug?: boolean; + + /** + * If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; + * id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See controls.md for more info on how the html needs to be structured. + * Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'] + */ + controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element; + + /** + * If you're using the default controls are used then you can specify which settings to show in the menu + * Defaults to ['captions', 'quality', 'speed', 'loop'] + */ + settings?: string[]; + + /** + * Used for internationalization (i18n) of the text within the UI. + */ + i18n?: any; + + /** + * Load the SVG sprite specified as the iconUrl option (if a URL). If false, it is assumed you are handling sprite loading yourself. + */ + loadSprite?: boolean; + + /** + * Specify a URL or path to the SVG sprite. See the SVG section for more info. + */ + iconUrl?: string; + + /** + * Specify the id prefix for the icons used in the default controls (e.g. plyr-play would be plyr). + * This is to prevent clashes if you're using your own SVG sprite but with the default controls. + * Most people can ignore this option. + */ + iconPrefix?: string; + + /** + * Specify a URL or path to a blank video file used to properly cancel network requests. + */ + blankUrl?: string; + + /** + * Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. + * If the autoplay attribute is present on a