@cloudnest/redxplyr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.editorconfig +10 -0
  2. package/.gitpod.yml +6 -0
  3. package/.node-version +1 -0
  4. package/.prettierrc +7 -0
  5. package/.stickler.yml +5 -0
  6. package/.stylelintrc.json +26 -0
  7. package/CHANGELOG.md +16 -0
  8. package/CONTRIBUTING.md +34 -0
  9. package/CONTROLS.md +49 -0
  10. package/Dockerfile +32 -0
  11. package/LICENSE.md +22 -0
  12. package/README.md +194 -0
  13. package/cspell.json +48 -0
  14. package/dist/redxplyr.css +1 -0
  15. package/dist/redxplyr.js +8801 -0
  16. package/dist/redxplyr.min.js +2 -0
  17. package/dist/redxplyr.min.js.map +1 -0
  18. package/dist/redxplyr.min.mjs +1 -0
  19. package/dist/redxplyr.min.mjs.map +1 -0
  20. package/dist/redxplyr.mjs +8793 -0
  21. package/dist/redxplyr.polyfilled.js +9294 -0
  22. package/dist/redxplyr.polyfilled.min.js +2 -0
  23. package/dist/redxplyr.polyfilled.min.js.map +1 -0
  24. package/dist/redxplyr.polyfilled.min.mjs +1 -0
  25. package/dist/redxplyr.polyfilled.min.mjs.map +1 -0
  26. package/dist/redxplyr.polyfilled.mjs +9286 -0
  27. package/dist/redxplyr.svg +1 -0
  28. package/eslint.config.mjs +39 -0
  29. package/gulpfile.js +8 -0
  30. package/package.json +114 -0
  31. package/pnpm-workspace.yaml +8 -0
  32. package/src/js/captions.js +411 -0
  33. package/src/js/config/defaults.js +459 -0
  34. package/src/js/config/states.js +10 -0
  35. package/src/js/config/types.js +34 -0
  36. package/src/js/console.js +28 -0
  37. package/src/js/controls.js +1870 -0
  38. package/src/js/fullscreen.js +305 -0
  39. package/src/js/html5.js +148 -0
  40. package/src/js/listeners.js +854 -0
  41. package/src/js/media.js +61 -0
  42. package/src/js/plugins/ads.js +647 -0
  43. package/src/js/plugins/preview-thumbnails.js +706 -0
  44. package/src/js/plugins/vimeo.js +443 -0
  45. package/src/js/plugins/youtube.js +451 -0
  46. package/src/js/plyr.d.ts +729 -0
  47. package/src/js/plyr.js +1291 -0
  48. package/src/js/plyr.polyfilled.js +13 -0
  49. package/src/js/source.js +155 -0
  50. package/src/js/storage.js +70 -0
  51. package/src/js/support.js +100 -0
  52. package/src/js/ui.js +297 -0
  53. package/src/js/utils/animation.js +33 -0
  54. package/src/js/utils/arrays.js +23 -0
  55. package/src/js/utils/browser.js +21 -0
  56. package/src/js/utils/elements.js +263 -0
  57. package/src/js/utils/events.js +116 -0
  58. package/src/js/utils/fetch.js +45 -0
  59. package/src/js/utils/i18n.js +47 -0
  60. package/src/js/utils/is.js +81 -0
  61. package/src/js/utils/load-image.js +19 -0
  62. package/src/js/utils/load-script.js +14 -0
  63. package/src/js/utils/load-sprite.js +77 -0
  64. package/src/js/utils/numbers.js +17 -0
  65. package/src/js/utils/objects.js +43 -0
  66. package/src/js/utils/promise.js +14 -0
  67. package/src/js/utils/strings.js +80 -0
  68. package/src/js/utils/style.js +148 -0
  69. package/src/js/utils/time.js +36 -0
  70. package/src/js/utils/urls.js +40 -0
  71. package/src/sass/base.scss +69 -0
  72. package/src/sass/components/badges.scss +12 -0
  73. package/src/sass/components/captions.scss +58 -0
  74. package/src/sass/components/control.scss +52 -0
  75. package/src/sass/components/controls.scss +65 -0
  76. package/src/sass/components/menus.scss +205 -0
  77. package/src/sass/components/poster.scss +27 -0
  78. package/src/sass/components/progress.scss +107 -0
  79. package/src/sass/components/sliders.scss +99 -0
  80. package/src/sass/components/times.scss +20 -0
  81. package/src/sass/components/tooltips.scss +91 -0
  82. package/src/sass/components/volume.scss +18 -0
  83. package/src/sass/lib/animation.scss +31 -0
  84. package/src/sass/lib/css-vars.scss +103 -0
  85. package/src/sass/lib/functions.scss +3 -0
  86. package/src/sass/lib/mixins.scss +82 -0
  87. package/src/sass/plugins/ads.scss +53 -0
  88. package/src/sass/plugins/preview-thumbnails/index.scss +121 -0
  89. package/src/sass/plugins/preview-thumbnails/settings.scss +17 -0
  90. package/src/sass/plyr.scss +46 -0
  91. package/src/sass/settings/badges.scss +7 -0
  92. package/src/sass/settings/breakpoints.scss +9 -0
  93. package/src/sass/settings/captions.scss +10 -0
  94. package/src/sass/settings/colors.scss +18 -0
  95. package/src/sass/settings/controls.scss +30 -0
  96. package/src/sass/settings/cosmetics.scss +5 -0
  97. package/src/sass/settings/helpers.scss +7 -0
  98. package/src/sass/settings/menus.scss +13 -0
  99. package/src/sass/settings/progress.scss +18 -0
  100. package/src/sass/settings/sliders.scss +39 -0
  101. package/src/sass/settings/tooltips.scss +11 -0
  102. package/src/sass/settings/type.scss +16 -0
  103. package/src/sass/states/fullscreen.scss +15 -0
  104. package/src/sass/types/audio.scss +61 -0
  105. package/src/sass/types/video.scss +170 -0
  106. package/src/sass/utils/animation.scss +7 -0
  107. package/src/sass/utils/hidden.scss +28 -0
  108. package/src/sprite/plyr-airplay.svg +8 -0
  109. package/src/sprite/plyr-captions-off.svg +7 -0
  110. package/src/sprite/plyr-captions-on.svg +7 -0
  111. package/src/sprite/plyr-download.svg +8 -0
  112. package/src/sprite/plyr-enter-fullscreen.svg +4 -0
  113. package/src/sprite/plyr-exit-fullscreen.svg +4 -0
  114. package/src/sprite/plyr-fast-forward.svg +3 -0
  115. package/src/sprite/plyr-logo-vimeo.svg +6 -0
  116. package/src/sprite/plyr-logo-youtube.svg +6 -0
  117. package/src/sprite/plyr-muted.svg +8 -0
  118. package/src/sprite/plyr-pause.svg +8 -0
  119. package/src/sprite/plyr-pip.svg +6 -0
  120. package/src/sprite/plyr-play.svg +5 -0
  121. package/src/sprite/plyr-restart.svg +5 -0
  122. package/src/sprite/plyr-rewind.svg +3 -0
  123. package/src/sprite/plyr-settings.svg +5 -0
  124. package/src/sprite/plyr-volume.svg +11 -0
  125. package/tasks/build.js +226 -0
  126. package/tasks/deploy.js +216 -0
  127. package/tasks/utils/publish.js +34 -0
package/src/js/plyr.js ADDED
@@ -0,0 +1,1291 @@
1
+ // ==========================================================================
2
+ // redxplyr
3
+ // redxplyr.js v1.0.0
4
+ // https://github.com/xgauravyaduvanshii/redxplyr
5
+ // License: The MIT License (MIT)
6
+ // ==========================================================================
7
+
8
+ import captions from './captions';
9
+ import defaults from './config/defaults';
10
+ import { pip } from './config/states';
11
+ import { getProviderByUrl, providers, types } from './config/types';
12
+ import Console from './console';
13
+ import controls from './controls';
14
+ import Fullscreen from './fullscreen';
15
+ import html5 from './html5';
16
+ import Listeners from './listeners';
17
+ import media from './media';
18
+ import Ads from './plugins/ads';
19
+ import PreviewThumbnails from './plugins/preview-thumbnails';
20
+ import source from './source';
21
+ import Storage from './storage';
22
+ import support from './support';
23
+ import ui from './ui';
24
+ import { closest } from './utils/arrays';
25
+ import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
26
+ import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
27
+ import is from './utils/is';
28
+ import loadSprite from './utils/load-sprite';
29
+ import { clamp } from './utils/numbers';
30
+ import { cloneDeep, extend } from './utils/objects';
31
+ import { silencePromise } from './utils/promise';
32
+ import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';
33
+ import { parseUrl } from './utils/urls';
34
+
35
+ // Private properties
36
+ // TODO: Use a WeakMap for private globals
37
+ // const globals = new WeakMap();
38
+
39
+ // Plyr instance
40
+ class Plyr {
41
+ constructor(target, options) {
42
+ this.timers = {};
43
+
44
+ // State
45
+ this.ready = false;
46
+ this.loading = false;
47
+ this.failed = false;
48
+
49
+ // Touch device
50
+ this.touch = support.touch;
51
+
52
+ // Set the media element
53
+ this.media = target;
54
+
55
+ // String selector passed
56
+ if (is.string(this.media)) {
57
+ this.media = document.querySelectorAll(this.media);
58
+ }
59
+
60
+ // jQuery, NodeList or Array passed, use first element
61
+ if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {
62
+ this.media = this.media[0];
63
+ }
64
+
65
+ // Set config
66
+ this.config = extend(
67
+ {},
68
+ defaults,
69
+ Plyr.defaults,
70
+ options || {},
71
+ (() => {
72
+ try {
73
+ return JSON.parse(this.media.getAttribute('data-plyr-config'));
74
+ }
75
+ catch {
76
+ return {};
77
+ }
78
+ })(),
79
+ );
80
+
81
+ // Elements cache
82
+ this.elements = {
83
+ container: null,
84
+ fullscreen: null,
85
+ captions: null,
86
+ buttons: {},
87
+ display: {},
88
+ progress: {},
89
+ inputs: {},
90
+ settings: {
91
+ popup: null,
92
+ menu: null,
93
+ panels: {},
94
+ buttons: {},
95
+ },
96
+ };
97
+
98
+ // Captions
99
+ this.captions = {
100
+ active: null,
101
+ currentTrack: -1,
102
+ meta: new WeakMap(),
103
+ };
104
+
105
+ // Fullscreen
106
+ this.fullscreen = {
107
+ active: false,
108
+ };
109
+
110
+ // Options
111
+ this.options = {
112
+ speed: [],
113
+ quality: [],
114
+ };
115
+
116
+ // Debugging
117
+ // TODO: move to globals
118
+ this.debug = new Console(this.config.debug);
119
+
120
+ // Log config options and support
121
+ this.debug.log('Config', this.config);
122
+ this.debug.log('Support', support);
123
+
124
+ // We need an element to setup
125
+ if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
126
+ this.debug.error('Setup failed: no suitable element passed');
127
+ return;
128
+ }
129
+
130
+ // Bail if the element is initialized
131
+ if (this.media.plyr) {
132
+ this.debug.warn('Target already setup');
133
+ return;
134
+ }
135
+
136
+ // Bail if not enabled
137
+ if (!this.config.enabled) {
138
+ this.debug.error('Setup failed: disabled by config');
139
+ return;
140
+ }
141
+
142
+ // Bail if disabled or no basic support
143
+ // You may want to disable certain UAs etc
144
+ if (!support.check().api) {
145
+ this.debug.error('Setup failed: no support');
146
+ return;
147
+ }
148
+
149
+ // Cache original element state for .destroy()
150
+ const clone = this.media.cloneNode(true);
151
+ clone.autoplay = false;
152
+ this.elements.original = clone;
153
+
154
+ // Set media type based on tag or data attribute
155
+ // Supported: video, audio, vimeo, youtube
156
+ const type = this.media.tagName.toLowerCase();
157
+ // Embed properties
158
+ let iframe = null;
159
+ let url = null;
160
+
161
+ // Different setup based on type
162
+ switch (type) {
163
+ case 'div':
164
+ // Find the frame
165
+ iframe = this.media.querySelector('iframe');
166
+
167
+ // <iframe> type
168
+ if (is.element(iframe)) {
169
+ // Detect provider
170
+ url = parseUrl(iframe.getAttribute('src'));
171
+ this.provider = getProviderByUrl(url.toString());
172
+
173
+ // Rework elements
174
+ this.elements.container = this.media;
175
+ this.media = iframe;
176
+
177
+ // Reset classname
178
+ this.elements.container.className = '';
179
+
180
+ // Get attributes from URL and set config
181
+ if (url.search.length) {
182
+ const truthy = ['1', 'true'];
183
+
184
+ if (truthy.includes(url.searchParams.get('autoplay'))) {
185
+ this.config.autoplay = true;
186
+ }
187
+ if (truthy.includes(url.searchParams.get('loop'))) {
188
+ this.config.loop.active = true;
189
+ }
190
+
191
+ // TODO: replace fullscreen.iosNative with this playsinline config option
192
+ // YouTube requires the playsinline in the URL
193
+ if (this.isYouTube) {
194
+ this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
195
+ this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
196
+ }
197
+ else {
198
+ this.config.playsinline = true;
199
+ }
200
+ }
201
+ }
202
+ else {
203
+ // <div> with attributes
204
+ this.provider = this.media.getAttribute(this.config.attributes.embed.provider);
205
+
206
+ // Remove attribute
207
+ this.media.removeAttribute(this.config.attributes.embed.provider);
208
+ }
209
+
210
+ // Unsupported or missing provider
211
+ if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {
212
+ this.debug.error('Setup failed: Invalid provider');
213
+ return;
214
+ }
215
+
216
+ // Audio will come later for external providers
217
+ this.type = types.video;
218
+
219
+ break;
220
+
221
+ case 'video':
222
+ case 'audio':
223
+ this.type = type;
224
+ this.provider = providers.html5;
225
+
226
+ // Get config from attributes
227
+ if (this.media.hasAttribute('crossorigin')) {
228
+ this.config.crossorigin = true;
229
+ }
230
+ if (this.media.hasAttribute('autoplay')) {
231
+ this.config.autoplay = true;
232
+ }
233
+ if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
234
+ this.config.playsinline = true;
235
+ }
236
+ if (this.media.hasAttribute('muted')) {
237
+ this.config.muted = true;
238
+ }
239
+ if (this.media.hasAttribute('loop')) {
240
+ this.config.loop.active = true;
241
+ }
242
+
243
+ break;
244
+
245
+ default:
246
+ this.debug.error('Setup failed: unsupported type');
247
+ return;
248
+ }
249
+
250
+ // Check for support again but with type
251
+ this.supported = support.check(this.type, this.provider);
252
+
253
+ // If no support for even API, bail
254
+ if (!this.supported.api) {
255
+ this.debug.error('Setup failed: no support');
256
+ return;
257
+ }
258
+
259
+ this.eventListeners = [];
260
+
261
+ // Create listeners
262
+ this.listeners = new Listeners(this);
263
+
264
+ // Setup local storage for user settings
265
+ this.storage = new Storage(this);
266
+
267
+ // Store reference
268
+ this.media.plyr = this;
269
+
270
+ // Wrap media
271
+ if (!is.element(this.elements.container)) {
272
+ this.elements.container = createElement('div');
273
+ wrap(this.media, this.elements.container);
274
+ }
275
+
276
+ // Migrate custom properties from media to container (so they work 😉)
277
+ ui.migrateStyles.call(this);
278
+
279
+ // Add style hook
280
+ ui.addStyleHook.call(this);
281
+
282
+ // Setup media
283
+ media.setup.call(this);
284
+
285
+ // Listen for events if debugging
286
+ if (this.config.debug) {
287
+ on.call(this, this.elements.container, this.config.events.join(' '), (event) => {
288
+ this.debug.log(`event: ${event.type}`);
289
+ });
290
+ }
291
+
292
+ // Setup fullscreen
293
+ this.fullscreen = new Fullscreen(this);
294
+
295
+ // Setup interface
296
+ // If embed but not fully supported, build interface now to avoid flash of controls
297
+ if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
298
+ ui.build.call(this);
299
+ }
300
+
301
+ // Container listeners
302
+ this.listeners.container();
303
+
304
+ // Global listeners
305
+ this.listeners.global();
306
+
307
+ // Setup ads if provided
308
+ if (this.config.ads.enabled) {
309
+ this.ads = new Ads(this);
310
+ }
311
+
312
+ // Autoplay if required
313
+ if (this.isHTML5 && this.config.autoplay) {
314
+ this.once('canplay', () => silencePromise(this.play()));
315
+ }
316
+
317
+ // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
318
+ this.lastSeekTime = 0;
319
+
320
+ // Setup preview thumbnails if enabled
321
+ if (this.config.previewThumbnails.enabled) {
322
+ this.previewThumbnails = new PreviewThumbnails(this);
323
+ }
324
+ }
325
+
326
+ // ---------------------------------------
327
+ // API
328
+ // ---------------------------------------
329
+
330
+ /**
331
+ * Types and provider helpers
332
+ */
333
+ get isHTML5() {
334
+ return this.provider === providers.html5;
335
+ }
336
+
337
+ get isEmbed() {
338
+ return this.isYouTube || this.isVimeo;
339
+ }
340
+
341
+ get isYouTube() {
342
+ return this.provider === providers.youtube;
343
+ }
344
+
345
+ get isVimeo() {
346
+ return this.provider === providers.vimeo;
347
+ }
348
+
349
+ get isVideo() {
350
+ return this.type === types.video;
351
+ }
352
+
353
+ get isAudio() {
354
+ return this.type === types.audio;
355
+ }
356
+
357
+ /**
358
+ * Play the media, or play the advertisement (if they are not blocked)
359
+ */
360
+ play = () => {
361
+ if (!is.function(this.media.play)) {
362
+ return null;
363
+ }
364
+
365
+ // Intecept play with ads
366
+ if (this.ads && this.ads.enabled) {
367
+ this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));
368
+ }
369
+
370
+ // Return the promise (for HTML5)
371
+ return this.media.play();
372
+ };
373
+
374
+ /**
375
+ * Pause the media
376
+ */
377
+ pause = () => {
378
+ if (!this.playing || !is.function(this.media.pause)) {
379
+ return null;
380
+ }
381
+
382
+ return this.media.pause();
383
+ };
384
+
385
+ /**
386
+ * Get playing state
387
+ */
388
+ get playing() {
389
+ return Boolean(this.ready && !this.paused && !this.ended);
390
+ }
391
+
392
+ /**
393
+ * Get paused state
394
+ */
395
+ get paused() {
396
+ return Boolean(this.media.paused);
397
+ }
398
+
399
+ /**
400
+ * Get stopped state
401
+ */
402
+ get stopped() {
403
+ return Boolean(this.paused && this.currentTime === 0);
404
+ }
405
+
406
+ /**
407
+ * Get ended state
408
+ */
409
+ get ended() {
410
+ return Boolean(this.media.ended);
411
+ }
412
+
413
+ /**
414
+ * Toggle playback based on current status
415
+ * @param {boolean} input
416
+ */
417
+ togglePlay = (input) => {
418
+ // Toggle based on current state if nothing passed
419
+ const toggle = is.boolean(input) ? input : !this.playing;
420
+
421
+ if (toggle) {
422
+ return this.play();
423
+ }
424
+
425
+ return this.pause();
426
+ };
427
+
428
+ /**
429
+ * Stop playback
430
+ */
431
+ stop = () => {
432
+ if (this.isHTML5) {
433
+ this.pause();
434
+ this.restart();
435
+ }
436
+ else if (is.function(this.media.stop)) {
437
+ this.media.stop();
438
+ }
439
+ };
440
+
441
+ /**
442
+ * Restart playback
443
+ */
444
+ restart = () => {
445
+ this.currentTime = 0;
446
+ };
447
+
448
+ /**
449
+ * Rewind
450
+ * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
451
+ */
452
+ rewind = (seekTime) => {
453
+ this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;
454
+ };
455
+
456
+ /**
457
+ * Fast forward
458
+ * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
459
+ */
460
+ forward = (seekTime) => {
461
+ this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;
462
+ };
463
+
464
+ /**
465
+ * Seek to a time
466
+ * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
467
+ */
468
+ set currentTime(input) {
469
+ // Bail if media duration isn't available yet
470
+ if (!this.duration) {
471
+ return;
472
+ }
473
+
474
+ // Validate input
475
+ const inputIsValid = is.number(input) && input > 0;
476
+
477
+ // Set
478
+ this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
479
+
480
+ // Logging
481
+ this.debug.log(`Seeking to ${this.currentTime} seconds`);
482
+ }
483
+
484
+ /**
485
+ * Get current time
486
+ */
487
+ get currentTime() {
488
+ return Number(this.media.currentTime);
489
+ }
490
+
491
+ /**
492
+ * Get buffered
493
+ */
494
+ get buffered() {
495
+ const { buffered } = this.media;
496
+
497
+ // YouTube / Vimeo return a float between 0-1
498
+ if (is.number(buffered)) {
499
+ return buffered;
500
+ }
501
+
502
+ // HTML5
503
+ // TODO: Handle buffered chunks of the media
504
+ // (i.e. seek to another section buffers only that section)
505
+ if (buffered && buffered.length && this.duration > 0) {
506
+ return buffered.end(0) / this.duration;
507
+ }
508
+
509
+ return 0;
510
+ }
511
+
512
+ /**
513
+ * Get seeking status
514
+ */
515
+ get seeking() {
516
+ return Boolean(this.media.seeking);
517
+ }
518
+
519
+ /**
520
+ * Get the duration of the current media
521
+ */
522
+ get duration() {
523
+ // Faux duration set via config
524
+ const fauxDuration = Number.parseFloat(this.config.duration);
525
+ // Media duration can be NaN or Infinity before the media has loaded
526
+ const realDuration = (this.media || {}).duration;
527
+ const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
528
+
529
+ // If config duration is funky, use regular duration
530
+ return fauxDuration || duration;
531
+ }
532
+
533
+ /**
534
+ * Set the player volume
535
+ * @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
536
+ */
537
+ set volume(value) {
538
+ let volume = value;
539
+ const max = 1;
540
+ const min = 0;
541
+
542
+ if (is.string(volume)) {
543
+ volume = Number(volume);
544
+ }
545
+
546
+ // Load volume from storage if no value specified
547
+ if (!is.number(volume)) {
548
+ volume = this.storage.get('volume');
549
+ }
550
+
551
+ // Use config if all else fails
552
+ if (!is.number(volume)) {
553
+ ({ volume } = this.config);
554
+ }
555
+
556
+ // Maximum is volumeMax
557
+ if (volume > max) {
558
+ volume = max;
559
+ }
560
+ // Minimum is volumeMin
561
+ if (volume < min) {
562
+ volume = min;
563
+ }
564
+
565
+ // Update config
566
+ this.config.volume = volume;
567
+
568
+ // Set the player volume
569
+ this.media.volume = volume;
570
+
571
+ // If muted, and we're increasing volume manually, reset muted state
572
+ if (!is.empty(value) && this.muted && volume > 0) {
573
+ this.muted = false;
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Get the current player volume
579
+ */
580
+ get volume() {
581
+ return Number(this.media.volume);
582
+ }
583
+
584
+ /**
585
+ * Increase volume
586
+ * @param {boolean} step - How much to decrease by (between 0 and 1)
587
+ */
588
+ increaseVolume = (step) => {
589
+ const volume = this.media.muted ? 0 : this.volume;
590
+ this.volume = volume + (is.number(step) ? step : 0);
591
+ };
592
+
593
+ /**
594
+ * Decrease volume
595
+ * @param {boolean} step - How much to decrease by (between 0 and 1)
596
+ */
597
+ decreaseVolume = (step) => {
598
+ this.increaseVolume(-step);
599
+ };
600
+
601
+ /**
602
+ * Set muted state
603
+ * @param {boolean} mute
604
+ */
605
+ set muted(mute) {
606
+ let toggle = mute;
607
+
608
+ // Load muted state from storage
609
+ if (!is.boolean(toggle)) {
610
+ toggle = this.storage.get('muted');
611
+ }
612
+
613
+ // Use config if all else fails
614
+ if (!is.boolean(toggle)) {
615
+ toggle = this.config.muted;
616
+ }
617
+
618
+ // Update config
619
+ this.config.muted = toggle;
620
+
621
+ // Set mute on the player
622
+ this.media.muted = toggle;
623
+ }
624
+
625
+ /**
626
+ * Get current muted state
627
+ */
628
+ get muted() {
629
+ return Boolean(this.media.muted);
630
+ }
631
+
632
+ /**
633
+ * Check if the media has audio
634
+ */
635
+ get hasAudio() {
636
+ // Assume yes for all non HTML5 (as we can't tell...)
637
+ if (!this.isHTML5) {
638
+ return true;
639
+ }
640
+
641
+ if (this.isAudio) {
642
+ return true;
643
+ }
644
+
645
+ // Get audio tracks
646
+ return (
647
+ Boolean(this.media.mozHasAudio)
648
+ || Boolean(this.media.webkitAudioDecodedByteCount)
649
+ || Boolean(this.media.audioTracks && this.media.audioTracks.length)
650
+ );
651
+ }
652
+
653
+ /**
654
+ * Set playback speed
655
+ * @param {number} input - the speed of playback (0.5-2.0)
656
+ */
657
+ set speed(input) {
658
+ let speed = null;
659
+
660
+ if (is.number(input)) {
661
+ speed = input;
662
+ }
663
+
664
+ if (!is.number(speed)) {
665
+ speed = this.storage.get('speed');
666
+ }
667
+
668
+ if (!is.number(speed)) {
669
+ speed = this.config.speed.selected;
670
+ }
671
+
672
+ // Clamp to min/max
673
+ const { minimumSpeed: min, maximumSpeed: max } = this;
674
+ speed = clamp(speed, min, max);
675
+
676
+ // Update config
677
+ this.config.speed.selected = speed;
678
+
679
+ // Set media speed
680
+ setTimeout(() => {
681
+ if (this.media) {
682
+ this.media.playbackRate = speed;
683
+ }
684
+ }, 0);
685
+ }
686
+
687
+ /**
688
+ * Get current playback speed
689
+ */
690
+ get speed() {
691
+ return Number(this.media.playbackRate);
692
+ }
693
+
694
+ /**
695
+ * Get the minimum allowed speed
696
+ */
697
+ get minimumSpeed() {
698
+ if (this.isYouTube) {
699
+ // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
700
+ return Math.min(...this.options.speed);
701
+ }
702
+
703
+ if (this.isVimeo) {
704
+ // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
705
+ return 0.5;
706
+ }
707
+
708
+ // https://stackoverflow.com/a/32320020/1191319
709
+ return 0.0625;
710
+ }
711
+
712
+ /**
713
+ * Get the maximum allowed speed
714
+ */
715
+ get maximumSpeed() {
716
+ if (this.isYouTube) {
717
+ // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
718
+ return Math.max(...this.options.speed);
719
+ }
720
+
721
+ if (this.isVimeo) {
722
+ // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
723
+ return 2;
724
+ }
725
+
726
+ // https://stackoverflow.com/a/32320020/1191319
727
+ return 16;
728
+ }
729
+
730
+ /**
731
+ * Set playback quality
732
+ * Currently HTML5 & YouTube only
733
+ * @param {number} input - Quality level
734
+ */
735
+ set quality(input) {
736
+ const config = this.config.quality;
737
+ const options = this.options.quality;
738
+
739
+ if (!options.length) {
740
+ return;
741
+ }
742
+
743
+ let quality = [
744
+ !is.empty(input) && Number(input),
745
+ this.storage.get('quality'),
746
+ config.selected,
747
+ config.default,
748
+ ].find(is.number);
749
+
750
+ let updateStorage = true;
751
+
752
+ if (!options.includes(quality)) {
753
+ const value = closest(options, quality);
754
+ this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
755
+ quality = value;
756
+
757
+ // Don't update storage if quality is not supported
758
+ updateStorage = false;
759
+ }
760
+
761
+ // Update config
762
+ config.selected = quality;
763
+
764
+ // Set quality
765
+ this.media.quality = quality;
766
+
767
+ // Save to storage
768
+ if (updateStorage) {
769
+ this.storage.set({ quality });
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Get current quality level
775
+ */
776
+ get quality() {
777
+ return this.media.quality;
778
+ }
779
+
780
+ /**
781
+ * Toggle loop
782
+ * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
783
+ * @param {boolean} input - Whether to loop or not
784
+ */
785
+ set loop(input) {
786
+ const toggle = is.boolean(input) ? input : this.config.loop.active;
787
+ this.config.loop.active = toggle;
788
+ this.media.loop = toggle;
789
+
790
+ // Set default to be a true toggle
791
+ /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
792
+
793
+ switch (type) {
794
+ case 'start':
795
+ if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
796
+ this.config.loop.end = null;
797
+ }
798
+ this.config.loop.start = this.currentTime;
799
+ // this.config.loop.indicator.start = this.elements.display.played.value;
800
+ break;
801
+
802
+ case 'end':
803
+ if (this.config.loop.start >= this.currentTime) {
804
+ return this;
805
+ }
806
+ this.config.loop.end = this.currentTime;
807
+ // this.config.loop.indicator.end = this.elements.display.played.value;
808
+ break;
809
+
810
+ case 'all':
811
+ this.config.loop.start = 0;
812
+ this.config.loop.end = this.duration - 2;
813
+ this.config.loop.indicator.start = 0;
814
+ this.config.loop.indicator.end = 100;
815
+ break;
816
+
817
+ case 'toggle':
818
+ if (this.config.loop.active) {
819
+ this.config.loop.start = 0;
820
+ this.config.loop.end = null;
821
+ } else {
822
+ this.config.loop.start = 0;
823
+ this.config.loop.end = this.duration - 2;
824
+ }
825
+ break;
826
+
827
+ default:
828
+ this.config.loop.start = 0;
829
+ this.config.loop.end = null;
830
+ break;
831
+ } */
832
+ }
833
+
834
+ /**
835
+ * Get current loop state
836
+ */
837
+ get loop() {
838
+ return Boolean(this.media.loop);
839
+ }
840
+
841
+ /**
842
+ * Set new media source
843
+ * @param {object} input - The new source object (see docs)
844
+ */
845
+ set source(input) {
846
+ source.change.call(this, input);
847
+ }
848
+
849
+ /**
850
+ * Get current source
851
+ */
852
+ get source() {
853
+ return this.media.currentSrc;
854
+ }
855
+
856
+ /**
857
+ * Get a download URL (either source or custom)
858
+ */
859
+ get download() {
860
+ const { download } = this.config.urls;
861
+
862
+ return is.url(download) ? download : this.source;
863
+ }
864
+
865
+ /**
866
+ * Set the download URL
867
+ */
868
+ set download(input) {
869
+ if (!is.url(input)) {
870
+ return;
871
+ }
872
+
873
+ this.config.urls.download = input;
874
+
875
+ controls.setDownloadUrl.call(this);
876
+ }
877
+
878
+ /**
879
+ * Set the poster image for a video
880
+ * @param {string} input - the URL for the new poster image
881
+ */
882
+ set poster(input) {
883
+ if (!this.isVideo) {
884
+ this.debug.warn('Poster can only be set for video');
885
+ return;
886
+ }
887
+
888
+ ui.setPoster.call(this, input, false).catch(() => {});
889
+ }
890
+
891
+ /**
892
+ * Get the current poster image
893
+ */
894
+ get poster() {
895
+ if (!this.isVideo) {
896
+ return null;
897
+ }
898
+
899
+ return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
900
+ }
901
+
902
+ /**
903
+ * Get the current aspect ratio in use
904
+ */
905
+ get ratio() {
906
+ if (!this.isVideo) {
907
+ return null;
908
+ }
909
+
910
+ const ratio = reduceAspectRatio(getAspectRatio.call(this));
911
+
912
+ return is.array(ratio) ? ratio.join(':') : ratio;
913
+ }
914
+
915
+ /**
916
+ * Set video aspect ratio
917
+ */
918
+ set ratio(input) {
919
+ if (!this.isVideo) {
920
+ this.debug.warn('Aspect ratio can only be set for video');
921
+ return;
922
+ }
923
+
924
+ if (!is.string(input) || !validateAspectRatio(input)) {
925
+ this.debug.error(`Invalid aspect ratio specified (${input})`);
926
+ return;
927
+ }
928
+
929
+ this.config.ratio = reduceAspectRatio(input);
930
+
931
+ setAspectRatio.call(this);
932
+ }
933
+
934
+ /**
935
+ * Set the autoplay state
936
+ * @param {boolean} input - Whether to autoplay or not
937
+ */
938
+ set autoplay(input) {
939
+ this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;
940
+ }
941
+
942
+ /**
943
+ * Get the current autoplay state
944
+ */
945
+ get autoplay() {
946
+ return Boolean(this.config.autoplay);
947
+ }
948
+
949
+ /**
950
+ * Toggle captions
951
+ * @param {boolean} input - Whether to enable captions
952
+ */
953
+ toggleCaptions(input) {
954
+ captions.toggle.call(this, input, false);
955
+ }
956
+
957
+ /**
958
+ * Set the caption track by index
959
+ * @param {number} input - Caption index
960
+ */
961
+ set currentTrack(input) {
962
+ captions.set.call(this, input, false);
963
+ captions.setup.call(this);
964
+ }
965
+
966
+ /**
967
+ * Get the current caption track index (-1 if disabled)
968
+ */
969
+ get currentTrack() {
970
+ const { toggled, currentTrack } = this.captions;
971
+ return toggled ? currentTrack : -1;
972
+ }
973
+
974
+ /**
975
+ * Set the wanted language for captions
976
+ * Since tracks can be added later it won't update the actual caption track until there is a matching track
977
+ * @param {string} input - Two character ISO language code (e.g. EN, FR, PT, etc)
978
+ */
979
+ set language(input) {
980
+ captions.setLanguage.call(this, input, false);
981
+ }
982
+
983
+ /**
984
+ * Get the current track's language
985
+ */
986
+ get language() {
987
+ return (captions.getCurrentTrack.call(this) || {}).language;
988
+ }
989
+
990
+ /**
991
+ * Toggle picture-in-picture playback on WebKit/MacOS
992
+ * TODO: update player with state, support, enabled
993
+ * TODO: detect outside changes
994
+ */
995
+ set pip(input) {
996
+ // Bail if no support
997
+ if (!support.pip) {
998
+ return;
999
+ }
1000
+
1001
+ // Toggle based on current state if not passed
1002
+ const toggle = is.boolean(input) ? input : !this.pip;
1003
+
1004
+ // Toggle based on current state
1005
+ // Safari
1006
+ if (is.function(this.media.webkitSetPresentationMode)) {
1007
+ this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
1008
+ }
1009
+
1010
+ // Chrome
1011
+ if (is.function(this.media.requestPictureInPicture)) {
1012
+ if (!this.pip && toggle) {
1013
+ this.media.requestPictureInPicture();
1014
+ }
1015
+ else if (this.pip && !toggle) {
1016
+ document.exitPictureInPicture();
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ /**
1022
+ * Get the current picture-in-picture state
1023
+ */
1024
+ get pip() {
1025
+ if (!support.pip) {
1026
+ return null;
1027
+ }
1028
+
1029
+ // Safari
1030
+ if (!is.empty(this.media.webkitPresentationMode)) {
1031
+ return this.media.webkitPresentationMode === pip.active;
1032
+ }
1033
+
1034
+ // Chrome
1035
+ return this.media === document.pictureInPictureElement;
1036
+ }
1037
+
1038
+ /**
1039
+ * Sets the preview thumbnails for the current source
1040
+ */
1041
+ setPreviewThumbnails(thumbnailSource) {
1042
+ if (this.previewThumbnails && this.previewThumbnails.loaded) {
1043
+ this.previewThumbnails.destroy();
1044
+ this.previewThumbnails = null;
1045
+ }
1046
+
1047
+ Object.assign(this.config.previewThumbnails, thumbnailSource);
1048
+
1049
+ // Create new instance if it is still enabled
1050
+ if (this.config.previewThumbnails.enabled) {
1051
+ this.previewThumbnails = new PreviewThumbnails(this);
1052
+ }
1053
+ }
1054
+
1055
+ /**
1056
+ * Trigger the airplay dialog
1057
+ * TODO: update player with state, support, enabled
1058
+ */
1059
+ airplay = () => {
1060
+ // Show dialog if supported
1061
+ if (support.airplay) {
1062
+ this.media.webkitShowPlaybackTargetPicker();
1063
+ }
1064
+ };
1065
+
1066
+ /**
1067
+ * Toggle the player controls
1068
+ * @param {boolean} [toggle] - Whether to show the controls
1069
+ */
1070
+ toggleControls = (toggle) => {
1071
+ // Don't toggle if missing UI support or if it's audio
1072
+ if (this.supported.ui && !this.isAudio) {
1073
+ // Get state before change
1074
+ const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
1075
+ // Negate the argument if not undefined since adding the class to hides the controls
1076
+ const force = typeof toggle === 'undefined' ? undefined : !toggle;
1077
+ // Apply and get updated state
1078
+ const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
1079
+
1080
+ // Close menu
1081
+ if (
1082
+ hiding
1083
+ && is.array(this.config.controls)
1084
+ && this.config.controls.includes('settings')
1085
+ && !is.empty(this.config.settings)
1086
+ ) {
1087
+ controls.toggleMenu.call(this, false);
1088
+ }
1089
+
1090
+ // Trigger event on change
1091
+ if (hiding !== isHidden) {
1092
+ const eventName = hiding ? 'controlshidden' : 'controlsshown';
1093
+ triggerEvent.call(this, this.media, eventName);
1094
+ }
1095
+
1096
+ return !hiding;
1097
+ }
1098
+
1099
+ return false;
1100
+ };
1101
+
1102
+ /**
1103
+ * Add event listeners
1104
+ * @param {string} event - Event type
1105
+ * @param {Function} callback - Callback for when event occurs
1106
+ */
1107
+ on = (event, callback) => {
1108
+ on.call(this, this.elements.container, event, callback);
1109
+ };
1110
+
1111
+ /**
1112
+ * Add event listeners once
1113
+ * @param {string} event - Event type
1114
+ * @param {Function} callback - Callback for when event occurs
1115
+ */
1116
+ once = (event, callback) => {
1117
+ once.call(this, this.elements.container, event, callback);
1118
+ };
1119
+
1120
+ /**
1121
+ * Remove event listeners
1122
+ * @param {string} event - Event type
1123
+ * @param {Function} callback - Callback for when event occurs
1124
+ */
1125
+ off = (event, callback) => {
1126
+ off(this.elements.container, event, callback);
1127
+ };
1128
+
1129
+ /**
1130
+ * Destroy an instance
1131
+ * Event listeners are removed when elements are removed
1132
+ * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
1133
+ * @param {Function} callback - Callback for when destroy is complete
1134
+ * @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
1135
+ */
1136
+ destroy = (callback, soft = false) => {
1137
+ if (!this.ready) {
1138
+ return;
1139
+ }
1140
+
1141
+ const done = () => {
1142
+ // Reset overflow (incase destroyed while in fullscreen)
1143
+ document.body.style.overflow = '';
1144
+
1145
+ // GC for embed
1146
+ this.embed = null;
1147
+
1148
+ // If it's a soft destroy, make minimal changes
1149
+ if (soft) {
1150
+ if (Object.keys(this.elements).length) {
1151
+ // Remove elements
1152
+ removeElement(this.elements.buttons.play);
1153
+ removeElement(this.elements.captions);
1154
+ removeElement(this.elements.controls);
1155
+ removeElement(this.elements.wrapper);
1156
+
1157
+ // Clear for GC
1158
+ this.elements.buttons.play = null;
1159
+ this.elements.captions = null;
1160
+ this.elements.controls = null;
1161
+ this.elements.wrapper = null;
1162
+ }
1163
+
1164
+ // Callback
1165
+ if (is.function(callback)) {
1166
+ callback();
1167
+ }
1168
+ }
1169
+ else {
1170
+ // Unbind listeners
1171
+ unbindListeners.call(this);
1172
+
1173
+ // Cancel current network requests
1174
+ html5.cancelRequests.call(this);
1175
+
1176
+ // Replace the container with the original element provided
1177
+ replaceElement(this.elements.original, this.elements.container);
1178
+
1179
+ // Event
1180
+ triggerEvent.call(this, this.elements.original, 'destroyed', true);
1181
+
1182
+ // Callback
1183
+ if (is.function(callback)) {
1184
+ callback.call(this.elements.original);
1185
+ }
1186
+
1187
+ // Reset state
1188
+ this.ready = false;
1189
+
1190
+ // Clear for garbage collection
1191
+ setTimeout(() => {
1192
+ this.elements = null;
1193
+ this.media = null;
1194
+ }, 200);
1195
+ }
1196
+ };
1197
+
1198
+ // Stop playback
1199
+ this.stop();
1200
+
1201
+ // Clear timeouts
1202
+ clearTimeout(this.timers.loading);
1203
+ clearTimeout(this.timers.controls);
1204
+ clearTimeout(this.timers.resized);
1205
+
1206
+ // Provider specific stuff
1207
+ if (this.isHTML5) {
1208
+ // Restore native video controls
1209
+ ui.toggleNativeControls.call(this, true);
1210
+
1211
+ // Clean up
1212
+ done();
1213
+ }
1214
+ else if (this.isYouTube) {
1215
+ // Clear timers
1216
+ clearInterval(this.timers.buffering);
1217
+ clearInterval(this.timers.playing);
1218
+
1219
+ // Destroy YouTube API
1220
+ if (this.embed !== null && is.function(this.embed.destroy)) {
1221
+ this.embed.destroy();
1222
+ }
1223
+
1224
+ // Clean up
1225
+ done();
1226
+ }
1227
+ else if (this.isVimeo) {
1228
+ // Destroy Vimeo API
1229
+ // then clean up (wait, to prevent postmessage errors)
1230
+ if (this.embed !== null) {
1231
+ this.embed.unload().then(done);
1232
+ }
1233
+
1234
+ // Vimeo does not always return
1235
+ setTimeout(done, 200);
1236
+ }
1237
+ };
1238
+
1239
+ /**
1240
+ * Check for support for a mime type (HTML5 only)
1241
+ * @param {string} type - Mime type
1242
+ */
1243
+ supports = type => support.mime.call(this, type);
1244
+
1245
+ /**
1246
+ * Check for support
1247
+ * @param {string} type - Player type (audio/video)
1248
+ * @param {string} provider - Provider (html5/youtube/vimeo)
1249
+ */
1250
+ static supported(type, provider) {
1251
+ return support.check(type, provider);
1252
+ }
1253
+
1254
+ /**
1255
+ * Load an SVG sprite into the page
1256
+ * @param {string} url - URL for the SVG sprite
1257
+ * @param {string} [id] - Unique ID
1258
+ */
1259
+ static loadSprite(url, id) {
1260
+ return loadSprite(url, id);
1261
+ }
1262
+
1263
+ /**
1264
+ * Setup multiple instances
1265
+ * @param {*} selector
1266
+ * @param {object} options
1267
+ */
1268
+ static setup(selector, options = {}) {
1269
+ let targets = null;
1270
+
1271
+ if (is.string(selector)) {
1272
+ targets = Array.from(document.querySelectorAll(selector));
1273
+ }
1274
+ else if (is.nodeList(selector)) {
1275
+ targets = Array.from(selector);
1276
+ }
1277
+ else if (is.array(selector)) {
1278
+ targets = selector.filter(is.element);
1279
+ }
1280
+
1281
+ if (is.empty(targets)) {
1282
+ return null;
1283
+ }
1284
+
1285
+ return targets.map(t => new Plyr(t, options));
1286
+ }
1287
+ }
1288
+
1289
+ Plyr.defaults = cloneDeep(defaults);
1290
+
1291
+ export default Plyr;