diff options
Diffstat (limited to 'src/js')
| -rw-r--r-- | src/js/captions.js | 26 | ||||
| -rw-r--r-- | src/js/config/defaults.js | 9 | ||||
| -rw-r--r-- | src/js/controls.js | 52 | ||||
| -rw-r--r-- | src/js/fullscreen.js | 16 | ||||
| -rw-r--r-- | src/js/html5.js | 8 | ||||
| -rw-r--r-- | src/js/listeners.js | 99 | ||||
| -rw-r--r-- | src/js/media.js | 1 | ||||
| -rw-r--r-- | src/js/plugins/ads.js | 20 | ||||
| -rw-r--r-- | src/js/plugins/preview-thumbnails.js | 24 | ||||
| -rw-r--r-- | src/js/plugins/vimeo.js | 64 | ||||
| -rw-r--r-- | src/js/plugins/youtube.js | 71 | ||||
| -rw-r--r-- | src/js/plyr.d.ts | 109 | ||||
| -rw-r--r-- | src/js/plyr.js | 17 | ||||
| -rw-r--r-- | src/js/source.js | 2 | ||||
| -rw-r--r-- | src/js/ui.js | 13 | ||||
| -rw-r--r-- | src/js/utils/animation.js | 2 | ||||
| -rw-r--r-- | src/js/utils/elements.js | 4 | ||||
| -rw-r--r-- | src/js/utils/events.js | 6 | ||||
| -rw-r--r-- | src/js/utils/is.js | 38 | ||||
| -rw-r--r-- | src/js/utils/load-sprite.js | 2 | ||||
| -rw-r--r-- | src/js/utils/objects.js | 2 | ||||
| -rw-r--r-- | src/js/utils/strings.js | 2 | ||||
| -rw-r--r-- | src/js/utils/style.js | 8 | ||||
| -rw-r--r-- | src/js/utils/time.js | 8 | 
24 files changed, 340 insertions, 263 deletions
| diff --git a/src/js/captions.js b/src/js/captions.js index ebb678f8..98d7d613 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -56,7 +56,7 @@ const captions = {      if (browser.isIE && window.URL) {        const elements = this.media.querySelectorAll('track'); -      Array.from(elements).forEach(track => { +      Array.from(elements).forEach((track) => {          const src = track.getAttribute('src');          const url = parseUrl(src); @@ -66,7 +66,7 @@ const captions = {            ['http:', 'https:'].includes(url.protocol)          ) {            fetch(src, 'blob') -            .then(blob => { +            .then((blob) => {                track.setAttribute('src', window.URL.createObjectURL(blob));              })              .catch(() => { @@ -84,7 +84,7 @@ const captions = {      // * toggled:   The real captions state      const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; -    const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); +    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' @@ -119,13 +119,13 @@ const captions = {      const tracks = captions.getTracks.call(this, true);      // Get the wanted language      const { active, language, meta, currentTrackNode } = this.captions; -    const languageExists = Boolean(tracks.find(track => track.language === language)); +    const languageExists = Boolean(tracks.find((track) => track.language === language));      // Handle tracks (add event listener and "pseudo"-default)      if (this.isHTML5 && this.isVideo) {        tracks -        .filter(track => !meta.get(track)) -        .forEach(track => { +        .filter((track) => !meta.get(track)) +        .forEach((track) => {            this.debug.log('Track added', track);            // Attempt to store if the original dom element was "default" @@ -309,19 +309,19 @@ const captions = {      // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)      // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)      return tracks -      .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) -      .filter(track => ['captions', 'subtitles'].includes(track.kind)); +      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track)) +      .filter((track) => ['captions', 'subtitles'].includes(track.kind));    },    // Match tracks based on languages and get the first    findTrack(languages, force = false) {      const tracks = captions.getTracks.call(this); -    const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); +    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);      const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));      let track; -    languages.every(language => { -      track = sorted.find(t => t.language === language); +    languages.every((language) => { +      track = sorted.find((t) => t.language === language);        return !track; // Break iteration if there is a match      }); @@ -383,12 +383,12 @@ const captions = {        const track = captions.getCurrentTrack.call(this);        cues = Array.from((track || {}).activeCues || []) -        .map(cue => cue.getCueAsHTML()) +        .map((cue) => cue.getCueAsHTML())          .map(getHTML);      }      // Set new caption text -    const content = cues.map(cueText => cueText.trim()).join('\n'); +    const content = cues.map((cueText) => cueText.trim()).join('\n');      const changed = content !== this.elements.captions.innerHTML;      if (changed) { diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 03c75150..a199d316 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -422,20 +422,23 @@ const defaults = {      title: false,      speed: true,      transparent: false, +    // Custom settings from Plyr +    customControls: true, +    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy      // Whether the owner of the video has a Pro or Business account      // (which allows us to properly hide controls without CSS hacks, etc)      premium: false, -    // Custom settings from Plyr -    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy    },    // YouTube plugin    youtube: { -    noCookie: true, // Whether to use an alternative version of YouTube without cookies      rel: 0, // No related vids      showinfo: 0, // Hide info      iv_load_policy: 3, // Hide annotations      modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) +    // Custom settings from Plyr +    customControls: true, +    noCookie: false, // Whether to use an alternative version of YouTube without cookies    },  }; diff --git a/src/js/controls.js b/src/js/controls.js index ad126de1..ff20982e 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -179,7 +179,7 @@ const controls = {        iconPressed: null,      }; -    ['element', 'icon', 'label'].forEach(key => { +    ['element', 'icon', 'label'].forEach((key) => {        if (Object.keys(attributes).includes(key)) {          props[key] = attributes[key];          delete attributes[key]; @@ -193,7 +193,7 @@ const controls = {      // Set class name      if (Object.keys(attributes).includes('class')) { -      if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) { +      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {          extend(attributes, {            class: `${attributes.class} ${this.config.classNames.control}`,          }); @@ -401,7 +401,7 @@ const controls = {        this,        menuItem,        'keydown keyup', -      event => { +      (event) => {          // We only care about space and ⬆️ ⬇️️ ➡️          if (![32, 38, 39, 40].includes(event.which)) {            return; @@ -448,7 +448,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.call(this, menuItem, 'keyup', event => { +    on.call(this, menuItem, 'keyup', (event) => {        if (event.which !== 13) {          return;        } @@ -493,8 +493,8 @@ const controls = {          // Ensure exclusivity          if (check) {            Array.from(menuItem.parentNode.children) -            .filter(node => matches(node, '[role="menuitemradio"]')) -            .forEach(node => node.setAttribute('aria-checked', 'false')); +            .filter((node) => matches(node, '[role="menuitemradio"]')) +            .forEach((node) => node.setAttribute('aria-checked', 'false'));          }          menuItem.setAttribute('aria-checked', check ? 'true' : 'false'); @@ -504,7 +504,7 @@ const controls = {      this.listeners.bind(        menuItem,        'click keyup', -      event => { +      (event) => {          if (is.keyboardEvent(event) && event.which !== 32) {            return;          } @@ -698,7 +698,7 @@ const controls = {      }      const visible = `${this.config.classNames.tooltip}--visible`; -    const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show); +    const toggle = (show) => toggleClass(this.elements.display.seekTooltip, visible, show);      // Hide on touch      if (this.touch) { @@ -894,7 +894,7 @@ const controls = {      // Set options if passed and filter based on uniqueness and config      if (is.array(options)) { -      this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality)); +      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));      }      // Toggle the pane and tab @@ -913,7 +913,7 @@ const controls = {      }      // Get the badge HTML for HD, 4K etc -    const getBadge = quality => { +    const getBadge = (quality) => {        const label = i18n.get(`qualityBadge.${quality}`, this.config);        if (!label.length) { @@ -929,7 +929,7 @@ const controls = {          const sorting = this.config.quality.options;          return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;        }) -      .forEach(quality => { +      .forEach((quality) => {          controls.createMenuItem.call(this, {            value: quality,            list, @@ -1052,7 +1052,7 @@ const controls = {      const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');      // Filter out invalid speeds -    this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed); +    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);      // Toggle the pane and tab      const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; @@ -1070,7 +1070,7 @@ const controls = {      }      // Create items -    this.options.speed.forEach(speed => { +    this.options.speed.forEach((speed) => {        controls.createMenuItem.call(this, {          value: speed,          list, @@ -1085,7 +1085,7 @@ const controls = {    // Check if we need to hide/show the settings menu    checkMenu() {      const { buttons } = this.elements.settings; -    const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); +    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);      toggleHidden(this.elements.settings.menu, !visible);    }, @@ -1099,7 +1099,7 @@ const controls = {      let target = pane;      if (!is.element(target)) { -      target = Object.values(this.elements.settings.panels).find(p => !p.hidden); +      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);      }      const firstItem = target.querySelector('[role^="menuitem"]'); @@ -1191,7 +1191,7 @@ const controls = {      // Hide all other panels      const container = target.parentNode; -    const current = Array.from(container.children).find(node => !node.hidden); +    const current = Array.from(container.children).find((node) => !node.hidden);      // If we can do fancy animations, we'll animate the height/width      if (support.transitions && !support.reducedMotion) { @@ -1203,7 +1203,7 @@ const controls = {        const size = controls.getMenuSize.call(this, target);        // Restore auto height/width -      const restore = event => { +      const restore = (event) => {          // We're only bothered about height and width on the container          if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {            return; @@ -1275,7 +1275,7 @@ const controls = {      const defaultAttributes = { class: 'plyr__controls__item' };      // Loop through controls in order -    dedupe(is.array(this.config.controls) ? this.config.controls: []).forEach(control => { +    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {        // Restart button        if (control === 'restart') {          container.appendChild(createButton.call(this, 'restart', defaultAttributes)); @@ -1437,7 +1437,7 @@ const controls = {          this.elements.settings.panels.home = home;          // Build the menu items -        this.config.settings.forEach(type => { +        this.config.settings.forEach((type) => {            // TODO: bundle this with the createMenuItem helper and bindings            const menuItem = createElement(              'button', @@ -1510,7 +1510,7 @@ const controls = {              this,              pane,              'keydown', -            event => { +            (event) => {                // We only care about <-                if (event.which !== 37) {                  return; @@ -1661,7 +1661,7 @@ const controls = {      }      // Replace props with their value -    const replace = input => { +    const replace = (input) => {        let result = input;        Object.entries(props).forEach(([key, value]) => { @@ -1702,7 +1702,7 @@ const controls = {      // Add pressed property to buttons      if (!is.empty(this.elements.buttons)) { -      const addProperty = button => { +      const addProperty = (button) => {          const className = this.config.classNames.controlPressed;          Object.defineProperty(button, 'pressed', {            enumerable: true, @@ -1718,11 +1718,9 @@ const controls = {        // Toggle classname when pressed property is set        Object.values(this.elements.buttons)          .filter(Boolean) -        .forEach(button => { +        .forEach((button) => {            if (is.array(button) || is.nodeList(button)) { -            Array.from(button) -              .filter(Boolean) -              .forEach(addProperty); +            Array.from(button).filter(Boolean).forEach(addProperty);            } else {              addProperty(button);            } @@ -1740,7 +1738,7 @@ const controls = {        const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;        const labels = getElements.call(this, selector); -      Array.from(labels).forEach(label => { +      Array.from(labels).forEach((label) => {          toggleClass(label, this.config.classNames.hidden, false);          toggleClass(label, this.config.classNames.tooltip, true);        }); diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 5029e7de..7bb22391 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -5,7 +5,7 @@  // ==========================================================================  import browser from './utils/browser'; -import { closest,getElements, hasClass, toggleClass } from './utils/elements'; +import { closest, getElements, hasClass, toggleClass } from './utils/elements';  import { on, triggerEvent } from './utils/events';  import is from './utils/is';  import { silencePromise } from './utils/promise'; @@ -43,17 +43,17 @@ class Fullscreen {      );      // Fullscreen toggle on double click -    on.call(this.player, this.player.elements.container, 'dblclick', event => { +    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {        // Ignore double click in controls        if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {          return;        } -      this.toggle(); +      this.player.listeners.proxy(event, this.toggle, 'fullscreen');      });      // Tap focus when in fullscreen -    on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event)); +    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));      // Update the UI      this.update(); @@ -85,7 +85,7 @@ class Fullscreen {      let value = '';      const prefixes = ['webkit', 'moz', 'ms']; -    prefixes.some(pre => { +    prefixes.some((pre) => {        if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {          value = pre;          return true; @@ -145,8 +145,10 @@ class Fullscreen {        button.pressed = this.active;      } +    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up +    const target = this.target === this.player.media ? this.target : this.player.elements.container;      // Trigger an event -    triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); +    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);    }    toggleFallback(toggle = false) { @@ -189,7 +191,7 @@ class Fullscreen {        } else if (this.cleanupViewport) {          viewport.content = viewport.content            .split(',') -          .filter(part => part.trim() !== property) +          .filter((part) => part.trim() !== property)            .join(',');        }      } diff --git a/src/js/html5.js b/src/js/html5.js index 658abf15..0cb5b27a 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -18,7 +18,7 @@ const html5 = {      const sources = Array.from(this.media.querySelectorAll('source'));      // Filter out unsupported sources (if type is specified) -    return sources.filter(source => { +    return sources.filter((source) => {        const type = source.getAttribute('type');        if (is.empty(type)) { @@ -39,7 +39,7 @@ const html5 = {      // Get sizes from <source> elements      return html5.getSources        .call(this) -      .map(source => Number(source.getAttribute('size'))) +      .map((source) => Number(source.getAttribute('size')))        .filter(Boolean);    }, @@ -63,7 +63,7 @@ const html5 = {        get() {          // Get sources          const sources = html5.getSources.call(player); -        const source = sources.find(s => s.getAttribute('src') === player.source); +        const source = sources.find((s) => s.getAttribute('src') === player.source);          // Return size, if match is found          return source && Number(source.getAttribute('size')); @@ -80,7 +80,7 @@ const html5 = {            // Get sources            const sources = html5.getSources.call(player);            // Get first match for requested size -          const source = sources.find(s => Number(s.getAttribute('size')) === input); +          const source = sources.find((s) => Number(s.getAttribute('size')) === input);            // No matching source found            if (!source) { diff --git a/src/js/listeners.js b/src/js/listeners.js index 2cc71537..48734bcf 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -277,7 +277,7 @@ class Listeners {        player,        elements.container,        'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', -      event => { +      (event) => {          const { controls: controlsElement } = elements;          // Remove button states for fullscreen @@ -319,7 +319,7 @@ class Listeners {      };      // Resize on fullscreen change -    const setPlayerSize = measure => { +    const setPlayerSize = (measure) => {        // If we don't need to measure the viewport        if (!measure) {          return setAspectRatio.call(player); @@ -336,7 +336,7 @@ class Listeners {        timers.resized = setTimeout(setPlayerSize, 50);      }; -    on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => { +    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {        const { target, usingNative } = player.fullscreen;        // Ignore events not from target @@ -356,6 +356,11 @@ class Listeners {        // Set Vimeo gutter        setGutter(ratio, padding, isEnter); +      // Horrible hack for Safari 14 not repainting properly on entering fullscreen +      if (isEnter) { +        setTimeout(() => repaint(elements.container), 100); +      } +        // If not using native browser fullscreen API, we need to check for resizes of viewport        if (!usingNative) {          if (isEnter) { @@ -373,10 +378,10 @@ class Listeners {      const { elements } = player;      // Time change on media -    on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event)); +    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));      // Display duration -    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => +    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>        controls.durationUpdate.call(player, event),      ); @@ -393,20 +398,20 @@ class Listeners {      });      // Check for buffer progress -    on.call(player, player.media, 'progress playing seeking seeked', event => +    on.call(player, player.media, 'progress playing seeking seeked', (event) =>        controls.updateProgress.call(player, event),      );      // Handle volume changes -    on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event)); +    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));      // Handle play/pause -    on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => +    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>        ui.checkPlaying.call(player, event),      );      // Loading state -    on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event)); +    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));      // Click video      if (player.supported.ui && player.config.clickToPlay && !player.isAudio) { @@ -419,7 +424,7 @@ class Listeners {        }        // On click play, pause or restart -      on.call(player, elements.container, 'click', event => { +      on.call(player, elements.container, 'click', (event) => {          const targets = [elements.container, wrapper];          // Ignore if click if not container or in video wrapper @@ -459,7 +464,7 @@ class Listeners {          player,          elements.wrapper,          'contextmenu', -        event => { +        (event) => {            event.preventDefault();          },          false, @@ -485,7 +490,7 @@ class Listeners {      });      // Quality change -    on.call(player, player.media, 'qualitychange', event => { +    on.call(player, player.media, 'qualitychange', (event) => {        // Update UI        controls.updateSetting.call(player, 'quality', null, event.detail.quality);      }); @@ -499,7 +504,7 @@ class Listeners {      // Bubble up key events for Edge      const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); -    on.call(player, player.media, proxyEvents, event => { +    on.call(player, player.media, proxyEvents, (event) => {        let { detail = {} } = event;        // Get error details from media @@ -539,7 +544,7 @@ class Listeners {        player,        element,        type, -      event => this.proxy(event, defaultHandler, customHandlerKey), +      (event) => this.proxy(event, defaultHandler, customHandlerKey),        passive && !hasCustomHandler,      );    } @@ -553,7 +558,7 @@ class Listeners {      // Play/pause toggle      if (elements.buttons.play) { -      Array.from(elements.buttons.play).forEach(button => { +      Array.from(elements.buttons.play).forEach((button) => {          this.bind(            button,            'click', @@ -569,10 +574,28 @@ class Listeners {      this.bind(elements.buttons.restart, 'click', player.restart, 'restart');      // Rewind -    this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); +    this.bind( +      elements.buttons.rewind, +      'click', +      () => { +        // Record seek time so we can prevent hiding controls for a few seconds after rewind +        player.lastSeekTime = Date.now(); +        player.rewind(); +      }, +      'rewind', +    );      // Rewind -    this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); +    this.bind( +      elements.buttons.fastForward, +      'click', +      () => { +        // Record seek time so we can prevent hiding controls for a few seconds after fast forward +        player.lastSeekTime = Date.now(); +        player.forward(); +      }, +      'fastForward', +    );      // Mute toggle      this.bind( @@ -624,7 +647,7 @@ class Listeners {      this.bind(        elements.buttons.settings,        'click', -      event => { +      (event) => {          // Prevent the document click listener closing the menu          event.stopPropagation();          event.preventDefault(); @@ -641,7 +664,7 @@ class Listeners {      this.bind(        elements.buttons.settings,        'keyup', -      event => { +      (event) => {          const code = event.which;          // We only care about space and return @@ -669,21 +692,21 @@ class Listeners {      );      // Escape closes menu -    this.bind(elements.settings.menu, 'keydown', event => { +    this.bind(elements.settings.menu, 'keydown', (event) => {        if (event.which === 27) {          controls.toggleMenu.call(player, event);        }      });      // Set range input alternative "value", which matches the tooltip time (#954) -    this.bind(elements.inputs.seek, 'mousedown mousemove', event => { +    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {        const rect = elements.progress.getBoundingClientRect();        const percent = (100 / rect.width) * (event.pageX - rect.left);        event.currentTarget.setAttribute('seek-value', percent);      });      // Pause while seeking -    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { +    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {        const seek = event.currentTarget;        const code = event.keyCode ? event.keyCode : event.which;        const attribute = 'play-on-seeked'; @@ -715,14 +738,14 @@ class Listeners {      // it takes over further interactions on the page. This is a hack      if (browser.isIos) {        const inputs = getElements.call(player, 'input[type="range"]'); -      Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target))); +      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));      }      // Seek      this.bind(        elements.inputs.seek,        inputEvent, -      event => { +      (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'); @@ -739,13 +762,13 @@ class Listeners {      );      // Seek tooltip -    this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => +    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>        controls.updateSeekTooltip.call(player, event),      );      // Preview thumbnails plugin      // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this -    this.bind(elements.progress, 'mousemove touchmove', event => { +    this.bind(elements.progress, 'mousemove touchmove', (event) => {        const { previewThumbnails } = player;        if (previewThumbnails && previewThumbnails.loaded) { @@ -763,7 +786,7 @@ class Listeners {      });      // Show scrubbing preview -    this.bind(elements.progress, 'mousedown touchstart', event => { +    this.bind(elements.progress, 'mousedown touchstart', (event) => {        const { previewThumbnails } = player;        if (previewThumbnails && previewThumbnails.loaded) { @@ -771,7 +794,7 @@ class Listeners {        }      }); -    this.bind(elements.progress, 'mouseup touchend', event => { +    this.bind(elements.progress, 'mouseup touchend', (event) => {        const { previewThumbnails } = player;        if (previewThumbnails && previewThumbnails.loaded) { @@ -781,8 +804,8 @@ class Listeners {      // Polyfill for lower fill in <input type="range"> for webkit      if (browser.isWebkit) { -      Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => { -        this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target)); +      Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => { +        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));        });      } @@ -805,30 +828,30 @@ class Listeners {      this.bind(        elements.inputs.volume,        inputEvent, -      event => { +      (event) => {          player.volume = event.target.value;        },        'volume',      );      // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) -    this.bind(elements.controls, 'mouseenter mouseleave', event => { +    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {        elements.controls.hover = !player.touch && event.type === 'mouseenter';      });      // Also update controls.hover state for any non-player children of fullscreen element (as above)      if (elements.fullscreen) {        Array.from(elements.fullscreen.children) -        .filter(c => !c.contains(elements.container)) -        .forEach(child => { -          this.bind(child, 'mouseenter mouseleave', event => { +        .filter((c) => !c.contains(elements.container)) +        .forEach((child) => { +          this.bind(child, 'mouseenter mouseleave', (event) => {              elements.controls.hover = !player.touch && event.type === 'mouseenter';            });          });      }      // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) -    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { +    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {        elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);      }); @@ -861,12 +884,12 @@ class Listeners {      this.bind(        elements.inputs.volume,        'wheel', -      event => { +      (event) => {          // 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)); +        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/media.js b/src/js/media.js index 4584fea3..ddac5ebf 100644 --- a/src/js/media.js +++ b/src/js/media.js @@ -41,6 +41,7 @@ const media = {        // Poster image container        this.elements.poster = createElement('div', {          class: this.config.classNames.poster, +        hidden: '',        });        this.elements.wrapper.appendChild(this.elements.poster); diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 1a52ebce..12b5cc31 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -15,7 +15,7 @@ import { silencePromise } from '../utils/promise';  import { formatTime } from '../utils/time';  import { buildUrlParams } from '../utils/urls'; -const destroy = instance => { +const destroy = (instance) => {    // Destroy our adsManager    if (instance.manager) {      instance.manager.destroy(); @@ -179,10 +179,10 @@ class Ads {      // Listen and respond to ads loaded and error events      this.loader.addEventListener(        google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, -      event => this.onAdsManagerLoaded(event), +      (event) => this.onAdsManagerLoaded(event),        false,      ); -    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); +    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);      // Request video ads to be pre-loaded      this.requestAds(); @@ -264,11 +264,11 @@ class Ads {      // Add listeners to the required events      // Advertisement error events -    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); +    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));      // Advertisement regular events -    Object.keys(google.ima.AdEvent.Type).forEach(type => { -      this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e)); +    Object.keys(google.ima.AdEvent.Type).forEach((type) => { +      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));      });      // Resolve our adsManager @@ -278,7 +278,7 @@ class Ads {    addCuePoints() {      // Add advertisement cue's within the time line if available      if (!is.empty(this.cuePoints)) { -      this.cuePoints.forEach(cuePoint => { +      this.cuePoints.forEach((cuePoint) => {          if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {            const seekElement = this.player.elements.progress; @@ -310,7 +310,7 @@ class Ads {      const adData = event.getAdData();      // Proxy event -    const dispatchEvent = type => { +    const dispatchEvent = (type) => {        triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);      }; @@ -565,7 +565,7 @@ class Ads {          }          // Re-set our adsManager promises -        this.managerPromise = new Promise(resolve => { +        this.managerPromise = new Promise((resolve) => {            this.on('loaded', resolve);            this.player.debug.log(this.manager);          }); @@ -586,7 +586,7 @@ class Ads {      const handlers = this.events[event];      if (is.array(handlers)) { -      handlers.forEach(handler => { +      handlers.forEach((handler) => {          if (is.function(handler)) {            handler.apply(this, args);          } diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index 6ce53f28..16167247 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -5,15 +5,15 @@ 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 parseVtt = (vttDataString) => {    const processedList = [];    const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/); -  frames.forEach(frame => { +  frames.forEach((frame) => {      const result = {};      const lines = frame.split(/\r\n|\n|\r/); -    lines.forEach(line => { +    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( @@ -130,7 +130,7 @@ class PreviewThumbnails {    // Download VTT files and parse them    getThumbnails() { -    return new Promise(resolve => { +    return new Promise((resolve) => {        const { src } = this.player.config.previewThumbnails;        if (is.empty(src)) { @@ -149,7 +149,7 @@ class PreviewThumbnails {        // Via callback()        if (is.function(src)) { -        src(thumbnails => { +        src((thumbnails) => {            this.thumbnails = thumbnails;            sortAndResolve();          }); @@ -159,7 +159,7 @@ 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)); +        const promises = urls.map((u) => this.getThumbnail(u));          // Resolve          Promise.all(promises).then(sortAndResolve);        } @@ -168,8 +168,8 @@ class PreviewThumbnails {    // Process individual VTT file    getThumbnail(url) { -    return new Promise(resolve => { -      fetch(url).then(response => { +    return new Promise((resolve) => { +      fetch(url).then((response) => {          const thumbnail = {            frames: parseVtt(response),            height: null, @@ -360,7 +360,7 @@ class PreviewThumbnails {      // 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, +      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,      );      const hasThumb = thumbNum >= 0;      let qualityIndex = 0; @@ -454,7 +454,7 @@ class PreviewThumbnails {    // 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 => { +    Array.from(this.currentImageContainer.children).forEach((image) => {        if (image.tagName.toLowerCase() !== 'img') {          return;        } @@ -481,7 +481,7 @@ class PreviewThumbnails {    // 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 => { +    return new Promise((resolve) => {        setTimeout(() => {          const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text; @@ -496,7 +496,7 @@ class PreviewThumbnails {            let foundOne = false; -          thumbnailsClone.forEach(frame => { +          thumbnailsClone.forEach((frame) => {              const newThumbFilename = frame.text;              if (newThumbFilename !== oldThumbFilename) { diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 33c327d7..b050cc53 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -58,7 +58,7 @@ const vimeo = {          .then(() => {            vimeo.ready.call(player);          }) -        .catch(error => { +        .catch((error) => {            player.debug.warn('Vimeo SDK (player.js) failed to load', error);          });      } else { @@ -112,25 +112,29 @@ const vimeo = {      }      // Inject the package -    const { poster } = player; -    if (premium) { -      iframe.setAttribute('data-poster', poster); +    if (premium || !config.customControls) { +      iframe.setAttribute('data-poster', player.poster);        player.media = replaceElement(iframe, player.media);      } else { -      const wrapper = createElement('div', { class: player.config.classNames.embedContainer, 'data-poster': poster }); +      const wrapper = createElement('div', { +        class: player.config.classNames.embedContainer, +        'data-poster': player.poster, +      });        wrapper.appendChild(iframe);        player.media = replaceElement(wrapper, player.media);      } -     +      // Get poster image -    fetch(format(player.config.urls.vimeo.api, src)).then(response => { -      if (is.empty(response) || !response.thumbnail_url) { -        return; -      } -       -      // Set and show poster -      ui.setPoster.call(player, response.thumbnail_url).catch(() => { }); -    }); +    if (!config.customControls) { +      fetch(format(player.config.urls.vimeo.api, src)).then((response) => { +        if (is.empty(response) || !response.thumbnail_url) { +          return; +        } + +        // Set and show poster +        ui.setPoster.call(player, response.thumbnail_url).catch(() => {}); +      }); +    }      // Setup instance      // https://github.com/vimeo/player.js @@ -263,11 +267,11 @@ const vimeo = {      let currentSrc;      player.embed        .getVideoUrl() -      .then(value => { +      .then((value) => {          currentSrc = value;          controls.setDownloadUrl.call(player);        }) -      .catch(error => { +      .catch((error) => {          this.debug.warn(error);        }); @@ -285,49 +289,49 @@ const vimeo = {      });      // Set aspect ratio based on video size -    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { +    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {        const [width, height] = dimensions;        player.embed.ratio = [width, height];        setAspectRatio.call(this);      });      // Set autopause -    player.embed.setAutopause(player.config.autopause).then(state => { +    player.embed.setAutopause(player.config.autopause).then((state) => {        player.config.autopause = state;      });      // Get title -    player.embed.getVideoTitle().then(title => { +    player.embed.getVideoTitle().then((title) => {        player.config.title = title;        ui.setTitle.call(this);      });      // Get current time -    player.embed.getCurrentTime().then(value => { +    player.embed.getCurrentTime().then((value) => {        currentTime = value;        triggerEvent.call(player, player.media, 'timeupdate');      });      // Get duration -    player.embed.getDuration().then(value => { +    player.embed.getDuration().then((value) => {        player.media.duration = value;        triggerEvent.call(player, player.media, 'durationchange');      });      // Get captions -    player.embed.getTextTracks().then(tracks => { +    player.embed.getTextTracks().then((tracks) => {        player.media.textTracks = tracks;        captions.setup.call(player);      });      player.embed.on('cuechange', ({ cues = [] }) => { -      const strippedCues = cues.map(cue => stripHTML(cue.text)); +      const strippedCues = cues.map((cue) => stripHTML(cue.text));        captions.updateCues.call(player, strippedCues);      });      player.embed.on('loaded', () => {        // Assure state and events are updated on autoplay -      player.embed.getPaused().then(paused => { +      player.embed.getPaused().then((paused) => {          assurePlaybackState.call(player, !paused);          if (!paused) {            triggerEvent.call(player, player.media, 'playing'); @@ -360,13 +364,13 @@ const vimeo = {        assurePlaybackState.call(player, false);      }); -    player.embed.on('timeupdate', data => { +    player.embed.on('timeupdate', (data) => {        player.media.seeking = false;        currentTime = data.seconds;        triggerEvent.call(player, player.media, 'timeupdate');      }); -    player.embed.on('progress', data => { +    player.embed.on('progress', (data) => {        player.media.buffered = data.percent;        triggerEvent.call(player, player.media, 'progress'); @@ -377,7 +381,7 @@ const vimeo = {        // Get duration as if we do it before load, it gives an incorrect value        // https://github.com/sampotts/plyr/issues/891 -      player.embed.getDuration().then(value => { +      player.embed.getDuration().then((value) => {          if (value !== player.media.duration) {            player.media.duration = value;            triggerEvent.call(player, player.media, 'durationchange'); @@ -395,13 +399,15 @@ const vimeo = {        triggerEvent.call(player, player.media, 'ended');      }); -    player.embed.on('error', detail => { +    player.embed.on('error', (detail) => {        player.media.error = detail;        triggerEvent.call(player, player.media, 'error');      });      // Rebuild UI -    setTimeout(() => ui.build.call(player), 0); +    if (config.customControls) { +      setTimeout(() => ui.build.call(player), 0); +    }    },  }; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 89a75d89..db5781e6 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -70,7 +70,7 @@ const youtube = {        };        // Load the SDK -      loadScript(this.config.urls.youtube.sdk).catch(error => { +      loadScript(this.config.urls.youtube.sdk).catch((error) => {          this.debug.warn('YouTube API failed to load', error);        });      } @@ -81,7 +81,7 @@ const youtube = {      const url = format(this.config.urls.youtube.api, videoId);      fetch(url) -      .then(data => { +      .then((data) => {          if (is.object(data)) {            const { title, height, width } = data; @@ -104,6 +104,7 @@ const youtube = {    // API ready    ready() {      const player = this; +    const config = player.config.youtube;      // Ignore already setup (race condition)      const currentId = player.media && player.media.getAttribute('id');      if (!is.empty(currentId) && currentId.startsWith('youtube-')) { @@ -121,43 +122,46 @@ const youtube = {      // Replace the <iframe> with a <div> 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, 'data-poster': poster }); +    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });      player.media = replaceElement(container, player.media); -    // Id to poster wrapper -    const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; - -    // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) -    loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded -      .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 -      .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists -      .then(image => ui.setPoster.call(player, image.src)) -      .then(src => { -        // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) -        if (!src.includes('maxres')) { -          player.elements.poster.style.backgroundSize = 'cover'; -        } -      }) -      .catch(() => {}); - -    const config = player.config.youtube; +    // Only load the poster when using custom controls +    if (config.customControls) { +      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; + +      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) +      loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded +        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 +        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists +        .then((image) => ui.setPoster.call(player, image.src)) +        .then((src) => { +          // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) +          if (!src.includes('maxres')) { +            player.elements.poster.style.backgroundSize = 'cover'; +          } +        }) +        .catch(() => {}); +    }      // Setup instance      // https://developers.google.com/youtube/iframe_api_reference -    player.embed = new window.YT.Player(id, { +    player.embed = new window.YT.Player(player.media, {        videoId,        host: getHost(config),        playerVars: extend(          {},          { -          autoplay: player.config.autoplay ? 1 : 0, // Autoplay -          hl: player.config.hl, // iframe interface language -          controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported -          disablekb: 1, // Disable keyboard as we handle it -          playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback +          // Autoplay +          autoplay: player.config.autoplay ? 1 : 0, +          // iframe interface language +          hl: player.config.hl, +          // Only show controls if not fully supported or opted out +          controls: player.supported.ui && config.customControls ? 0 : 1, +          // Disable keyboard as we handle it +          disablekb: 1, +          // Allow iOS inline playback +          playsinline: !player.config.fullscreen.iosNative ? 1 : 0,            // Captions are flaky on YouTube            cc_load_policy: player.captions.active ? 1 : 0,            cc_lang_pref: player.config.captions.language, @@ -278,6 +282,7 @@ const youtube = {                const toggle = is.boolean(input) ? input : muted;                muted = toggle;                instance[toggle ? 'mute' : 'unMute'](); +              instance.setVolume(volume * 100);                triggerEvent.call(player, player.media, 'volumechange');              },            }); @@ -299,10 +304,10 @@ const youtube = {            // Get available speeds            const speeds = instance.getAvailablePlaybackRates();            // Filter based on config -          player.options.speed = speeds.filter(s => player.config.speed.options.includes(s)); +          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));            // Set the tabindex to avoid focus entering iframe -          if (player.supported.ui) { +          if (player.supported.ui && config.customControls) {              player.media.setAttribute('tabindex', -1);            } @@ -335,7 +340,9 @@ const youtube = {            }, 200);            // Rebuild UI -          setTimeout(() => ui.build.call(player), 50); +          if (config.customControls) { +            setTimeout(() => ui.build.call(player), 50); +          }          },          onStateChange(event) {            // Get the instance @@ -386,7 +393,7 @@ const youtube = {              case 1:                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) -              if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) { +              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {                  player.media.pause();                } else {                  assurePlaybackState.call(player, true); diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts index 13523b35..4b332aeb 100644 --- a/src/js/plyr.d.ts +++ b/src/js/plyr.d.ts @@ -214,26 +214,17 @@ declare class Plyr {    /**     * Add an event listener for the specified event.     */ -  on( -    event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, -    callback: (this: this, event: Plyr.PlyrEvent) => void, -  ): void; +  on<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => 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; +  once<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => 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; +  off<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;    /**     * Check support for a mime type. @@ -249,37 +240,51 @@ declare class Plyr {  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'; +  type StandardEventMap = { +    progress: PlyrEvent; +    playing: PlyrEvent; +    play: PlyrEvent; +    pause: PlyrEvent; +    timeupdate: PlyrEvent; +    volumechange: PlyrEvent; +    seeking: PlyrEvent; +    seeked: PlyrEvent; +    ratechange: PlyrEvent; +    ended: PlyrEvent; +    enterfullscreen: PlyrEvent; +    exitfullscreen: PlyrEvent; +    captionsenabled: PlyrEvent; +    captionsdisabled: PlyrEvent; +    languagechange: PlyrEvent; +    controlshidden: PlyrEvent; +    controlsshown: PlyrEvent; +    ready: PlyrEvent; +  }; +  // For retrocompatibility, we keep StandadEvent +  type StandadEvent = keyof Plyr.StandardEventMap; +  type Html5EventMap = { +    loadstart: PlyrEvent; +    loadeddata: PlyrEvent; +    loadedmetadata: PlyrEvent; +    canplay: PlyrEvent; +    canplaythrough: PlyrEvent; +    stalled: PlyrEvent; +    waiting: PlyrEvent; +    emptied: PlyrEvent; +    cuechange: PlyrEvent; +    error: PlyrEvent; +  }; +  // For retrocompatibility, we keep Html5Event +  type Html5Event = keyof Plyr.Html5EventMap; +  type YoutubeEventMap = { +    statechange: PlyrStateChangeEvent; +    qualitychange: PlyrEvent; +    qualityrequested: PlyrEvent; +  }; +  // For retrocompatibility, we keep YoutubeEvent +  type YoutubeEvent = keyof Plyr.YoutubeEventMap; + +  type PlyrEventMap = StandardEventMap & Html5EventMap & YoutubeEventMap;    interface FullscreenControl {      /** @@ -552,7 +557,7 @@ declare namespace Plyr {    interface PreviewThumbnailsOptions {      enabled?: boolean; -    src?: string; +    src?: string | string[];    }    interface SourceInfo { @@ -623,6 +628,22 @@ declare namespace Plyr {      readonly detail: { readonly plyr: Plyr };    } +  enum YoutubeState { +    UNSTARTED = -1, +    ENDED = 0, +    PLAYING = 1, +    PAUSED = 2, +    BUFFERING = 3, +    CUED = 5, +  } + +  interface PlyrStateChangeEvent extends CustomEvent { +    readonly detail: { +      readonly plyr: Plyr; +      readonly code: YoutubeState; +    }; +  } +    interface Support {      api: boolean;      ui: boolean; diff --git a/src/js/plyr.js b/src/js/plyr.js index ff92d95e..f1dcc68a 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -12,6 +12,7 @@ import { getProviderByUrl, providers, types } from './config/types';  import Console from './console';  import controls from './controls';  import Fullscreen from './fullscreen'; +import html5 from './html5';  import Listeners from './listeners';  import media from './media';  import Ads from './plugins/ads'; @@ -281,7 +282,7 @@ class Plyr {      // Listen for events if debugging      if (this.config.debug) { -      on.call(this, this.elements.container, this.config.events.join(' '), event => { +      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {          this.debug.log(`event: ${event.type}`);        });      } @@ -308,7 +309,7 @@ class Plyr {      // Autoplay if required      if (this.isHTML5 && this.config.autoplay) { -      setTimeout(() => silencePromise(this.play()), 10); +      this.once('canplay', () => silencePromise(this.play()));      }      // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek @@ -1054,7 +1055,12 @@ class Plyr {        const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);        // Close menu -      if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { +      if ( +        hiding && +        is.array(this.config.controls) && +        this.config.controls.includes('settings') && +        !is.empty(this.config.settings) +      ) {          controls.toggleMenu.call(this, false);        } @@ -1140,6 +1146,9 @@ class Plyr {          // Unbind listeners          unbindListeners.call(this); +        // Cancel current network requests +        html5.cancelRequests.call(this); +          // Replace the container with the original element provided          replaceElement(this.elements.original, this.elements.container); @@ -1248,7 +1257,7 @@ class Plyr {        return null;      } -    return targets.map(t => new Plyr(t, options)); +    return targets.map((t) => new Plyr(t, options));    }  } diff --git a/src/js/source.js b/src/js/source.js index b9fc7732..a62edbba 100644 --- a/src/js/source.js +++ b/src/js/source.js @@ -20,7 +20,7 @@ const source = {          src: attributes,        });      } else if (is.array(attributes)) { -      attributes.forEach(attribute => { +      attributes.forEach((attribute) => {          insertElement(type, this.media, attribute);        });      } diff --git a/src/js/ui.js b/src/js/ui.js index d3d6fd69..c8b19677 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -135,7 +135,7 @@ const ui = {      }      // If there's a play button, set label -    Array.from(this.elements.buttons.play || []).forEach(button => { +    Array.from(this.elements.buttons.play || []).forEach((button) => {        button.setAttribute('aria-label', label);      }); @@ -172,13 +172,16 @@ const ui = {      // Set property synchronously to respect the call order      this.media.setAttribute('data-poster', poster); +    // Show the poster +    this.elements.poster.removeAttribute('hidden'); +      // Wait until ui is ready      return (        ready          .call(this)          // Load image          .then(() => loadImage(poster)) -        .catch(err => { +        .catch((err) => {            // Hide poster on error unless it's been set by another call            if (poster === this.poster) {              ui.togglePoster.call(this, false); @@ -214,7 +217,7 @@ const ui = {      toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);      // Set state -    Array.from(this.elements.buttons.play || []).forEach(target => { +    Array.from(this.elements.buttons.play || []).forEach((target) => {        Object.assign(target, { pressed: this.playing });        target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));      }); @@ -270,8 +273,8 @@ const ui = {      // Loop through values (as they are the keys when the object is spread 🤔)      Object.values({ ...this.media.style })        // We're only fussed about Plyr specific properties -      .filter(key => !is.empty(key) && key.startsWith('--plyr')) -      .forEach(key => { +      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr')) +      .forEach((key) => {          // Set on the container          this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key)); diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index d9e7615e..b4ccf268 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -14,7 +14,7 @@ export const transitionEndEvent = (() => {      transition: 'transitionend',    }; -  const type = Object.keys(events).find(event => element.style[event] !== undefined); +  const type = Object.keys(events).find((event) => element.style[event] !== undefined);    return is.string(type) ? events[type] : false;  })(); diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 1d13b701..e8d2e595 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -138,7 +138,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {    const attributes = {};    const existing = extend({}, existingAttributes); -  sel.split(',').forEach(s => { +  sel.split(',').forEach((s) => {      // Remove whitespace      const selector = s.trim();      const className = selector.replace('.', ''); @@ -198,7 +198,7 @@ export function toggleHidden(element, hidden) {  // Mirror Element.classList.toggle, with IE compatibility for "force" argument  export function toggleClass(element, className, force) {    if (is.nodeList(element)) { -    return Array.from(element).map(e => toggleClass(e, className, force)); +    return Array.from(element).map((e) => toggleClass(e, className, force));    }    if (is.element(element)) { diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 235eb629..287129f1 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -50,7 +50,7 @@ export function toggleListener(element, event, callback, toggle = false, passive    }    // If a single node is passed, bind the event listener -  events.forEach(type => { +  events.forEach((type) => {      if (this && this.eventListeners && toggle) {        // Cache event listener        this.eventListeners.push({ element, type, callback, options }); @@ -100,7 +100,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) {  // Unbind all cached event listeners  export function unbindListeners() {    if (this && this.eventListeners) { -    this.eventListeners.forEach(item => { +    this.eventListeners.forEach((item) => {        const { element, type, callback, options } = item;        element.removeEventListener(type, callback, options);      }); @@ -111,7 +111,7 @@ export function unbindListeners() {  // Run method when / if player is ready  export function ready() { -  return new Promise(resolve => +  return new Promise((resolve) =>      this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),    ).then(() => {});  } diff --git a/src/js/utils/is.js b/src/js/utils/is.js index 1cc33848..3bb50a00 100644 --- a/src/js/utils/is.js +++ b/src/js/utils/is.js @@ -2,31 +2,31 @@  // Type checking utils  // ========================================================================== -const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null); +const getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);  const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); -const isNullOrUndefined = input => input === null || typeof input === 'undefined'; -const isObject = input => getConstructor(input) === Object; -const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input); -const isString = input => getConstructor(input) === String; -const isBoolean = input => getConstructor(input) === Boolean; -const isFunction = input => getConstructor(input) === Function; -const isArray = input => Array.isArray(input); -const isWeakMap = input => instanceOf(input, WeakMap); -const isNodeList = input => instanceOf(input, NodeList); -const isElement = input => instanceOf(input, Element); -const isTextNode = input => getConstructor(input) === Text; -const isEvent = input => instanceOf(input, Event); -const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); -const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); -const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); -const isPromise = input => instanceOf(input, Promise) && isFunction(input.then); +const isNullOrUndefined = (input) => input === null || typeof input === 'undefined'; +const isObject = (input) => getConstructor(input) === Object; +const isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input); +const isString = (input) => getConstructor(input) === String; +const isBoolean = (input) => getConstructor(input) === Boolean; +const isFunction = (input) => getConstructor(input) === Function; +const isArray = (input) => Array.isArray(input); +const isWeakMap = (input) => instanceOf(input, WeakMap); +const isNodeList = (input) => instanceOf(input, NodeList); +const isElement = (input) => instanceOf(input, Element); +const isTextNode = (input) => getConstructor(input) === Text; +const isEvent = (input) => instanceOf(input, Event); +const isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent); +const isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); +const isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); +const isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then); -const isEmpty = input => +const isEmpty = (input) =>    isNullOrUndefined(input) ||    ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||    (isObject(input) && !Object.keys(input).length); -const isUrl = input => { +const isUrl = (input) => {    // Accept a URL object    if (instanceOf(input, window.URL)) {      return true; diff --git a/src/js/utils/load-sprite.js b/src/js/utils/load-sprite.js index 0a4eff99..293163e5 100644 --- a/src/js/utils/load-sprite.js +++ b/src/js/utils/load-sprite.js @@ -54,7 +54,7 @@ export default function loadSprite(url, id) {      // Get the sprite      fetch(url) -      .then(result => { +      .then((result) => {          if (is.empty(result)) {            return;          } diff --git a/src/js/utils/objects.js b/src/js/utils/objects.js index a327e488..d64002ae 100644 --- a/src/js/utils/objects.js +++ b/src/js/utils/objects.js @@ -26,7 +26,7 @@ export function extend(target = {}, ...sources) {      return target;    } -  Object.keys(source).forEach(key => { +  Object.keys(source).forEach((key) => {      if (is.object(source[key])) {        if (!Object.keys(target).includes(key)) {          Object.assign(target, { [key]: {} }); diff --git a/src/js/utils/strings.js b/src/js/utils/strings.js index b7de04c1..d4cc1efa 100644 --- a/src/js/utils/strings.js +++ b/src/js/utils/strings.js @@ -33,7 +33,7 @@ export const replaceAll = (input = '', find = '', replace = '') =>  // Convert to title case  export const toTitleCase = (input = '') => -  input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()); +  input.toString().replace(/\w\S*/g, (text) => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());  // Convert string to pascalCase  export function toPascalCase(input = '') { diff --git a/src/js/utils/style.js b/src/js/utils/style.js index c2004fcb..f02b0ba5 100644 --- a/src/js/utils/style.js +++ b/src/js/utils/style.js @@ -27,7 +27,7 @@ export function reduceAspectRatio(ratio) {  }  export function getAspectRatio(input) { -  const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null); +  const parse = (ratio) => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);    // Try provided ratio    let ratio = parse(input); @@ -68,7 +68,11 @@ export function setAspectRatio(input) {      const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);      const offset = (height - padding) / (height / 50); -    this.media.style.transform = `translateY(-${offset}%)`; +    if (this.fullscreen.active) { +      wrapper.style.paddingBottom = null; +    } else { +      this.media.style.transform = `translateY(-${offset}%)`; +    }    } else if (this.isHTML5) {      wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);    } diff --git a/src/js/utils/time.js b/src/js/utils/time.js index 31660c4a..36e0d59b 100644 --- a/src/js/utils/time.js +++ b/src/js/utils/time.js @@ -5,9 +5,9 @@  import is from './is';  // Time helpers -export const getHours = value => Math.trunc((value / 60 / 60) % 60, 10); -export const getMinutes = value => Math.trunc((value / 60) % 60, 10); -export const getSeconds = value => Math.trunc(value % 60, 10); +export const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10); +export const getMinutes = (value) => Math.trunc((value / 60) % 60, 10); +export const getSeconds = (value) => Math.trunc(value % 60, 10);  // Format time to UI friendly string  export function formatTime(time = 0, displayHours = false, inverted = false) { @@ -17,7 +17,7 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {    }    // Format time component to add leading zero -  const format = value => `0${value}`.slice(-2); +  const format = (value) => `0${value}`.slice(-2);    // Breakdown to hours, mins, secs    let hours = getHours(time);    const mins = getMinutes(time); | 
