@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.
- package/.editorconfig +10 -0
- package/.gitpod.yml +6 -0
- package/.node-version +1 -0
- package/.prettierrc +7 -0
- package/.stickler.yml +5 -0
- package/.stylelintrc.json +26 -0
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +34 -0
- package/CONTROLS.md +49 -0
- package/Dockerfile +32 -0
- package/LICENSE.md +22 -0
- package/README.md +194 -0
- package/cspell.json +48 -0
- package/dist/redxplyr.css +1 -0
- package/dist/redxplyr.js +8801 -0
- package/dist/redxplyr.min.js +2 -0
- package/dist/redxplyr.min.js.map +1 -0
- package/dist/redxplyr.min.mjs +1 -0
- package/dist/redxplyr.min.mjs.map +1 -0
- package/dist/redxplyr.mjs +8793 -0
- package/dist/redxplyr.polyfilled.js +9294 -0
- package/dist/redxplyr.polyfilled.min.js +2 -0
- package/dist/redxplyr.polyfilled.min.js.map +1 -0
- package/dist/redxplyr.polyfilled.min.mjs +1 -0
- package/dist/redxplyr.polyfilled.min.mjs.map +1 -0
- package/dist/redxplyr.polyfilled.mjs +9286 -0
- package/dist/redxplyr.svg +1 -0
- package/eslint.config.mjs +39 -0
- package/gulpfile.js +8 -0
- package/package.json +114 -0
- package/pnpm-workspace.yaml +8 -0
- package/src/js/captions.js +411 -0
- package/src/js/config/defaults.js +459 -0
- package/src/js/config/states.js +10 -0
- package/src/js/config/types.js +34 -0
- package/src/js/console.js +28 -0
- package/src/js/controls.js +1870 -0
- package/src/js/fullscreen.js +305 -0
- package/src/js/html5.js +148 -0
- package/src/js/listeners.js +854 -0
- package/src/js/media.js +61 -0
- package/src/js/plugins/ads.js +647 -0
- package/src/js/plugins/preview-thumbnails.js +706 -0
- package/src/js/plugins/vimeo.js +443 -0
- package/src/js/plugins/youtube.js +451 -0
- package/src/js/plyr.d.ts +729 -0
- package/src/js/plyr.js +1291 -0
- package/src/js/plyr.polyfilled.js +13 -0
- package/src/js/source.js +155 -0
- package/src/js/storage.js +70 -0
- package/src/js/support.js +100 -0
- package/src/js/ui.js +297 -0
- package/src/js/utils/animation.js +33 -0
- package/src/js/utils/arrays.js +23 -0
- package/src/js/utils/browser.js +21 -0
- package/src/js/utils/elements.js +263 -0
- package/src/js/utils/events.js +116 -0
- package/src/js/utils/fetch.js +45 -0
- package/src/js/utils/i18n.js +47 -0
- package/src/js/utils/is.js +81 -0
- package/src/js/utils/load-image.js +19 -0
- package/src/js/utils/load-script.js +14 -0
- package/src/js/utils/load-sprite.js +77 -0
- package/src/js/utils/numbers.js +17 -0
- package/src/js/utils/objects.js +43 -0
- package/src/js/utils/promise.js +14 -0
- package/src/js/utils/strings.js +80 -0
- package/src/js/utils/style.js +148 -0
- package/src/js/utils/time.js +36 -0
- package/src/js/utils/urls.js +40 -0
- package/src/sass/base.scss +69 -0
- package/src/sass/components/badges.scss +12 -0
- package/src/sass/components/captions.scss +58 -0
- package/src/sass/components/control.scss +52 -0
- package/src/sass/components/controls.scss +65 -0
- package/src/sass/components/menus.scss +205 -0
- package/src/sass/components/poster.scss +27 -0
- package/src/sass/components/progress.scss +107 -0
- package/src/sass/components/sliders.scss +99 -0
- package/src/sass/components/times.scss +20 -0
- package/src/sass/components/tooltips.scss +91 -0
- package/src/sass/components/volume.scss +18 -0
- package/src/sass/lib/animation.scss +31 -0
- package/src/sass/lib/css-vars.scss +103 -0
- package/src/sass/lib/functions.scss +3 -0
- package/src/sass/lib/mixins.scss +82 -0
- package/src/sass/plugins/ads.scss +53 -0
- package/src/sass/plugins/preview-thumbnails/index.scss +121 -0
- package/src/sass/plugins/preview-thumbnails/settings.scss +17 -0
- package/src/sass/plyr.scss +46 -0
- package/src/sass/settings/badges.scss +7 -0
- package/src/sass/settings/breakpoints.scss +9 -0
- package/src/sass/settings/captions.scss +10 -0
- package/src/sass/settings/colors.scss +18 -0
- package/src/sass/settings/controls.scss +30 -0
- package/src/sass/settings/cosmetics.scss +5 -0
- package/src/sass/settings/helpers.scss +7 -0
- package/src/sass/settings/menus.scss +13 -0
- package/src/sass/settings/progress.scss +18 -0
- package/src/sass/settings/sliders.scss +39 -0
- package/src/sass/settings/tooltips.scss +11 -0
- package/src/sass/settings/type.scss +16 -0
- package/src/sass/states/fullscreen.scss +15 -0
- package/src/sass/types/audio.scss +61 -0
- package/src/sass/types/video.scss +170 -0
- package/src/sass/utils/animation.scss +7 -0
- package/src/sass/utils/hidden.scss +28 -0
- package/src/sprite/plyr-airplay.svg +8 -0
- package/src/sprite/plyr-captions-off.svg +7 -0
- package/src/sprite/plyr-captions-on.svg +7 -0
- package/src/sprite/plyr-download.svg +8 -0
- package/src/sprite/plyr-enter-fullscreen.svg +4 -0
- package/src/sprite/plyr-exit-fullscreen.svg +4 -0
- package/src/sprite/plyr-fast-forward.svg +3 -0
- package/src/sprite/plyr-logo-vimeo.svg +6 -0
- package/src/sprite/plyr-logo-youtube.svg +6 -0
- package/src/sprite/plyr-muted.svg +8 -0
- package/src/sprite/plyr-pause.svg +8 -0
- package/src/sprite/plyr-pip.svg +6 -0
- package/src/sprite/plyr-play.svg +5 -0
- package/src/sprite/plyr-restart.svg +5 -0
- package/src/sprite/plyr-rewind.svg +3 -0
- package/src/sprite/plyr-settings.svg +5 -0
- package/src/sprite/plyr-volume.svg +11 -0
- package/tasks/build.js +226 -0
- package/tasks/deploy.js +216 -0
- 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;
|