Sam Potts 10 months ago
parent
commit
e49da6c13f

+ 7
- 0
changelog.md View File

@@ -1,3 +1,10 @@
1
+# v3.4.5
2
+
3
+-   Added download button option to download either current source or a custom URL you specify in options
4
+-   Prevent immediate hiding of controls on mobile (thanks @jamesoflol)
5
+-   Don't hide controls on focusout event (fixes #1122) (thanks @jamesoflol)
6
+-   Fix HTML5 quality settings being incorrectly set in local storage (thanks @TechGuard)
7
+
1 8
 # v3.4.4
2 9
 
3 10
 -   Fixed issue with double binding for `click` and `touchstart` for `clickToPlay` option

+ 1
- 0
controls.md View File

@@ -28,6 +28,7 @@ controls: [
28 28
     'settings', // Settings menu
29 29
     'pip', // Picture-in-picture (currently Safari only)
30 30
     'airplay', // Airplay (currently Safari only)
31
+    'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
31 32
     'fullscreen', // Toggle fullscreen
32 33
 ];
33 34
 ```

+ 1
- 1
demo/dist/demo.css
File diff suppressed because it is too large
View File


+ 1
- 1
dist/plyr.css
File diff suppressed because it is too large
View File


+ 162
- 78
dist/plyr.js View File

@@ -178,6 +178,11 @@ typeof navigator === "object" && (function (global, factory) {
178 178
     // Accept a URL object
179 179
     if (instanceOf(input, window.URL)) {
180 180
       return true;
181
+    } // Must be string from here
182
+
183
+
184
+    if (!isString(input)) {
185
+      return false;
181 186
     } // Add the protocol if required
182 187
 
183 188
 
@@ -1006,6 +1011,13 @@ typeof navigator === "object" && (function (global, factory) {
1006 1011
     return wrapper.innerHTML;
1007 1012
   }
1008 1013
 
1014
+  var resources = {
1015
+    pip: 'PIP',
1016
+    airplay: 'AirPlay',
1017
+    html5: 'HTML5',
1018
+    vimeo: 'Vimeo',
1019
+    youtube: 'YouTube'
1020
+  };
1009 1021
   var i18n = {
1010 1022
     get: function get() {
1011 1023
       var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -1018,6 +1030,10 @@ typeof navigator === "object" && (function (global, factory) {
1018 1030
       var string = getDeep(config.i18n, key);
1019 1031
 
1020 1032
       if (is.empty(string)) {
1033
+        if (Object.keys(resources).includes(key)) {
1034
+          return resources[key];
1035
+        }
1036
+
1021 1037
         return '';
1022 1038
       }
1023 1039
 
@@ -1330,23 +1346,18 @@ typeof navigator === "object" && (function (global, factory) {
1330 1346
 
1331 1347
       if ('href' in use) {
1332 1348
         use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
1333
-      } else {
1334
-        use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
1335
-      } // Add <use> to <svg>
1349
+      } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
1336 1350
 
1337 1351
 
1352
+      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>
1353
+
1338 1354
       icon.appendChild(use);
1339 1355
       return icon;
1340 1356
     },
1341 1357
     // Create hidden text label
1342
-    createLabel: function createLabel(type) {
1358
+    createLabel: function createLabel(key) {
1343 1359
       var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1344
-      // Skip i18n for abbreviations and brand names
1345
-      var universals = {
1346
-        pip: 'PIP',
1347
-        airplay: 'AirPlay'
1348
-      };
1349
-      var text = universals[type] || i18n.get(type, this.config);
1360
+      var text = i18n.get(key, this.config);
1350 1361
       var attributes = Object.assign({}, attr, {
1351 1362
         class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
1352 1363
       });
@@ -1368,20 +1379,29 @@ typeof navigator === "object" && (function (global, factory) {
1368 1379
     },
1369 1380
     // Create a <button>
1370 1381
     createButton: function createButton(buttonType, attr) {
1371
-      var button = createElement('button');
1372 1382
       var attributes = Object.assign({}, attr);
1373 1383
       var type = toCamelCase(buttonType);
1374
-      var toggle = false;
1375
-      var label;
1376
-      var icon;
1377
-      var labelPressed;
1378
-      var iconPressed;
1384
+      var props = {
1385
+        element: 'button',
1386
+        toggle: false,
1387
+        label: null,
1388
+        icon: null,
1389
+        labelPressed: null,
1390
+        iconPressed: null
1391
+      };
1392
+      ['element', 'icon', 'label'].forEach(function (key) {
1393
+        if (Object.keys(attributes).includes(key)) {
1394
+          props[key] = attributes[key];
1395
+          delete attributes[key];
1396
+        }
1397
+      }); // Default to 'button' type to prevent form submission
1379 1398
 
1380
-      if (!('type' in attributes)) {
1399
+      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
1381 1400
         attributes.type = 'button';
1382
-      }
1401
+      } // Set class name
1383 1402
 
1384
-      if ('class' in attributes) {
1403
+
1404
+      if (Object.keys(attributes).includes('class')) {
1385 1405
         if (!attributes.class.includes(this.config.classNames.control)) {
1386 1406
           attributes.class += " ".concat(this.config.classNames.control);
1387 1407
         }
@@ -1392,69 +1412,76 @@ typeof navigator === "object" && (function (global, factory) {
1392 1412
 
1393 1413
       switch (buttonType) {
1394 1414
         case 'play':
1395
-          toggle = true;
1396
-          label = 'play';
1397
-          labelPressed = 'pause';
1398
-          icon = 'play';
1399
-          iconPressed = 'pause';
1415
+          props.toggle = true;
1416
+          props.label = 'play';
1417
+          props.labelPressed = 'pause';
1418
+          props.icon = 'play';
1419
+          props.iconPressed = 'pause';
1400 1420
           break;
1401 1421
 
1402 1422
         case 'mute':
1403
-          toggle = true;
1404
-          label = 'mute';
1405
-          labelPressed = 'unmute';
1406
-          icon = 'volume';
1407
-          iconPressed = 'muted';
1423
+          props.toggle = true;
1424
+          props.label = 'mute';
1425
+          props.labelPressed = 'unmute';
1426
+          props.icon = 'volume';
1427
+          props.iconPressed = 'muted';
1408 1428
           break;
1409 1429
 
1410 1430
         case 'captions':
1411
-          toggle = true;
1412
-          label = 'enableCaptions';
1413
-          labelPressed = 'disableCaptions';
1414
-          icon = 'captions-off';
1415
-          iconPressed = 'captions-on';
1431
+          props.toggle = true;
1432
+          props.label = 'enableCaptions';
1433
+          props.labelPressed = 'disableCaptions';
1434
+          props.icon = 'captions-off';
1435
+          props.iconPressed = 'captions-on';
1416 1436
           break;
1417 1437
 
1418 1438
         case 'fullscreen':
1419
-          toggle = true;
1420
-          label = 'enterFullscreen';
1421
-          labelPressed = 'exitFullscreen';
1422
-          icon = 'enter-fullscreen';
1423
-          iconPressed = 'exit-fullscreen';
1439
+          props.toggle = true;
1440
+          props.label = 'enterFullscreen';
1441
+          props.labelPressed = 'exitFullscreen';
1442
+          props.icon = 'enter-fullscreen';
1443
+          props.iconPressed = 'exit-fullscreen';
1424 1444
           break;
1425 1445
 
1426 1446
         case 'play-large':
1427 1447
           attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
1428 1448
           type = 'play';
1429
-          label = 'play';
1430
-          icon = 'play';
1449
+          props.label = 'play';
1450
+          props.icon = 'play';
1431 1451
           break;
1432 1452
 
1433 1453
         default:
1434
-          label = type;
1435
-          icon = buttonType;
1436
-      } // Setup toggle icon and labels
1454
+          if (is.empty(props.label)) {
1455
+            props.label = type;
1456
+          }
1457
+
1458
+          if (is.empty(props.icon)) {
1459
+            props.icon = buttonType;
1460
+          }
1437 1461
 
1462
+      }
1438 1463
 
1439
-      if (toggle) {
1464
+      var button = createElement(props.element); // Setup toggle icon and labels
1465
+
1466
+      if (props.toggle) {
1440 1467
         // Icon
1441
-        button.appendChild(controls.createIcon.call(this, iconPressed, {
1468
+        button.appendChild(controls.createIcon.call(this, props.iconPressed, {
1442 1469
           class: 'icon--pressed'
1443 1470
         }));
1444
-        button.appendChild(controls.createIcon.call(this, icon, {
1471
+        button.appendChild(controls.createIcon.call(this, props.icon, {
1445 1472
           class: 'icon--not-pressed'
1446 1473
         })); // Label/Tooltip
1447 1474
 
1448
-        button.appendChild(controls.createLabel.call(this, labelPressed, {
1475
+        button.appendChild(controls.createLabel.call(this, props.labelPressed, {
1449 1476
           class: 'label--pressed'
1450 1477
         }));
1451
-        button.appendChild(controls.createLabel.call(this, label, {
1478
+        button.appendChild(controls.createLabel.call(this, props.label, {
1452 1479
           class: 'label--not-pressed'
1453 1480
         }));
1454 1481
       } else {
1455
-        button.appendChild(controls.createIcon.call(this, icon));
1456
-        button.appendChild(controls.createLabel.call(this, label));
1457
-      } // Merge attributes
1482
+        button.appendChild(controls.createIcon.call(this, props.icon));
1483
+        button.appendChild(controls.createLabel.call(this, props.label));
1484
+      } // Merge and set attributes
1458 1485
 
1459 1486
 
1460 1487
       extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
@@ -2307,6 +2334,17 @@ typeof navigator === "object" && (function (global, factory) {
2307 2334
 
2308 2335
       controls.focusFirstMenuItem.call(this, target, tabFocus);
2309 2336
     },
2337
+    // Set the download link
2338
+    setDownloadLink: function setDownloadLink() {
2339
+      var button = this.elements.buttons.download; // Bail if no button
2340
+
2341
+      if (!is.element(button)) {
2342
+        return;
2343
+      } // Set download link
2344
+
2345
+
2346
+      button.setAttribute('href', this.download);
2347
+    },
2310 2348
     // Build the default HTML
2311 2349
     // TODO: Set order based on order in the config.controls array?
2312 2350
     create: function create(data) {
@@ -2512,6 +2550,25 @@ typeof navigator === "object" && (function (global, factory) {
2512 2550
 
2513 2551
       if (this.config.controls.includes('airplay') && support.airplay) {
2514 2552
         container.appendChild(controls.createButton.call(this, 'airplay'));
2553
+      } // Download button
2554
+
2555
+
2556
+      if (this.config.controls.includes('download')) {
2557
+        var _attributes = {
2558
+          element: 'a',
2559
+          href: this.download,
2560
+          target: '_blank'
2561
+        };
2562
+        var download = this.config.urls.download;
2563
+
2564
+        if (!is.url(download) && this.isEmbed) {
2565
+          extend(_attributes, {
2566
+            icon: "logo-".concat(this.provider),
2567
+            label: this.provider
2568
+          });
2569
+        }
2570
+
2571
+        container.appendChild(controls.createButton.call(this, 'download', _attributes));
2515 2572
       } // Toggle fullscreen button
2516 2573
 
2517 2574
 
@@ -3178,7 +3235,8 @@ typeof navigator === "object" && (function (global, factory) {
3178 3235
     controls: ['play-large', // 'restart',
3179 3236
     // 'rewind',
3180 3237
     'play', // 'fast-forward',
3181
-    'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
3238
+    'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
3239
+    'fullscreen'],
3182 3240
     settings: ['captions', 'quality', 'speed'],
3183 3241
     // Localisation
3184 3242
     i18n: {
@@ -3198,6 +3256,7 @@ typeof navigator === "object" && (function (global, factory) {
3198 3256
       unmute: 'Unmute',
3199 3257
       enableCaptions: 'Enable captions',
3200 3258
       disableCaptions: 'Disable captions',
3259
+      download: 'Download',
3201 3260
       enterFullscreen: 'Enter fullscreen',
3202 3261
       exitFullscreen: 'Exit fullscreen',
3203 3262
       frameTitle: 'Player for {title}',
@@ -3226,6 +3285,7 @@ typeof navigator === "object" && (function (global, factory) {
3226 3285
     },
3227 3286
     // URLs
3228 3287
     urls: {
3288
+      download: null,
3229 3289
       vimeo: {
3230 3290
         sdk: 'https://player.vimeo.com/api/player.js',
3231 3291
         iframe: 'https://player.vimeo.com/video/{0}?{1}',
@@ -3250,6 +3310,7 @@ typeof navigator === "object" && (function (global, factory) {
3250 3310
       mute: null,
3251 3311
       volume: null,
3252 3312
       captions: null,
3313
+      download: null,
3253 3314
       fullscreen: null,
3254 3315
       pip: null,
3255 3316
       airplay: null,
@@ -3262,7 +3323,7 @@ typeof navigator === "object" && (function (global, factory) {
3262 3323
     events: [// Events to watch on HTML5 media elements and bubble
3263 3324
     // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
3264 3325
     'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
3265
-    'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
3326
+    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
3266 3327
     'statechange', // Quality
3267 3328
     'qualitychange', // Ads
3268 3329
     'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
@@ -3284,6 +3345,7 @@ typeof navigator === "object" && (function (global, factory) {
3284 3345
         fastForward: '[data-plyr="fast-forward"]',
3285 3346
         mute: '[data-plyr="mute"]',
3286 3347
         captions: '[data-plyr="captions"]',
3348
+        download: '[data-plyr="download"]',
3287 3349
         fullscreen: '[data-plyr="fullscreen"]',
3288 3350
         pip: '[data-plyr="pip"]',
3289 3351
         airplay: '[data-plyr="airplay"]',
@@ -3396,7 +3458,7 @@ typeof navigator === "object" && (function (global, factory) {
3396 3458
   };
3397 3459
   /**
3398 3460
    * Get provider by URL
3399
-   * @param {string} url
3461
+   * @param {String} url
3400 3462
    */
3401 3463
 
3402 3464
   function getProviderByUrl(url) {
@@ -3923,8 +3985,10 @@ typeof navigator === "object" && (function (global, factory) {
3923 3985
       var controls$$1 = this.elements.controls;
3924 3986
 
3925 3987
       if (controls$$1 && this.config.hideControls) {
3926
-        // Show controls if force, loading, paused, or button interaction, otherwise hide
3927
-        this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover));
3988
+        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
3989
+        var recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
3990
+
3991
+        this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover || recentTouchSeek));
3928 3992
       }
3929 3993
     }
3930 3994
   };
@@ -4283,7 +4347,7 @@ typeof navigator === "object" && (function (global, factory) {
4283 4347
 
4284 4348
           if (!is.element(wrapper)) {
4285 4349
             return;
4286
-          } // On click play, pause ore restart
4350
+          } // On click play, pause or restart
4287 4351
 
4288 4352
 
4289 4353
           on.call(player, elements.container, 'click', function (event) {
@@ -4336,6 +4400,10 @@ typeof navigator === "object" && (function (global, factory) {
4336 4400
         on.call(player, player.media, 'qualitychange', function (event) {
4337 4401
           // Update UI
4338 4402
           controls.updateSetting.call(player, 'quality', null, event.detail.quality);
4403
+        }); // Update download link when ready and if quality changes
4404
+
4405
+        on.call(player, player.media, 'ready qualitychange', function () {
4406
+          controls.setDownloadLink.call(player);
4339 4407
         }); // Proxy events to container
4340 4408
         // Bubble up key events for Edge
4341 4409
 
@@ -4413,7 +4481,11 @@ typeof navigator === "object" && (function (global, factory) {
4413 4481
 
4414 4482
         this.bind(elements.buttons.captions, 'click', function () {
4415 4483
           return player.toggleCaptions();
4416
-        }); // Fullscreen toggle
4484
+        }); // Download
4485
+
4486
+        this.bind(elements.buttons.download, 'click', function () {
4487
+          triggerEvent.call(player, player.media, 'download');
4488
+        }, 'download'); // Fullscreen toggle
4417 4489
 
4418 4490
         this.bind(elements.buttons.fullscreen, 'click', function () {
4419 4491
           player.fullscreen.toggle();
@@ -4476,8 +4548,10 @@ typeof navigator === "object" && (function (global, factory) {
4476 4548
 
4477 4549
           if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
4478 4550
             return;
4479
-          } // Was playing before?
4551
+          } // Record seek time so we can prevent hiding controls for a few seconds after seek
4552
+
4480 4553
 
4554
+          player.lastSeekTime = Date.now(); // Was playing before?
4481 4555
 
4482 4556
           var play = seek.hasAttribute(attribute); // Done seeking
4483 4557
 
@@ -4555,32 +4629,28 @@ typeof navigator === "object" && (function (global, factory) {
4555 4629
 
4556 4630
         this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
4557 4631
           elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
4558
-        }); // Focus in/out on controls
4632
+        }); // Show controls when they receive focus (e.g., when using keyboard tab key)
4559 4633
 
4560
-        this.bind(elements.controls, 'focusin focusout', function (event) {
4634
+        this.bind(elements.controls, 'focusin', function () {
4561 4635
           var config = player.config,
4562 4636
               elements = player.elements,
4563
-              timers = player.timers;
4564
-          var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element
4637
+              timers = player.timers; // Skip transition to prevent focus from scrolling the parent element
4565 4638
 
4566
-          toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); // Toggle
4639
+          toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
4567 4640
 
4568
-          ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay
4641
+          ui.toggleControls.call(player, true); // Restore transition
4569 4642
 
4570
-          if (isFocusIn) {
4571
-            // Restore transition
4572
-            setTimeout(function () {
4573
-              toggleClass(elements.controls, config.classNames.noTransition, false);
4574
-            }, 0); // Delay a little more for keyboard users
4643
+          setTimeout(function () {
4644
+            toggleClass(elements.controls, config.classNames.noTransition, false);
4645
+          }, 0); // Delay a little more for mouse users
4575 4646
 
4576
-            var delay = _this2.touch ? 3000 : 4000; // Clear timer
4647
+          var delay = _this2.touch ? 3000 : 4000; // Clear timer
4577 4648
 
4578
-            clearTimeout(timers.controls); // Hide
4649
+          clearTimeout(timers.controls); // Hide again after delay
4579 4650
 
4580
-            timers.controls = setTimeout(function () {
4581
-              return ui.toggleControls.call(player, false);
4582
-            }, delay);
4583
-          }
4651
+          timers.controls = setTimeout(function () {
4652
+            return ui.toggleControls.call(player, false);
4653
+          }, delay);
4584 4654
         }); // Mouse wheel for volume
4585 4655
 
4586 4656
         this.bind(elements.inputs.volume, 'wheel', function (event) {
@@ -5171,6 +5241,7 @@ typeof navigator === "object" && (function (global, factory) {
5171 5241
       var currentSrc;
5172 5242
       player.embed.getVideoUrl().then(function (value) {
5173 5243
         currentSrc = value;
5244
+        controls.setDownloadLink.call(player);
5174 5245
       }).catch(function (error) {
5175 5246
         _this2.debug.warn(error);
5176 5247
       });
@@ -6724,7 +6795,10 @@ typeof navigator === "object" && (function (global, factory) {
6724 6795
 
6725 6796
       if (this.config.autoplay) {
6726 6797
         this.play();
6727
-      }
6798
+      } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
6799
+
6800
+
6801
+      this.lastSeekTime = 0;
6728 6802
     } // ---------------------------------------
6729 6803
     // API
6730 6804
     // ---------------------------------------
@@ -7449,6 +7523,16 @@ typeof navigator === "object" && (function (global, factory) {
7449 7523
         return this.media.currentSrc;
7450 7524
       }
7451 7525
       /**
7526
+       * Get a download URL (either source or custom)
7527
+       */
7528
+
7529
+    }, {
7530
+      key: "download",
7531
+      get: function get() {
7532
+        var download = this.config.urls.download;
7533
+        return is.url(download) ? download : this.source;
7534
+      }
7535
+      /**
7452 7536
        * Set the poster image for a video
7453 7537
        * @param {input} - the URL for the new poster image
7454 7538
        */

+ 1
- 1
dist/plyr.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
dist/plyr.min.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/plyr.min.js.map
File diff suppressed because it is too large
View File


+ 162
- 78
dist/plyr.polyfilled.js View File

@@ -2804,6 +2804,11 @@ typeof navigator === "object" && (function (global, factory) {
2804 2804
     // Accept a URL object
2805 2805
     if (instanceOf(input, window.URL)) {
2806 2806
       return true;
2807
+    } // Must be string from here
2808
+
2809
+
2810
+    if (!isString(input)) {
2811
+      return false;
2807 2812
     } // Add the protocol if required
2808 2813
 
2809 2814
 
@@ -3669,6 +3674,13 @@ typeof navigator === "object" && (function (global, factory) {
3669 3674
     return wrapper.innerHTML;
3670 3675
   }
3671 3676
 
3677
+  var resources = {
3678
+    pip: 'PIP',
3679
+    airplay: 'AirPlay',
3680
+    html5: 'HTML5',
3681
+    vimeo: 'Vimeo',
3682
+    youtube: 'YouTube'
3683
+  };
3672 3684
   var i18n = {
3673 3685
     get: function get() {
3674 3686
       var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -3681,6 +3693,10 @@ typeof navigator === "object" && (function (global, factory) {
3681 3693
       var string = getDeep(config.i18n, key);
3682 3694
 
3683 3695
       if (is$1.empty(string)) {
3696
+        if (Object.keys(resources).includes(key)) {
3697
+          return resources[key];
3698
+        }
3699
+
3684 3700
         return '';
3685 3701
       }
3686 3702
 
@@ -3993,23 +4009,18 @@ typeof navigator === "object" && (function (global, factory) {
3993 4009
 
3994 4010
       if ('href' in use) {
3995 4011
         use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
3996
-      } else {
3997
-        use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
3998
-      } // Add <use> to <svg>
4012
+      } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
3999 4013
 
4000 4014
 
4015
+      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>
4016
+
4001 4017
       icon.appendChild(use);
4002 4018
       return icon;
4003 4019
     },
4004 4020
     // Create hidden text label
4005
-    createLabel: function createLabel(type) {
4021
+    createLabel: function createLabel(key) {
4006 4022
       var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
4007
-      // Skip i18n for abbreviations and brand names
4008
-      var universals = {
4009
-        pip: 'PIP',
4010
-        airplay: 'AirPlay'
4011
-      };
4012
-      var text = universals[type] || i18n.get(type, this.config);
4023
+      var text = i18n.get(key, this.config);
4013 4024
       var attributes = Object.assign({}, attr, {
4014 4025
         class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
4015 4026
       });
@@ -4031,20 +4042,29 @@ typeof navigator === "object" && (function (global, factory) {
4031 4042
     },
4032 4043
     // Create a <button>
4033 4044
     createButton: function createButton(buttonType, attr) {
4034
-      var button = createElement('button');
4035 4045
       var attributes = Object.assign({}, attr);
4036 4046
       var type = toCamelCase(buttonType);
4037
-      var toggle = false;
4038
-      var label;
4039
-      var icon;
4040
-      var labelPressed;
4041
-      var iconPressed;
4047
+      var props = {
4048
+        element: 'button',
4049
+        toggle: false,
4050
+        label: null,
4051
+        icon: null,
4052
+        labelPressed: null,
4053
+        iconPressed: null
4054
+      };
4055
+      ['element', 'icon', 'label'].forEach(function (key) {
4056
+        if (Object.keys(attributes).includes(key)) {
4057
+          props[key] = attributes[key];
4058
+          delete attributes[key];
4059
+        }
4060
+      }); // Default to 'button' type to prevent form submission
4042 4061
 
4043
-      if (!('type' in attributes)) {
4062
+      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
4044 4063
         attributes.type = 'button';
4045
-      }
4064
+      } // Set class name
4046 4065
 
4047
-      if ('class' in attributes) {
4066
+
4067
+      if (Object.keys(attributes).includes('class')) {
4048 4068
         if (!attributes.class.includes(this.config.classNames.control)) {
4049 4069
           attributes.class += " ".concat(this.config.classNames.control);
4050 4070
         }
@@ -4055,69 +4075,76 @@ typeof navigator === "object" && (function (global, factory) {
4055 4075
 
4056 4076
       switch (buttonType) {
4057 4077
         case 'play':
4058
-          toggle = true;
4059
-          label = 'play';
4060
-          labelPressed = 'pause';
4061
-          icon = 'play';
4062
-          iconPressed = 'pause';
4078
+          props.toggle = true;
4079
+          props.label = 'play';
4080
+          props.labelPressed = 'pause';
4081
+          props.icon = 'play';
4082
+          props.iconPressed = 'pause';
4063 4083
           break;
4064 4084
 
4065 4085
         case 'mute':
4066
-          toggle = true;
4067
-          label = 'mute';
4068
-          labelPressed = 'unmute';
4069
-          icon = 'volume';
4070
-          iconPressed = 'muted';
4086
+          props.toggle = true;
4087
+          props.label = 'mute';
4088
+          props.labelPressed = 'unmute';
4089
+          props.icon = 'volume';
4090
+          props.iconPressed = 'muted';
4071 4091
           break;
4072 4092
 
4073 4093
         case 'captions':
4074
-          toggle = true;
4075
-          label = 'enableCaptions';
4076
-          labelPressed = 'disableCaptions';
4077
-          icon = 'captions-off';
4078
-          iconPressed = 'captions-on';
4094
+          props.toggle = true;
4095
+          props.label = 'enableCaptions';
4096
+          props.labelPressed = 'disableCaptions';
4097
+          props.icon = 'captions-off';
4098
+          props.iconPressed = 'captions-on';
4079 4099
           break;
4080 4100
 
4081 4101
         case 'fullscreen':
4082
-          toggle = true;
4083
-          label = 'enterFullscreen';
4084
-          labelPressed = 'exitFullscreen';
4085
-          icon = 'enter-fullscreen';
4086
-          iconPressed = 'exit-fullscreen';
4102
+          props.toggle = true;
4103
+          props.label = 'enterFullscreen';
4104
+          props.labelPressed = 'exitFullscreen';
4105
+          props.icon = 'enter-fullscreen';
4106
+          props.iconPressed = 'exit-fullscreen';
4087 4107
           break;
4088 4108
 
4089 4109
         case 'play-large':
4090 4110
           attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
4091 4111
           type = 'play';
4092
-          label = 'play';
4093
-          icon = 'play';
4112
+          props.label = 'play';
4113
+          props.icon = 'play';
4094 4114
           break;
4095 4115
 
4096 4116
         default:
4097
-          label = type;
4098
-          icon = buttonType;
4099
-      } // Setup toggle icon and labels
4117
+          if (is$1.empty(props.label)) {
4118
+            props.label = type;
4119
+          }
4120
+
4121
+          if (is$1.empty(props.icon)) {
4122
+            props.icon = buttonType;
4123
+          }
4100 4124
 
4125
+      }
4101 4126
 
4102
-      if (toggle) {
4127
+      var button = createElement(props.element); // Setup toggle icon and labels
4128
+
4129
+      if (props.toggle) {
4103 4130
         // Icon
4104
-        button.appendChild(controls.createIcon.call(this, iconPressed, {
4131
+        button.appendChild(controls.createIcon.call(this, props.iconPressed, {
4105 4132
           class: 'icon--pressed'
4106 4133
         }));
4107
-        button.appendChild(controls.createIcon.call(this, icon, {
4134
+        button.appendChild(controls.createIcon.call(this, props.icon, {
4108 4135
           class: 'icon--not-pressed'
4109 4136
         })); // Label/Tooltip
4110 4137
 
4111
-        button.appendChild(controls.createLabel.call(this, labelPressed, {
4138
+        button.appendChild(controls.createLabel.call(this, props.labelPressed, {
4112 4139
           class: 'label--pressed'
4113 4140
         }));
4114
-        button.appendChild(controls.createLabel.call(this, label, {
4141
+        button.appendChild(controls.createLabel.call(this, props.label, {
4115 4142
           class: 'label--not-pressed'
4116 4143
         }));
4117 4144
       } else {
4118
-        button.appendChild(controls.createIcon.call(this, icon));
4119
-        button.appendChild(controls.createLabel.call(this, label));
4120
-      } // Merge attributes
4145
+        button.appendChild(controls.createIcon.call(this, props.icon));
4146
+        button.appendChild(controls.createLabel.call(this, props.label));
4147
+      } // Merge and set attributes
4121 4148
 
4122 4149
 
4123 4150
       extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
@@ -4970,6 +4997,17 @@ typeof navigator === "object" && (function (global, factory) {
4970 4997
 
4971 4998
       controls.focusFirstMenuItem.call(this, target, tabFocus);
4972 4999
     },
5000
+    // Set the download link
5001
+    setDownloadLink: function setDownloadLink() {
5002
+      var button = this.elements.buttons.download; // Bail if no button
5003
+
5004
+      if (!is$1.element(button)) {
5005
+        return;
5006
+      } // Set download link
5007
+
5008
+
5009
+      button.setAttribute('href', this.download);
5010
+    },
4973 5011
     // Build the default HTML
4974 5012
     // TODO: Set order based on order in the config.controls array?
4975 5013
     create: function create(data) {
@@ -5175,6 +5213,25 @@ typeof navigator === "object" && (function (global, factory) {
5175 5213
 
5176 5214
       if (this.config.controls.includes('airplay') && support.airplay) {
5177 5215
         container.appendChild(controls.createButton.call(this, 'airplay'));
5216
+      } // Download button
5217
+
5218
+
5219
+      if (this.config.controls.includes('download')) {
5220
+        var _attributes = {
5221
+          element: 'a',
5222
+          href: this.download,
5223
+          target: '_blank'
5224
+        };
5225
+        var download = this.config.urls.download;
5226
+
5227
+        if (!is$1.url(download) && this.isEmbed) {
5228
+          extend(_attributes, {
5229
+            icon: "logo-".concat(this.provider),
5230
+            label: this.provider
5231
+          });
5232
+        }
5233
+
5234
+        container.appendChild(controls.createButton.call(this, 'download', _attributes));
5178 5235
       } // Toggle fullscreen button
5179 5236
 
5180 5237
 
@@ -5841,7 +5898,8 @@ typeof navigator === "object" && (function (global, factory) {
5841 5898
     controls: ['play-large', // 'restart',
5842 5899
     // 'rewind',
5843 5900
     'play', // 'fast-forward',
5844
-    'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
5901
+    'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
5902
+    'fullscreen'],
5845 5903
     settings: ['captions', 'quality', 'speed'],
5846 5904
     // Localisation
5847 5905
     i18n: {
@@ -5861,6 +5919,7 @@ typeof navigator === "object" && (function (global, factory) {
5861 5919
       unmute: 'Unmute',
5862 5920
       enableCaptions: 'Enable captions',
5863 5921
       disableCaptions: 'Disable captions',
5922
+      download: 'Download',
5864 5923
       enterFullscreen: 'Enter fullscreen',
5865 5924
       exitFullscreen: 'Exit fullscreen',
5866 5925
       frameTitle: 'Player for {title}',
@@ -5889,6 +5948,7 @@ typeof navigator === "object" && (function (global, factory) {
5889 5948
     },
5890 5949
     // URLs
5891 5950
     urls: {
5951
+      download: null,
5892 5952
       vimeo: {
5893 5953
         sdk: 'https://player.vimeo.com/api/player.js',
5894 5954
         iframe: 'https://player.vimeo.com/video/{0}?{1}',
@@ -5913,6 +5973,7 @@ typeof navigator === "object" && (function (global, factory) {
5913 5973
       mute: null,
5914 5974
       volume: null,
5915 5975
       captions: null,
5976
+      download: null,
5916 5977
       fullscreen: null,
5917 5978
       pip: null,
5918 5979
       airplay: null,
@@ -5925,7 +5986,7 @@ typeof navigator === "object" && (function (global, factory) {
5925 5986
     events: [// Events to watch on HTML5 media elements and bubble
5926 5987
     // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
5927 5988
     'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
5928
-    'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
5989
+    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
5929 5990
     'statechange', // Quality
5930 5991
     'qualitychange', // Ads
5931 5992
     'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
@@ -5947,6 +6008,7 @@ typeof navigator === "object" && (function (global, factory) {
5947 6008
         fastForward: '[data-plyr="fast-forward"]',
5948 6009
         mute: '[data-plyr="mute"]',
5949 6010
         captions: '[data-plyr="captions"]',
6011
+        download: '[data-plyr="download"]',
5950 6012
         fullscreen: '[data-plyr="fullscreen"]',
5951 6013
         pip: '[data-plyr="pip"]',
5952 6014
         airplay: '[data-plyr="airplay"]',
@@ -6059,7 +6121,7 @@ typeof navigator === "object" && (function (global, factory) {
6059 6121
   };
6060 6122
   /**
6061 6123
    * Get provider by URL
6062
-   * @param {string} url
6124
+   * @param {String} url
6063 6125
    */
6064 6126
 
6065 6127
   function getProviderByUrl(url) {
@@ -6596,8 +6658,10 @@ typeof navigator === "object" && (function (global, factory) {
6596 6658
       var controls$$1 = this.elements.controls;
6597 6659
 
6598 6660
       if (controls$$1 && this.config.hideControls) {
6599
-        // Show controls if force, loading, paused, or button interaction, otherwise hide
6600
-        this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover));
6661
+        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
6662
+        var recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
6663
+
6664
+        this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover || recentTouchSeek));
6601 6665
       }
6602 6666
     }
6603 6667
   };
@@ -6956,7 +7020,7 @@ typeof navigator === "object" && (function (global, factory) {
6956 7020
 
6957 7021
           if (!is$1.element(wrapper)) {
6958 7022
             return;
6959
-          } // On click play, pause ore restart
7023
+          } // On click play, pause or restart
6960 7024
 
6961 7025
 
6962 7026
           on.call(player, elements.container, 'click', function (event) {
@@ -7009,6 +7073,10 @@ typeof navigator === "object" && (function (global, factory) {
7009 7073
         on.call(player, player.media, 'qualitychange', function (event) {
7010 7074
           // Update UI
7011 7075
           controls.updateSetting.call(player, 'quality', null, event.detail.quality);
7076
+        }); // Update download link when ready and if quality changes
7077
+
7078
+        on.call(player, player.media, 'ready qualitychange', function () {
7079
+          controls.setDownloadLink.call(player);
7012 7080
         }); // Proxy events to container
7013 7081
         // Bubble up key events for Edge
7014 7082
 
@@ -7086,7 +7154,11 @@ typeof navigator === "object" && (function (global, factory) {
7086 7154
 
7087 7155
         this.bind(elements.buttons.captions, 'click', function () {
7088 7156
           return player.toggleCaptions();
7089
-        }); // Fullscreen toggle
7157
+        }); // Download
7158
+
7159
+        this.bind(elements.buttons.download, 'click', function () {
7160
+          triggerEvent.call(player, player.media, 'download');
7161
+        }, 'download'); // Fullscreen toggle
7090 7162
 
7091 7163
         this.bind(elements.buttons.fullscreen, 'click', function () {
7092 7164
           player.fullscreen.toggle();
@@ -7149,8 +7221,10 @@ typeof navigator === "object" && (function (global, factory) {
7149 7221
 
7150 7222
           if (is$1.keyboardEvent(event) && code !== 39 && code !== 37) {
7151 7223
             return;
7152
-          } // Was playing before?
7224
+          } // Record seek time so we can prevent hiding controls for a few seconds after seek
7225
+
7153 7226
 
7227
+          player.lastSeekTime = Date.now(); // Was playing before?
7154 7228
 
7155 7229
           var play = seek.hasAttribute(attribute); // Done seeking
7156 7230
 
@@ -7228,32 +7302,28 @@ typeof navigator === "object" && (function (global, factory) {
7228 7302
 
7229 7303
         this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
7230 7304
           elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
7231
-        }); // Focus in/out on controls
7305
+        }); // Show controls when they receive focus (e.g., when using keyboard tab key)
7232 7306
 
7233
-        this.bind(elements.controls, 'focusin focusout', function (event) {
7307
+        this.bind(elements.controls, 'focusin', function () {
7234 7308
           var config = player.config,
7235 7309
               elements = player.elements,
7236
-              timers = player.timers;
7237
-          var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element
7310
+              timers = player.timers; // Skip transition to prevent focus from scrolling the parent element
7238 7311
 
7239
-          toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); // Toggle
7312
+          toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
7240 7313
 
7241
-          ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay
7314
+          ui.toggleControls.call(player, true); // Restore transition
7242 7315
 
7243
-          if (isFocusIn) {
7244
-            // Restore transition
7245
-            setTimeout(function () {
7246
-              toggleClass(elements.controls, config.classNames.noTransition, false);
7247
-            }, 0); // Delay a little more for keyboard users
7316
+          setTimeout(function () {
7317
+            toggleClass(elements.controls, config.classNames.noTransition, false);
7318
+          }, 0); // Delay a little more for mouse users
7248 7319
 
7249
-            var delay = _this2.touch ? 3000 : 4000; // Clear timer
7320
+          var delay = _this2.touch ? 3000 : 4000; // Clear timer
7250 7321
 
7251
-            clearTimeout(timers.controls); // Hide
7322
+          clearTimeout(timers.controls); // Hide again after delay
7252 7323
 
7253
-            timers.controls = setTimeout(function () {
7254
-              return ui.toggleControls.call(player, false);
7255
-            }, delay);
7256
-          }
7324
+          timers.controls = setTimeout(function () {
7325
+            return ui.toggleControls.call(player, false);
7326
+          }, delay);
7257 7327
         }); // Mouse wheel for volume
7258 7328
 
7259 7329
         this.bind(elements.inputs.volume, 'wheel', function (event) {
@@ -7864,6 +7934,7 @@ typeof navigator === "object" && (function (global, factory) {
7864 7934
       var currentSrc;
7865 7935
       player.embed.getVideoUrl().then(function (value) {
7866 7936
         currentSrc = value;
7937
+        controls.setDownloadLink.call(player);
7867 7938
       }).catch(function (error) {
7868 7939
         _this2.debug.warn(error);
7869 7940
       });
@@ -9414,7 +9485,10 @@ typeof navigator === "object" && (function (global, factory) {
9414 9485
 
9415 9486
       if (this.config.autoplay) {
9416 9487
         this.play();
9417
-      }
9488
+      } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
9489
+
9490
+
9491
+      this.lastSeekTime = 0;
9418 9492
     } // ---------------------------------------
9419 9493
     // API
9420 9494
     // ---------------------------------------
@@ -10139,6 +10213,16 @@ typeof navigator === "object" && (function (global, factory) {
10139 10213
         return this.media.currentSrc;
10140 10214
       }
10141 10215
       /**
10216
+       * Get a download URL (either source or custom)
10217
+       */
10218
+
10219
+    }, {
10220
+      key: "download",
10221
+      get: function get() {
10222
+        var download = this.config.urls.download;
10223
+        return is$1.url(download) ? download : this.source;
10224
+      }
10225
+      /**
10142 10226
        * Set the poster image for a video
10143 10227
        * @param {input} - the URL for the new poster image
10144 10228
        */

+ 1
- 1
dist/plyr.polyfilled.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
dist/plyr.polyfilled.min.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/plyr.polyfilled.min.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
dist/plyr.svg
File diff suppressed because it is too large
View File


+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
1 1
 {
2 2
     "name": "plyr",
3
-    "version": "3.4.4",
3
+    "version": "3.4.5",
4 4
     "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
5 5
     "homepage": "https://plyr.io",
6 6
     "author": "Sam Potts <sam@potts.es>",

+ 5
- 4
readme.md View File

@@ -132,13 +132,13 @@ See [initialising](#initialising) for more information on advanced setups.
132 132
 You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
133 133
 
134 134
 ```html
135
-<script src="https://cdn.plyr.io/3.4.4/plyr.js"></script>
135
+<script src="https://cdn.plyr.io/3.4.5/plyr.js"></script>
136 136
 ```
137 137
 
138 138
 ...or...
139 139
 
140 140
 ```html
141
-<script src="https://cdn.plyr.io/3.4.4/plyr.polyfilled.js"></script>
141
+<script src="https://cdn.plyr.io/3.4.5/plyr.polyfilled.js"></script>
142 142
 ```
143 143
 
144 144
 ### CSS
@@ -152,13 +152,13 @@ Include the `plyr.css` stylsheet into your `<head>`
152 152
 If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
153 153
 
154 154
 ```html
155
-<link rel="stylesheet" href="https://cdn.plyr.io/3.4.4/plyr.css">
155
+<link rel="stylesheet" href="https://cdn.plyr.io/3.4.5/plyr.css">
156 156
 ```
157 157
 
158 158
 ### SVG Sprite
159 159
 
160 160
 The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
161
-reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.4/plyr.svg`.
161
+reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.5/plyr.svg`.
162 162
 
163 163
 ## Ads
164 164
 
@@ -315,6 +315,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
315 315
 | `quality`            | Object                     | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }`      | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display.                                                                                                                                                                                                                              |
316 316
 | `loop`               | Object                     | `{ active: false }`                                                                                                            | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality.                                                                                                                                                         |
317 317
 | `ads`                | Object                     | `{ enabled: false, publisherId: '' }`                                                                                          | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID.                                                                                                                                                                                                                                                                                 |
318
+| `urls`               | Object                     | See source.                                                                                                                    | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button.                                                                                                                                                                                                                                          |
318 319
 
319 320
 1.  Vimeo only
320 321
 

+ 1
- 1
src/js/plyr.js View File

@@ -1,6 +1,6 @@
1 1
 // ==========================================================================
2 2
 // Plyr
3
-// plyr.js v3.4.4
3
+// plyr.js v3.4.5
4 4
 // https://github.com/sampotts/plyr
5 5
 // License: The MIT License (MIT)
6 6
 // ==========================================================================

+ 1
- 1
src/js/plyr.polyfilled.js View File

@@ -1,6 +1,6 @@
1 1
 // ==========================================================================
2 2
 // Plyr Polyfilled Build
3
-// plyr.js v3.4.4
3
+// plyr.js v3.4.5
4 4
 // https://github.com/sampotts/plyr
5 5
 // License: The MIT License (MIT)
6 6
 // ==========================================================================