@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
@@ -0,0 +1,451 @@
1
+ // ==========================================================================
2
+ // YouTube plugin
3
+ // ==========================================================================
4
+
5
+ import ui from '../ui';
6
+ import { createElement, replaceElement, toggleClass } from '../utils/elements';
7
+ import { triggerEvent } from '../utils/events';
8
+ import fetch from '../utils/fetch';
9
+ import is from '../utils/is';
10
+ import loadImage from '../utils/load-image';
11
+ import loadScript from '../utils/load-script';
12
+ import { extend } from '../utils/objects';
13
+ import { format, generateId } from '../utils/strings';
14
+ import { roundAspectRatio, setAspectRatio } from '../utils/style';
15
+
16
+ // Parse YouTube ID from URL
17
+ function parseId(url) {
18
+ if (is.empty(url)) {
19
+ return null;
20
+ }
21
+
22
+ const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
23
+ const match = url.match(regex);
24
+ return match && match[2] ? match[2] : url;
25
+ }
26
+
27
+ // Set playback state and trigger change (only on actual change)
28
+ function assurePlaybackState(play) {
29
+ if (play && !this.embed.hasPlayed) {
30
+ this.embed.hasPlayed = true;
31
+ }
32
+ if (this.media.paused === play) {
33
+ this.media.paused = !play;
34
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
35
+ }
36
+ }
37
+
38
+ function getHost(config) {
39
+ if (config.noCookie) {
40
+ return 'https://www.youtube-nocookie.com';
41
+ }
42
+
43
+ if (window.location.protocol === 'http:') {
44
+ return 'http://www.youtube.com';
45
+ }
46
+
47
+ // Use YouTube's default
48
+ return undefined;
49
+ }
50
+
51
+ const youtube = {
52
+ setup() {
53
+ // Add embed class for responsive
54
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
55
+
56
+ // Setup API
57
+ if (is.object(window.YT) && is.function(window.YT.Player)) {
58
+ youtube.ready.call(this);
59
+ }
60
+ else {
61
+ // Reference current global callback
62
+ const callback = window.onYouTubeIframeAPIReady;
63
+
64
+ // Set callback to process queue
65
+ window.onYouTubeIframeAPIReady = () => {
66
+ // Call global callback if set
67
+ if (is.function(callback)) {
68
+ callback();
69
+ }
70
+
71
+ youtube.ready.call(this);
72
+ };
73
+
74
+ // Load the SDK
75
+ loadScript(this.config.urls.youtube.sdk).catch((error) => {
76
+ this.debug.warn('YouTube API failed to load', error);
77
+ });
78
+ }
79
+ },
80
+
81
+ // Get the media title
82
+ getTitle(videoId) {
83
+ const url = format(this.config.urls.youtube.api, videoId);
84
+
85
+ fetch(url)
86
+ .then((data) => {
87
+ if (is.object(data)) {
88
+ const { title, height, width } = data;
89
+
90
+ // Set title
91
+ this.config.title = title;
92
+ ui.setTitle.call(this);
93
+
94
+ // Set aspect ratio
95
+ this.embed.ratio = roundAspectRatio(width, height);
96
+ }
97
+
98
+ setAspectRatio.call(this);
99
+ })
100
+ .catch(() => {
101
+ // Set aspect ratio
102
+ setAspectRatio.call(this);
103
+ });
104
+ },
105
+
106
+ // API ready
107
+ ready() {
108
+ const player = this;
109
+ const config = player.config.youtube;
110
+ // Ignore already setup (race condition)
111
+ const currentId = player.media && player.media.getAttribute('id');
112
+ if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
113
+ return;
114
+ }
115
+
116
+ // Get the source URL or ID
117
+ let source = player.media.getAttribute('src');
118
+
119
+ // Get from <div> if needed
120
+ if (is.empty(source)) {
121
+ source = player.media.getAttribute(this.config.attributes.embed.id);
122
+ }
123
+
124
+ // Replace the <iframe> with a <div> due to YouTube API issues
125
+ const videoId = parseId(source);
126
+ const id = generateId(player.provider);
127
+ // Replace media element
128
+ const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });
129
+ player.media = replaceElement(container, player.media);
130
+
131
+ // Only load the poster when using custom controls
132
+ if (config.customControls) {
133
+ const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
134
+
135
+ // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
136
+ loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded
137
+ .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
138
+ .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
139
+ .then(image => ui.setPoster.call(player, image.src))
140
+ .then((src) => {
141
+ // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
142
+ if (!src.includes('maxres')) {
143
+ player.elements.poster.style.backgroundSize = 'cover';
144
+ }
145
+ })
146
+ .catch(() => {});
147
+ }
148
+
149
+ // Setup instance
150
+ // https://developers.google.com/youtube/iframe_api_reference
151
+ player.embed = new window.YT.Player(player.media, {
152
+ videoId,
153
+ host: getHost(config),
154
+ playerVars: extend(
155
+ {},
156
+ {
157
+ // Autoplay
158
+ autoplay: player.config.autoplay ? 1 : 0,
159
+ // iframe interface language
160
+ hl: player.config.hl,
161
+ // Only show controls if not fully supported or opted out
162
+ controls: player.supported.ui && config.customControls ? 0 : 1,
163
+ // Disable keyboard as we handle it
164
+ disablekb: 1,
165
+ // Allow iOS inline playback
166
+ playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
167
+ // Captions are flaky on YouTube
168
+ cc_load_policy: player.captions.active ? 1 : 0,
169
+ cc_lang_pref: player.config.captions.language,
170
+ // Tracking for stats
171
+ widget_referrer: window ? window.location.href : null,
172
+ },
173
+ config,
174
+ ),
175
+ events: {
176
+ onError(event) {
177
+ // YouTube may fire onError twice, so only handle it once
178
+ if (!player.media.error) {
179
+ const code = event.data;
180
+ // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
181
+ const message
182
+ = {
183
+ 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
184
+ 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
185
+ 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
186
+ 101: 'The owner of the requested video does not allow it to be played in embedded players.',
187
+ 150: 'The owner of the requested video does not allow it to be played in embedded players.',
188
+ }[code] || 'An unknown error occurred';
189
+
190
+ player.media.error = { code, message };
191
+
192
+ triggerEvent.call(player, player.media, 'error');
193
+ }
194
+ },
195
+ onPlaybackRateChange(event) {
196
+ // Get the instance
197
+ const instance = event.target;
198
+
199
+ // Get current speed
200
+ player.media.playbackRate = instance.getPlaybackRate();
201
+
202
+ triggerEvent.call(player, player.media, 'ratechange');
203
+ },
204
+ onReady(event) {
205
+ // Bail if onReady has already been called. See issue #1108
206
+ if (is.function(player.media.play)) {
207
+ return;
208
+ }
209
+ // Get the instance
210
+ const instance = event.target;
211
+
212
+ // Get the title
213
+ youtube.getTitle.call(player, videoId);
214
+
215
+ // Create a faux HTML5 API using the YouTube API
216
+ player.media.play = () => {
217
+ assurePlaybackState.call(player, true);
218
+ instance.playVideo();
219
+ };
220
+
221
+ player.media.pause = () => {
222
+ assurePlaybackState.call(player, false);
223
+ instance.pauseVideo();
224
+ };
225
+
226
+ player.media.stop = () => {
227
+ instance.stopVideo();
228
+ };
229
+
230
+ player.media.duration = instance.getDuration();
231
+ player.media.paused = true;
232
+
233
+ // Seeking
234
+ player.media.currentTime = 0;
235
+ Object.defineProperty(player.media, 'currentTime', {
236
+ get() {
237
+ return Number(instance.getCurrentTime());
238
+ },
239
+ set(time) {
240
+ // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
241
+ if (player.paused && !player.embed.hasPlayed) {
242
+ player.embed.mute();
243
+ }
244
+
245
+ // Set seeking state and trigger event
246
+ player.media.seeking = true;
247
+ triggerEvent.call(player, player.media, 'seeking');
248
+
249
+ // Seek after events sent
250
+ instance.seekTo(time);
251
+ },
252
+ });
253
+
254
+ // Playback speed
255
+ Object.defineProperty(player.media, 'playbackRate', {
256
+ get() {
257
+ return instance.getPlaybackRate();
258
+ },
259
+ set(input) {
260
+ instance.setPlaybackRate(input);
261
+ },
262
+ });
263
+
264
+ // Volume
265
+ let { volume } = player.config;
266
+ Object.defineProperty(player.media, 'volume', {
267
+ get() {
268
+ return volume;
269
+ },
270
+ set(input) {
271
+ volume = input;
272
+ instance.setVolume(volume * 100);
273
+ triggerEvent.call(player, player.media, 'volumechange');
274
+ },
275
+ });
276
+
277
+ // Muted
278
+ let { muted } = player.config;
279
+ Object.defineProperty(player.media, 'muted', {
280
+ get() {
281
+ return muted;
282
+ },
283
+ set(input) {
284
+ const toggle = is.boolean(input) ? input : muted;
285
+ muted = toggle;
286
+ instance[toggle ? 'mute' : 'unMute']();
287
+ instance.setVolume(volume * 100);
288
+ triggerEvent.call(player, player.media, 'volumechange');
289
+ },
290
+ });
291
+
292
+ // Source
293
+ Object.defineProperty(player.media, 'currentSrc', {
294
+ get() {
295
+ return instance.getVideoUrl();
296
+ },
297
+ });
298
+
299
+ // Ended
300
+ Object.defineProperty(player.media, 'ended', {
301
+ get() {
302
+ return player.currentTime === player.duration;
303
+ },
304
+ });
305
+
306
+ // Get available speeds
307
+ const speeds = instance.getAvailablePlaybackRates();
308
+ // Filter based on config
309
+ player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
310
+
311
+ // Set the tabindex to avoid focus entering iframe
312
+ if (player.supported.ui && config.customControls) {
313
+ player.media.setAttribute('tabindex', -1);
314
+ }
315
+
316
+ triggerEvent.call(player, player.media, 'timeupdate');
317
+ triggerEvent.call(player, player.media, 'durationchange');
318
+
319
+ // Reset timer
320
+ clearInterval(player.timers.buffering);
321
+
322
+ // Setup buffering
323
+ player.timers.buffering = setInterval(() => {
324
+ // Get loaded % from YouTube
325
+ player.media.buffered = instance.getVideoLoadedFraction();
326
+
327
+ // Trigger progress only when we actually buffer something
328
+ if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
329
+ triggerEvent.call(player, player.media, 'progress');
330
+ }
331
+
332
+ // Set last buffer point
333
+ player.media.lastBuffered = player.media.buffered;
334
+
335
+ // Bail if we're at 100%
336
+ if (player.media.buffered === 1) {
337
+ clearInterval(player.timers.buffering);
338
+
339
+ // Trigger event
340
+ triggerEvent.call(player, player.media, 'canplaythrough');
341
+ }
342
+ }, 200);
343
+
344
+ // Rebuild UI
345
+ if (config.customControls) {
346
+ setTimeout(() => ui.build.call(player), 50);
347
+ }
348
+ },
349
+ onStateChange(event) {
350
+ // Get the instance
351
+ const instance = event.target;
352
+
353
+ // Reset timer
354
+ clearInterval(player.timers.playing);
355
+
356
+ const seeked = player.media.seeking && [1, 2].includes(event.data);
357
+
358
+ if (seeked) {
359
+ // Unset seeking and fire seeked event
360
+ player.media.seeking = false;
361
+ triggerEvent.call(player, player.media, 'seeked');
362
+ }
363
+
364
+ // Handle events
365
+ // -1 Unstarted
366
+ // 0 Ended
367
+ // 1 Playing
368
+ // 2 Paused
369
+ // 3 Buffering
370
+ // 5 Video cued
371
+ switch (event.data) {
372
+ case -1:
373
+ // Update scrubber
374
+ triggerEvent.call(player, player.media, 'timeupdate');
375
+
376
+ // Get loaded % from YouTube
377
+ player.media.buffered = instance.getVideoLoadedFraction();
378
+ triggerEvent.call(player, player.media, 'progress');
379
+
380
+ break;
381
+
382
+ case 0:
383
+ assurePlaybackState.call(player, false);
384
+
385
+ // YouTube doesn't support loop for a single video, so mimick it.
386
+ if (player.media.loop) {
387
+ // YouTube needs a call to `stopVideo` before playing again
388
+ instance.stopVideo();
389
+ instance.playVideo();
390
+ }
391
+ else {
392
+ triggerEvent.call(player, player.media, 'ended');
393
+ }
394
+
395
+ break;
396
+
397
+ case 1:
398
+ // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
399
+ if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
400
+ player.media.pause();
401
+ }
402
+ else {
403
+ assurePlaybackState.call(player, true);
404
+
405
+ triggerEvent.call(player, player.media, 'playing');
406
+
407
+ // Poll to get playback progress
408
+ player.timers.playing = setInterval(() => {
409
+ triggerEvent.call(player, player.media, 'timeupdate');
410
+ }, 50);
411
+
412
+ // Check duration again due to YouTube bug
413
+ // https://github.com/xgauravyaduvanshii/redxplyr/issues/374
414
+ // https://code.google.com/p/gdata-issues/issues/detail?id=8690
415
+ if (player.media.duration !== instance.getDuration()) {
416
+ player.media.duration = instance.getDuration();
417
+ triggerEvent.call(player, player.media, 'durationchange');
418
+ }
419
+ }
420
+
421
+ break;
422
+
423
+ case 2:
424
+ // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
425
+ if (!player.muted) {
426
+ player.embed.unMute();
427
+ }
428
+ assurePlaybackState.call(player, false);
429
+
430
+ break;
431
+
432
+ case 3:
433
+ // Trigger waiting event to add loading classes to container as the video buffers.
434
+ triggerEvent.call(player, player.media, 'waiting');
435
+
436
+ break;
437
+
438
+ default:
439
+ break;
440
+ }
441
+
442
+ triggerEvent.call(player, player.elements.container, 'statechange', false, {
443
+ code: event.data,
444
+ });
445
+ },
446
+ },
447
+ });
448
+ },
449
+ };
450
+
451
+ export default youtube;