@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// redxplyr Polyfilled Build
|
|
3
|
+
// redxplyr.js v1.0.0
|
|
4
|
+
// https://github.com/xgauravyaduvanshii/redxplyr
|
|
5
|
+
// License: The MIT License (MIT)
|
|
6
|
+
// ==========================================================================
|
|
7
|
+
|
|
8
|
+
import Plyr from './plyr';
|
|
9
|
+
import 'custom-event-polyfill';
|
|
10
|
+
|
|
11
|
+
import 'url-polyfill';
|
|
12
|
+
|
|
13
|
+
export default Plyr;
|
package/src/js/source.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// Plyr source update
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
import { providers } from './config/types';
|
|
6
|
+
import html5 from './html5';
|
|
7
|
+
import media from './media';
|
|
8
|
+
import PreviewThumbnails from './plugins/preview-thumbnails';
|
|
9
|
+
import support from './support';
|
|
10
|
+
import ui from './ui';
|
|
11
|
+
import { createElement, insertElement, removeElement } from './utils/elements';
|
|
12
|
+
import is from './utils/is';
|
|
13
|
+
import { getDeep } from './utils/objects';
|
|
14
|
+
|
|
15
|
+
const source = {
|
|
16
|
+
// Add elements to HTML5 media (source, tracks, etc)
|
|
17
|
+
insertElements(type, attributes) {
|
|
18
|
+
if (is.string(attributes)) {
|
|
19
|
+
insertElement(type, this.media, {
|
|
20
|
+
src: attributes,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else if (is.array(attributes)) {
|
|
24
|
+
attributes.forEach((attribute) => {
|
|
25
|
+
insertElement(type, this.media, attribute);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Update source
|
|
31
|
+
// Sources are not checked for support so be careful
|
|
32
|
+
change(input) {
|
|
33
|
+
if (!getDeep(input, 'sources.length')) {
|
|
34
|
+
this.debug.warn('Invalid source format');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cancel current network requests
|
|
39
|
+
html5.cancelRequests.call(this);
|
|
40
|
+
|
|
41
|
+
// Destroy instance and re-setup
|
|
42
|
+
this.destroy(() => {
|
|
43
|
+
// Reset quality options
|
|
44
|
+
this.options.quality = [];
|
|
45
|
+
|
|
46
|
+
// Remove elements
|
|
47
|
+
removeElement(this.media);
|
|
48
|
+
this.media = null;
|
|
49
|
+
|
|
50
|
+
// Reset class name
|
|
51
|
+
if (is.element(this.elements.container)) {
|
|
52
|
+
this.elements.container.removeAttribute('class');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Set the type and provider
|
|
56
|
+
const { sources, type } = input;
|
|
57
|
+
const [{ provider = providers.html5, src }] = sources;
|
|
58
|
+
const tagName = provider === 'html5' ? type : 'div';
|
|
59
|
+
const attributes = provider === 'html5' ? {} : { src };
|
|
60
|
+
|
|
61
|
+
Object.assign(this, {
|
|
62
|
+
provider,
|
|
63
|
+
type,
|
|
64
|
+
// Check for support
|
|
65
|
+
supported: support.check(type, provider, this.config.playsinline),
|
|
66
|
+
// Create new element
|
|
67
|
+
media: createElement(tagName, attributes),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Inject the new element
|
|
71
|
+
this.elements.container.appendChild(this.media);
|
|
72
|
+
|
|
73
|
+
// Autoplay the new source?
|
|
74
|
+
if (is.boolean(input.autoplay)) {
|
|
75
|
+
this.config.autoplay = input.autoplay;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Set attributes for audio and video
|
|
79
|
+
if (this.isHTML5) {
|
|
80
|
+
if (this.config.crossorigin) {
|
|
81
|
+
this.media.setAttribute('crossorigin', '');
|
|
82
|
+
}
|
|
83
|
+
if (this.config.autoplay) {
|
|
84
|
+
this.media.setAttribute('autoplay', '');
|
|
85
|
+
}
|
|
86
|
+
if (!is.empty(input.poster)) {
|
|
87
|
+
this.poster = input.poster;
|
|
88
|
+
}
|
|
89
|
+
if (this.config.loop.active) {
|
|
90
|
+
this.media.setAttribute('loop', '');
|
|
91
|
+
}
|
|
92
|
+
if (this.config.muted) {
|
|
93
|
+
this.media.setAttribute('muted', '');
|
|
94
|
+
}
|
|
95
|
+
if (this.config.playsinline) {
|
|
96
|
+
this.media.setAttribute('playsinline', '');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Restore class hook
|
|
101
|
+
ui.addStyleHook.call(this);
|
|
102
|
+
|
|
103
|
+
// Set new sources for html5
|
|
104
|
+
if (this.isHTML5) {
|
|
105
|
+
source.insertElements.call(this, 'source', sources);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Set video title
|
|
109
|
+
this.config.title = input.title;
|
|
110
|
+
|
|
111
|
+
// Set up from scratch
|
|
112
|
+
media.setup.call(this);
|
|
113
|
+
|
|
114
|
+
// HTML5 stuff
|
|
115
|
+
if (this.isHTML5) {
|
|
116
|
+
// Setup captions
|
|
117
|
+
if (Object.keys(input).includes('tracks')) {
|
|
118
|
+
source.insertElements.call(this, 'track', input.tracks);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If HTML5 or embed but not fully supported, setupInterface and call ready now
|
|
123
|
+
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
|
|
124
|
+
// Setup interface
|
|
125
|
+
ui.build.call(this);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load HTML5 sources
|
|
129
|
+
if (this.isHTML5) {
|
|
130
|
+
this.media.load();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Update previewThumbnails config & reload plugin
|
|
134
|
+
if (!is.empty(input.previewThumbnails)) {
|
|
135
|
+
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
|
|
136
|
+
|
|
137
|
+
// Cleanup previewThumbnails plugin if it was loaded
|
|
138
|
+
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
|
139
|
+
this.previewThumbnails.destroy();
|
|
140
|
+
this.previewThumbnails = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create new instance if it is still enabled
|
|
144
|
+
if (this.config.previewThumbnails.enabled) {
|
|
145
|
+
this.previewThumbnails = new PreviewThumbnails(this);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Update the fullscreen support
|
|
150
|
+
this.fullscreen.update();
|
|
151
|
+
}, true);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default source;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// Plyr storage
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
import is from './utils/is';
|
|
6
|
+
import { extend } from './utils/objects';
|
|
7
|
+
|
|
8
|
+
class Storage {
|
|
9
|
+
constructor(player) {
|
|
10
|
+
this.enabled = player.config.storage.enabled;
|
|
11
|
+
this.key = player.config.storage.key;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Check for actual support (see if we can use it)
|
|
15
|
+
static get supported() {
|
|
16
|
+
try {
|
|
17
|
+
if (!('localStorage' in window)) return false;
|
|
18
|
+
const test = '___test';
|
|
19
|
+
// Try to use it (it might be disabled, e.g. user is in private mode)
|
|
20
|
+
// see: https://github.com/xgauravyaduvanshii/redxplyr/issues/131
|
|
21
|
+
window.localStorage.setItem(test, test);
|
|
22
|
+
window.localStorage.removeItem(test);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get = (key) => {
|
|
31
|
+
if (!Storage.supported || !this.enabled) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const store = window.localStorage.getItem(this.key);
|
|
35
|
+
if (is.empty(store)) return null;
|
|
36
|
+
const json = JSON.parse(store);
|
|
37
|
+
return is.string(key) && key.length ? json[key] : json;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
set = (object) => {
|
|
41
|
+
// Bail if we don't have localStorage support or it's disabled
|
|
42
|
+
if (!Storage.supported || !this.enabled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Can only store objects
|
|
47
|
+
if (!is.object(object)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get current storage
|
|
52
|
+
let storage = this.get();
|
|
53
|
+
|
|
54
|
+
// Default to empty object
|
|
55
|
+
if (is.empty(storage)) {
|
|
56
|
+
storage = {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Update the working copy of the values
|
|
60
|
+
extend(storage, object);
|
|
61
|
+
|
|
62
|
+
// Update storage
|
|
63
|
+
try {
|
|
64
|
+
window.localStorage.setItem(this.key, JSON.stringify(storage));
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default Storage;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// Plyr support checks
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
import { transitionEndEvent } from './utils/animation';
|
|
6
|
+
import { createElement } from './utils/elements';
|
|
7
|
+
import is from './utils/is';
|
|
8
|
+
|
|
9
|
+
// Default codecs for checking mimetype support
|
|
10
|
+
const defaultCodecs = {
|
|
11
|
+
'audio/ogg': 'vorbis',
|
|
12
|
+
'audio/wav': '1',
|
|
13
|
+
'video/webm': 'vp8, vorbis',
|
|
14
|
+
'video/mp4': 'avc1.42E01E, mp4a.40.2',
|
|
15
|
+
'video/ogg': 'theora',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Check for feature support
|
|
19
|
+
const support = {
|
|
20
|
+
// Basic support
|
|
21
|
+
audio: 'canPlayType' in document.createElement('audio'),
|
|
22
|
+
video: 'canPlayType' in document.createElement('video'),
|
|
23
|
+
|
|
24
|
+
// Check for support
|
|
25
|
+
// Basic functionality vs full UI
|
|
26
|
+
check(type, provider) {
|
|
27
|
+
const api = support[type] || provider !== 'html5';
|
|
28
|
+
const ui = api && support.rangeInput;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
api,
|
|
32
|
+
ui,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Picture-in-picture support
|
|
37
|
+
pip: (() => {
|
|
38
|
+
return (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture);
|
|
39
|
+
})(),
|
|
40
|
+
|
|
41
|
+
// Airplay support
|
|
42
|
+
// Safari only currently
|
|
43
|
+
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
|
|
44
|
+
|
|
45
|
+
// Inline playback support
|
|
46
|
+
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
|
47
|
+
playsinline: 'playsInline' in document.createElement('video'),
|
|
48
|
+
|
|
49
|
+
// Check for mime type support against a player instance
|
|
50
|
+
// Credits: http://diveintohtml5.info/everything.html
|
|
51
|
+
// Related: http://www.leanbackplayer.com/test/h5mt.html
|
|
52
|
+
mime(input) {
|
|
53
|
+
if (is.empty(input)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [mediaType] = input.split('/');
|
|
58
|
+
let type = input;
|
|
59
|
+
|
|
60
|
+
// Verify we're using HTML5 and there's no media type mismatch
|
|
61
|
+
if (!this.isHTML5 || mediaType !== this.type) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add codec if required
|
|
66
|
+
if (Object.keys(defaultCodecs).includes(type)) {
|
|
67
|
+
type += `; codecs="${defaultCodecs[input]}"`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Check for textTracks support
|
|
79
|
+
textTracks: 'textTracks' in document.createElement('video'),
|
|
80
|
+
|
|
81
|
+
// <input type="range"> Sliders
|
|
82
|
+
rangeInput: (() => {
|
|
83
|
+
const range = document.createElement('input');
|
|
84
|
+
range.type = 'range';
|
|
85
|
+
return range.type === 'range';
|
|
86
|
+
})(),
|
|
87
|
+
|
|
88
|
+
// Touch
|
|
89
|
+
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
|
|
90
|
+
touch: 'ontouchstart' in document.documentElement,
|
|
91
|
+
|
|
92
|
+
// Detect transitions support
|
|
93
|
+
transitions: transitionEndEvent !== false,
|
|
94
|
+
|
|
95
|
+
// Reduced motion iOS & MacOS setting
|
|
96
|
+
// https://webkit.org/blog/7551/responsive-design-for-motion/
|
|
97
|
+
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default support;
|
package/src/js/ui.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// Plyr UI
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
import captions from './captions';
|
|
6
|
+
import controls from './controls';
|
|
7
|
+
import support from './support';
|
|
8
|
+
import { getElement, toggleClass } from './utils/elements';
|
|
9
|
+
import { ready, triggerEvent } from './utils/events';
|
|
10
|
+
import i18n from './utils/i18n';
|
|
11
|
+
import is from './utils/is';
|
|
12
|
+
import loadImage from './utils/load-image';
|
|
13
|
+
|
|
14
|
+
const ui = {
|
|
15
|
+
addStyleHook() {
|
|
16
|
+
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
|
17
|
+
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// Toggle native HTML5 media controls
|
|
21
|
+
toggleNativeControls(toggle = false) {
|
|
22
|
+
if (toggle && this.isHTML5) {
|
|
23
|
+
this.media.setAttribute('controls', '');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.media.removeAttribute('controls');
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Setup the UI
|
|
31
|
+
build() {
|
|
32
|
+
// Re-attach media element listeners
|
|
33
|
+
// TODO: Use event bubbling?
|
|
34
|
+
this.listeners.media();
|
|
35
|
+
|
|
36
|
+
// Don't setup interface if no support
|
|
37
|
+
if (!this.supported.ui) {
|
|
38
|
+
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
|
|
39
|
+
|
|
40
|
+
// Restore native controls
|
|
41
|
+
ui.toggleNativeControls.call(this, true);
|
|
42
|
+
|
|
43
|
+
// Bail
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Inject custom controls if not present
|
|
48
|
+
if (!is.element(this.elements.controls)) {
|
|
49
|
+
// Inject custom controls
|
|
50
|
+
controls.inject.call(this);
|
|
51
|
+
|
|
52
|
+
// Re-attach control listeners
|
|
53
|
+
this.listeners.controls();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Remove native controls
|
|
57
|
+
ui.toggleNativeControls.call(this);
|
|
58
|
+
|
|
59
|
+
// Setup captions for HTML5
|
|
60
|
+
if (this.isHTML5) {
|
|
61
|
+
captions.setup.call(this);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Reset volume
|
|
65
|
+
this.volume = null;
|
|
66
|
+
|
|
67
|
+
// Reset mute state
|
|
68
|
+
this.muted = null;
|
|
69
|
+
|
|
70
|
+
// Reset loop state
|
|
71
|
+
this.loop = null;
|
|
72
|
+
|
|
73
|
+
// Reset quality setting
|
|
74
|
+
this.quality = null;
|
|
75
|
+
|
|
76
|
+
// Reset speed
|
|
77
|
+
this.speed = null;
|
|
78
|
+
|
|
79
|
+
// Reset volume display
|
|
80
|
+
controls.updateVolume.call(this);
|
|
81
|
+
|
|
82
|
+
// Reset time display
|
|
83
|
+
controls.timeUpdate.call(this);
|
|
84
|
+
|
|
85
|
+
// Reset duration display
|
|
86
|
+
controls.durationUpdate.call(this);
|
|
87
|
+
|
|
88
|
+
// Update the UI
|
|
89
|
+
ui.checkPlaying.call(this);
|
|
90
|
+
|
|
91
|
+
// Check for picture-in-picture support
|
|
92
|
+
toggleClass(
|
|
93
|
+
this.elements.container,
|
|
94
|
+
this.config.classNames.pip.supported,
|
|
95
|
+
support.pip && this.isHTML5 && this.isVideo,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Check for airplay support
|
|
99
|
+
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
|
100
|
+
|
|
101
|
+
// Add touch class
|
|
102
|
+
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
|
103
|
+
|
|
104
|
+
// Ready for API calls
|
|
105
|
+
this.ready = true;
|
|
106
|
+
|
|
107
|
+
// Ready event at end of execution stack
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
triggerEvent.call(this, this.media, 'ready');
|
|
110
|
+
}, 0);
|
|
111
|
+
|
|
112
|
+
// Set the title
|
|
113
|
+
ui.setTitle.call(this);
|
|
114
|
+
|
|
115
|
+
// Assure the poster image is set, if the property was added before the element was created
|
|
116
|
+
if (this.poster) {
|
|
117
|
+
ui.setPoster.call(this, this.poster, false).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Manually set the duration if user has overridden it.
|
|
121
|
+
// The event listeners for it doesn't get called if preload is disabled (#701)
|
|
122
|
+
if (this.config.duration) {
|
|
123
|
+
controls.durationUpdate.call(this);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Media metadata
|
|
127
|
+
if (this.config.mediaMetadata) {
|
|
128
|
+
controls.setMediaMetadata.call(this);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Setup aria attribute for play and iframe title
|
|
133
|
+
setTitle() {
|
|
134
|
+
// Find the current text
|
|
135
|
+
let label = i18n.get('play', this.config);
|
|
136
|
+
|
|
137
|
+
// If there's a media title set, use that for the label
|
|
138
|
+
if (is.string(this.config.title) && !is.empty(this.config.title)) {
|
|
139
|
+
label += `, ${this.config.title}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If there's a play button, set label
|
|
143
|
+
Array.from(this.elements.buttons.play || []).forEach((button) => {
|
|
144
|
+
button.setAttribute('aria-label', label);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Set iframe title
|
|
148
|
+
// https://github.com/xgauravyaduvanshii/redxplyr/issues/124
|
|
149
|
+
if (this.isEmbed) {
|
|
150
|
+
const iframe = getElement.call(this, 'iframe');
|
|
151
|
+
|
|
152
|
+
if (!is.element(iframe)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default to media type
|
|
157
|
+
const title = !is.empty(this.config.title) ? this.config.title : 'video';
|
|
158
|
+
const format = i18n.get('frameTitle', this.config);
|
|
159
|
+
|
|
160
|
+
iframe.setAttribute('title', format.replace('{title}', title));
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Toggle poster
|
|
165
|
+
togglePoster(enable) {
|
|
166
|
+
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Set the poster image (async)
|
|
170
|
+
// Used internally for the poster setter, with the passive option forced to false
|
|
171
|
+
setPoster(poster, passive = true) {
|
|
172
|
+
// Don't override if call is passive
|
|
173
|
+
if (passive && this.poster) {
|
|
174
|
+
return Promise.reject(new Error('Poster already set'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Set property synchronously to respect the call order
|
|
178
|
+
this.media.setAttribute('data-poster', poster);
|
|
179
|
+
|
|
180
|
+
// Show the poster
|
|
181
|
+
this.elements.poster.removeAttribute('hidden');
|
|
182
|
+
|
|
183
|
+
// Wait until ui is ready
|
|
184
|
+
return (
|
|
185
|
+
ready
|
|
186
|
+
.call(this)
|
|
187
|
+
// Load image
|
|
188
|
+
.then(() => loadImage(poster))
|
|
189
|
+
.catch((error) => {
|
|
190
|
+
// Hide poster on error unless it's been set by another call
|
|
191
|
+
if (poster === this.poster) {
|
|
192
|
+
ui.togglePoster.call(this, false);
|
|
193
|
+
}
|
|
194
|
+
// Rethrow
|
|
195
|
+
throw error;
|
|
196
|
+
})
|
|
197
|
+
.then(() => {
|
|
198
|
+
// Prevent race conditions
|
|
199
|
+
if (poster !== this.poster) {
|
|
200
|
+
throw new Error('setPoster cancelled by later call to setPoster');
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
.then(() => {
|
|
204
|
+
Object.assign(this.elements.poster.style, {
|
|
205
|
+
backgroundImage: `url('${poster}')`,
|
|
206
|
+
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
|
207
|
+
backgroundSize: '',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
ui.togglePoster.call(this, true);
|
|
211
|
+
|
|
212
|
+
return poster;
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Check playing state
|
|
218
|
+
checkPlaying(event) {
|
|
219
|
+
// Class hooks
|
|
220
|
+
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
|
221
|
+
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
|
222
|
+
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
|
223
|
+
|
|
224
|
+
// Set state
|
|
225
|
+
Array.from(this.elements.buttons.play || []).forEach((target) => {
|
|
226
|
+
Object.assign(target, { pressed: this.playing });
|
|
227
|
+
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Only update controls on non timeupdate events
|
|
231
|
+
if (is.event(event) && event.type === 'timeupdate') {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Toggle controls
|
|
236
|
+
ui.toggleControls.call(this);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// Check if media is loading
|
|
240
|
+
checkLoading(event) {
|
|
241
|
+
this.loading = ['stalled', 'waiting'].includes(event.type);
|
|
242
|
+
|
|
243
|
+
// Clear timer
|
|
244
|
+
clearTimeout(this.timers.loading);
|
|
245
|
+
|
|
246
|
+
// Timer to prevent flicker when seeking
|
|
247
|
+
this.timers.loading = setTimeout(
|
|
248
|
+
() => {
|
|
249
|
+
// Update progress bar loading class state
|
|
250
|
+
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
|
251
|
+
|
|
252
|
+
// Update controls visibility
|
|
253
|
+
ui.toggleControls.call(this);
|
|
254
|
+
},
|
|
255
|
+
this.loading ? 250 : 0,
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// Toggle controls based on state and `force` argument
|
|
260
|
+
toggleControls(force) {
|
|
261
|
+
const { controls: controlsElement } = this.elements;
|
|
262
|
+
|
|
263
|
+
if (controlsElement && this.config.hideControls) {
|
|
264
|
+
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
|
265
|
+
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
|
|
266
|
+
|
|
267
|
+
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
|
268
|
+
this.toggleControls(
|
|
269
|
+
Boolean(
|
|
270
|
+
force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// Migrate any custom properties from the media to the parent
|
|
277
|
+
migrateStyles() {
|
|
278
|
+
// Loop through values (as they are the keys when the object is spread 🤔)
|
|
279
|
+
Object.values({ ...this.media.style })
|
|
280
|
+
// We're only fussed about Plyr specific properties
|
|
281
|
+
.filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
|
|
282
|
+
.forEach((key) => {
|
|
283
|
+
// Set on the container
|
|
284
|
+
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
|
|
285
|
+
|
|
286
|
+
// Clean up from media element
|
|
287
|
+
this.media.style.removeProperty(key);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Remove attribute if empty
|
|
291
|
+
if (is.empty(this.media.style)) {
|
|
292
|
+
this.media.removeAttribute('style');
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export default ui;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// Animation utils
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
import is from './is';
|
|
6
|
+
|
|
7
|
+
export const transitionEndEvent = (() => {
|
|
8
|
+
const element = document.createElement('span');
|
|
9
|
+
|
|
10
|
+
const events = {
|
|
11
|
+
WebkitTransition: 'webkitTransitionEnd',
|
|
12
|
+
MozTransition: 'transitionend',
|
|
13
|
+
OTransition: 'oTransitionEnd otransitionend',
|
|
14
|
+
transition: 'transitionend',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const type = Object.keys(events).find(event => element.style[event] !== undefined);
|
|
18
|
+
|
|
19
|
+
return is.string(type) ? events[type] : false;
|
|
20
|
+
})();
|
|
21
|
+
|
|
22
|
+
// Force repaint of element
|
|
23
|
+
export function repaint(element, delay) {
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
try {
|
|
26
|
+
element.hidden = true;
|
|
27
|
+
// eslint-disable-next-line no-unused-expressions
|
|
28
|
+
element.offsetHeight;
|
|
29
|
+
element.hidden = false;
|
|
30
|
+
}
|
|
31
|
+
catch {}
|
|
32
|
+
}, delay);
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// Array utils
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
import is from './is';
|
|
6
|
+
|
|
7
|
+
// Remove duplicates in an array
|
|
8
|
+
export function dedupe(array) {
|
|
9
|
+
if (!is.array(array)) {
|
|
10
|
+
return array;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return array.filter((item, index) => array.indexOf(item) === index);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get the closest value in an array
|
|
17
|
+
export function closest(array, value) {
|
|
18
|
+
if (!is.array(array) || !array.length) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
|
23
|
+
}
|