@oat-sa/tao-core-ui 1.6.2 → 1.6.3
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/dist/mediaplayer/css/player.css +34 -2
- package/dist/mediaplayer/css/player.css.map +1 -1
- package/dist/mediaplayer/players/html5.js +236 -59
- package/dist/mediaplayer/support.js +17 -3
- package/dist/mediaplayer/utils/reminder.js +198 -0
- package/dist/mediaplayer/utils/timeObserver.js +149 -0
- package/dist/mediaplayer.js +44 -97
- package/package.json +1 -1
- package/src/itemButtonList/css/item-button-list.css +225 -0
- package/src/itemButtonList/css/item-button-list.css.map +1 -0
- package/src/mediaplayer/css/player.css +34 -2
- package/src/mediaplayer/css/player.css.map +1 -1
- package/src/mediaplayer/players/html5.js +247 -67
- package/src/mediaplayer/scss/player.scss +47 -6
- package/src/mediaplayer/support.js +17 -4
- package/src/mediaplayer/tpl/player.tpl +1 -2
- package/src/mediaplayer/utils/reminder.js +184 -0
- package/src/mediaplayer/utils/timeObserver.js +143 -0
- package/src/mediaplayer.js +38 -88
|
@@ -23,22 +23,30 @@ import support from 'ui/mediaplayer/support';
|
|
|
23
23
|
import audioTpl from 'ui/mediaplayer/tpl/audio';
|
|
24
24
|
import videoTpl from 'ui/mediaplayer/tpl/video';
|
|
25
25
|
import sourceTpl from 'ui/mediaplayer/tpl/source';
|
|
26
|
+
import reminderManagerFactory from 'ui/mediaplayer/utils/reminder';
|
|
27
|
+
import timeObserverFactory from 'ui/mediaplayer/utils/timeObserver';
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* CSS namespace
|
|
29
|
-
* @type {
|
|
31
|
+
* @type {string}
|
|
30
32
|
*/
|
|
31
33
|
const ns = '.mediaplayer';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Range value of the volume
|
|
35
|
-
* @type {
|
|
37
|
+
* @type {number}
|
|
36
38
|
*/
|
|
37
39
|
const volumeRange = 100;
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Delay before considering a media stalled
|
|
43
|
+
* @type {number}
|
|
44
|
+
*/
|
|
45
|
+
const stalledDetectionDelay = 2000;
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* List of media events that can be listened to for debugging
|
|
41
|
-
* @type {
|
|
49
|
+
* @type {string[]}
|
|
42
50
|
*/
|
|
43
51
|
const mediaEvents = [
|
|
44
52
|
'abort',
|
|
@@ -71,40 +79,32 @@ const mediaEvents = [
|
|
|
71
79
|
|
|
72
80
|
/**
|
|
73
81
|
* List of player events that can be listened to for debugging
|
|
74
|
-
* @type {
|
|
82
|
+
* @type {string[]}
|
|
75
83
|
*/
|
|
76
|
-
const playerEvents = [
|
|
77
|
-
'end',
|
|
78
|
-
'error',
|
|
79
|
-
'pause',
|
|
80
|
-
'play',
|
|
81
|
-
'playing',
|
|
82
|
-
'ready',
|
|
83
|
-
'recovererror',
|
|
84
|
-
'resize',
|
|
85
|
-
'stalled',
|
|
86
|
-
'timeupdate'
|
|
87
|
-
];
|
|
84
|
+
const playerEvents = ['end', 'error', 'pause', 'play', 'playing', 'ready', 'resize', 'stalled', 'timeupdate'];
|
|
88
85
|
|
|
89
86
|
/**
|
|
90
87
|
* Defines a player object dedicated to the native HTML5 player
|
|
91
88
|
* @param {jQuery} $container - Where to render the player
|
|
92
|
-
* @param {
|
|
89
|
+
* @param {object} config - The list of config entries
|
|
93
90
|
* @param {Array} config.sources - The list of media sources
|
|
94
|
-
* @param {
|
|
95
|
-
* @param {
|
|
96
|
-
* @param {
|
|
97
|
-
* @
|
|
91
|
+
* @param {string} [config.type] - The type of player (video or audio) (default: video)
|
|
92
|
+
* @param {boolean} [config.preview] - Enables the media preview (load media metadata)
|
|
93
|
+
* @param {boolean} [config.debug] - Enables the debug mode
|
|
94
|
+
* @param {number} [config.config.stalledDetectionDelay] - The delay before considering a media is stalled
|
|
95
|
+
* @returns {object} player
|
|
98
96
|
*/
|
|
99
97
|
export default function html5PlayerFactory($container, config = {}) {
|
|
100
98
|
const type = config.type || 'video';
|
|
101
99
|
const sources = config.sources || [];
|
|
100
|
+
const updateObserver = reminderManagerFactory();
|
|
101
|
+
const timeObserver = timeObserverFactory();
|
|
102
|
+
|
|
103
|
+
config.stalledDetectionDelay = config.stalledDetectionDelay || stalledDetectionDelay;
|
|
102
104
|
|
|
103
105
|
let $media;
|
|
104
106
|
let media;
|
|
105
|
-
let
|
|
106
|
-
let loaded = false;
|
|
107
|
-
let stalled = false;
|
|
107
|
+
let state = {};
|
|
108
108
|
|
|
109
109
|
const getDebugContext = action => {
|
|
110
110
|
const networkState = media && media.networkState;
|
|
@@ -112,9 +112,10 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
112
112
|
return `[html5-${type}(networkState=${networkState},readyState=${readyState}):${action}]`;
|
|
113
113
|
};
|
|
114
114
|
// eslint-disable-next-line
|
|
115
|
-
const debug = (action, ...args) =>
|
|
115
|
+
const debug = (action, ...args) =>
|
|
116
|
+
(config.debug === true || config.debug === action) && window.console.log(getDebugContext(action), ...args);
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
return eventifier({
|
|
118
119
|
init() {
|
|
119
120
|
const tpl = 'audio' === type ? audioTpl : videoTpl;
|
|
120
121
|
const page = new UrlParser(window.location);
|
|
@@ -124,6 +125,8 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
124
125
|
let link = '';
|
|
125
126
|
let result = false;
|
|
126
127
|
|
|
128
|
+
state = {};
|
|
129
|
+
|
|
127
130
|
sources.forEach(source => {
|
|
128
131
|
if (!page.sameDomain(source.src)) {
|
|
129
132
|
cors = true;
|
|
@@ -139,38 +142,65 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
139
142
|
$media = $(tpl({ cors, preload, poster, link }));
|
|
140
143
|
$container.append($media);
|
|
141
144
|
|
|
142
|
-
playback = false;
|
|
143
|
-
loaded = false;
|
|
144
|
-
stalled = false;
|
|
145
|
-
|
|
146
145
|
media = $media.get(0);
|
|
147
146
|
result = !!(media && support.checkSupport(media));
|
|
148
147
|
|
|
149
|
-
//
|
|
148
|
+
// Remove the browser native controls if we can use the API instead
|
|
150
149
|
if (support.canControl()) {
|
|
151
150
|
$media.removeAttr('controls');
|
|
152
151
|
}
|
|
153
152
|
|
|
153
|
+
// Detect stalled video when the timer suddenly jump to the end
|
|
154
|
+
timeObserver.removeAllListeners().on('irregularity', position => {
|
|
155
|
+
if (state.playback && state.stallDetection) {
|
|
156
|
+
this.stalled(position);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
154
160
|
$media
|
|
155
161
|
.on(`play${ns}`, () => {
|
|
156
|
-
playback = true;
|
|
162
|
+
state.playback = true;
|
|
163
|
+
state.playedViaApi = false;
|
|
164
|
+
timeObserver.init(media.currentTime, media.duration);
|
|
157
165
|
this.trigger('play');
|
|
158
166
|
})
|
|
159
167
|
.on(`pause${ns}`, () => {
|
|
168
|
+
if (
|
|
169
|
+
state.stallDetection &&
|
|
170
|
+
!state.pausedViaApi &&
|
|
171
|
+
updateObserver.running &&
|
|
172
|
+
updateObserver.elapsed < 100
|
|
173
|
+
) {
|
|
174
|
+
// The pause event may be triggered after a connectivity issue as the player is out of data
|
|
175
|
+
this.stalled();
|
|
176
|
+
}
|
|
177
|
+
state.pausedViaApi = false;
|
|
178
|
+
state.playing = false;
|
|
179
|
+
updateObserver.stop();
|
|
160
180
|
this.trigger('pause');
|
|
161
181
|
})
|
|
182
|
+
.on(`seeked${ns}`, () => {
|
|
183
|
+
// When the user try changing the current playing position while the network is down,
|
|
184
|
+
// the player will end the playback by moving straight to the end.
|
|
185
|
+
if (state.seekedViaApi && Math.floor(state.seekAt) !== Math.floor(media.currentTime)) {
|
|
186
|
+
state.stallDetection = true;
|
|
187
|
+
}
|
|
188
|
+
state.seekedViaApi = false;
|
|
189
|
+
})
|
|
162
190
|
.on(`ended${ns}`, () => {
|
|
163
|
-
|
|
191
|
+
updateObserver.forget().stop();
|
|
192
|
+
timeObserver.update(media.currentTime);
|
|
193
|
+
state.playback = false;
|
|
194
|
+
state.playing = false;
|
|
164
195
|
this.trigger('end');
|
|
165
196
|
})
|
|
166
197
|
.on(`timeupdate${ns}`, () => {
|
|
198
|
+
state.playing = true;
|
|
199
|
+
updateObserver.start();
|
|
200
|
+
timeObserver.update(media.currentTime);
|
|
167
201
|
this.trigger('timeupdate');
|
|
168
202
|
})
|
|
169
203
|
.on('loadstart', () => {
|
|
170
|
-
if (stalled) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
204
|
if (media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE) {
|
|
175
205
|
this.trigger('error');
|
|
176
206
|
}
|
|
@@ -178,24 +208,54 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
178
208
|
if (!config.preview && media.networkState === HTMLMediaElement.NETWORK_IDLE) {
|
|
179
209
|
this.trigger('ready');
|
|
180
210
|
}
|
|
211
|
+
|
|
212
|
+
// The media may be unreachable straight from the beginning
|
|
213
|
+
this.detectStalledNetwork();
|
|
214
|
+
})
|
|
215
|
+
.on(`waiting${ns}`, () => {
|
|
216
|
+
// The "waiting" event means the player is pending data,
|
|
217
|
+
// it may be the symptom of a connectivity issue
|
|
218
|
+
this.detectStalledNetwork();
|
|
181
219
|
})
|
|
182
220
|
.on(`error${ns}`, () => {
|
|
183
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE ||
|
|
223
|
+
(media.error instanceof MediaError &&
|
|
224
|
+
media.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
|
|
225
|
+
) {
|
|
226
|
+
// No source means the player does not support the supplied media.
|
|
227
|
+
// Or it can be more explicit with the not supported error.
|
|
228
|
+
// There is nothing that we can do from this stage.
|
|
184
229
|
this.trigger('error');
|
|
185
230
|
} else {
|
|
186
|
-
|
|
231
|
+
// Other errors need special attention as they can be recoverable
|
|
232
|
+
this.handleError(media.error);
|
|
187
233
|
}
|
|
188
234
|
})
|
|
235
|
+
.on('loadedmetadata', () => {
|
|
236
|
+
timeObserver.init(media.currentTime, media.duration);
|
|
237
|
+
this.ready();
|
|
238
|
+
})
|
|
189
239
|
.on(`canplay${ns}`, () => {
|
|
190
|
-
|
|
191
|
-
|
|
240
|
+
if (!state.stalled) {
|
|
241
|
+
this.ready();
|
|
242
|
+
}
|
|
192
243
|
})
|
|
193
244
|
.on(`stalled${ns}`, () => {
|
|
194
|
-
stalled
|
|
195
|
-
this
|
|
245
|
+
// The "stalled" event may be triggered once the player is halted after initialisation,
|
|
246
|
+
// but this does not mean the playback is actually stalled, hence we only take care of the playing state
|
|
247
|
+
if (state.playing && !media.paused) {
|
|
248
|
+
this.handleError(media.error);
|
|
249
|
+
}
|
|
196
250
|
})
|
|
197
251
|
.on(`playing${ns}`, () => {
|
|
198
|
-
|
|
252
|
+
if (state.stallDetection) {
|
|
253
|
+
// The "playing" event may occur after a connectivity issue.
|
|
254
|
+
// For the sake of the stall detection, we need to discard this event
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
updateObserver.forget().start();
|
|
258
|
+
state.playing = true;
|
|
199
259
|
this.trigger('playing');
|
|
200
260
|
});
|
|
201
261
|
|
|
@@ -212,19 +272,133 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
212
272
|
});
|
|
213
273
|
}
|
|
214
274
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this.addMedia(src, type);
|
|
218
|
-
});
|
|
275
|
+
result =
|
|
276
|
+
result &&
|
|
277
|
+
sources.reduce((supported, source) => this.addMedia(source.src, source.type) || supported, false);
|
|
219
278
|
|
|
220
279
|
return result;
|
|
221
280
|
},
|
|
222
281
|
|
|
282
|
+
handleError(error) {
|
|
283
|
+
// Discard legitimate and non-blocking errors
|
|
284
|
+
switch (error && error.name) {
|
|
285
|
+
case 'NotAllowedError':
|
|
286
|
+
debug('api call', 'handleError', 'the autoplay is not allowed without a user interaction', error);
|
|
287
|
+
return;
|
|
288
|
+
|
|
289
|
+
case 'AbortError':
|
|
290
|
+
debug('api call', 'handleError', 'the action has been aborted for some reason', error);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
debug('api call', 'handleError', error);
|
|
295
|
+
|
|
296
|
+
// Detect if the playback can continue a bit
|
|
297
|
+
const canContinueTemporarily =
|
|
298
|
+
media &&
|
|
299
|
+
(media.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
|
|
300
|
+
media.readyState === HTMLMediaElement.HAVE_FUTURE_DATA ||
|
|
301
|
+
media.readyState === HTMLMediaElement.HAVE_CURRENT_DATA);
|
|
302
|
+
|
|
303
|
+
// If a connectivity error occurs we may need to enter in stalled mode unless we can wait a bit
|
|
304
|
+
if (
|
|
305
|
+
error instanceof MediaError &&
|
|
306
|
+
(error.code === MediaError.MEDIA_ERR_NETWORK || error.code === MediaError.MEDIA_ERR_DECODE) &&
|
|
307
|
+
!canContinueTemporarily
|
|
308
|
+
) {
|
|
309
|
+
this.stalled();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// To this point, there is a big chance the media is stalled.
|
|
314
|
+
// We start an observer to remind as soon as an irregularity occurs on the time update
|
|
315
|
+
state.stallDetection = true;
|
|
316
|
+
updateObserver.remind(() => {
|
|
317
|
+
// The last time update is a bit old, the media is most probably stalled now
|
|
318
|
+
if (updateObserver.elapsed >= config.stalledDetectionDelay) {
|
|
319
|
+
this.stalled();
|
|
320
|
+
}
|
|
321
|
+
}, config.stalledDetectionDelay);
|
|
322
|
+
|
|
323
|
+
updateObserver.start();
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
ready() {
|
|
327
|
+
if (!state.ready) {
|
|
328
|
+
state.ready = true;
|
|
329
|
+
this.trigger('ready');
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
detectStalledNetwork() {
|
|
334
|
+
// Install an observer that will watch the network state after a small delay.
|
|
335
|
+
// It is needed since the network state may need time to settle.
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
if (
|
|
338
|
+
media &&
|
|
339
|
+
media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE &&
|
|
340
|
+
media.readyState === HTMLMediaElement.HAVE_NOTHING
|
|
341
|
+
) {
|
|
342
|
+
if (!state.ready) {
|
|
343
|
+
this.trigger('ready');
|
|
344
|
+
}
|
|
345
|
+
this.stalled();
|
|
346
|
+
}
|
|
347
|
+
}, config.stalledDetectionDelay);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
stalled(position) {
|
|
351
|
+
debug('api call', 'stalled');
|
|
352
|
+
|
|
353
|
+
if (media) {
|
|
354
|
+
if ('undefined' !== typeof position) {
|
|
355
|
+
state.stalledAt = position;
|
|
356
|
+
} else {
|
|
357
|
+
state.stalledAt = timeObserver.position;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
state.stalled = true;
|
|
361
|
+
state.stallDetection = false;
|
|
362
|
+
updateObserver.forget().stop();
|
|
363
|
+
|
|
364
|
+
this.pause();
|
|
365
|
+
this.trigger('stalled');
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
recover() {
|
|
369
|
+
debug('api call', 'recover');
|
|
370
|
+
|
|
371
|
+
state.stalled = false;
|
|
372
|
+
state.stallDetection = false;
|
|
373
|
+
if (media) {
|
|
374
|
+
// Special processing of video player to prevent visual glitch while reloading
|
|
375
|
+
if (media.tagName === 'VIDEO') {
|
|
376
|
+
// Temporarily set the size of the media to prevent a shrink while reloading it
|
|
377
|
+
$media.width($media.width());
|
|
378
|
+
$media.height($media.height());
|
|
379
|
+
$media.on('loadedmetadata.recover', () => {
|
|
380
|
+
$media.off('loadedmetadata.recover');
|
|
381
|
+
$media.css({ width: '', height: '' });
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
media.load();
|
|
386
|
+
if (state.stalledAt) {
|
|
387
|
+
this.seek(state.stalledAt);
|
|
388
|
+
}
|
|
389
|
+
if ((state.playback && !state.playing) || state.playedViaApi) {
|
|
390
|
+
this.play();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
|
|
223
395
|
destroy() {
|
|
224
396
|
debug('api call', 'destroy');
|
|
225
397
|
|
|
226
398
|
this.stop();
|
|
227
399
|
this.removeAllListeners();
|
|
400
|
+
updateObserver.forget();
|
|
401
|
+
timeObserver.removeAllListeners();
|
|
228
402
|
|
|
229
403
|
if ($media) {
|
|
230
404
|
$media.off(ns).remove();
|
|
@@ -232,9 +406,7 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
232
406
|
|
|
233
407
|
$media = void 0;
|
|
234
408
|
media = void 0;
|
|
235
|
-
|
|
236
|
-
loaded = false;
|
|
237
|
-
stalled = false;
|
|
409
|
+
state = {};
|
|
238
410
|
},
|
|
239
411
|
|
|
240
412
|
getMedia() {
|
|
@@ -305,7 +477,10 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
305
477
|
|
|
306
478
|
if (media) {
|
|
307
479
|
media.currentTime = parseFloat(time);
|
|
308
|
-
|
|
480
|
+
state.seekedViaApi = true;
|
|
481
|
+
state.seekAt = media.currentTime;
|
|
482
|
+
timeObserver.seek(media.currentTime);
|
|
483
|
+
if (!state.playback) {
|
|
309
484
|
this.play();
|
|
310
485
|
}
|
|
311
486
|
}
|
|
@@ -315,7 +490,11 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
315
490
|
debug('api call', 'play');
|
|
316
491
|
|
|
317
492
|
if (media) {
|
|
318
|
-
|
|
493
|
+
state.playedViaApi = true;
|
|
494
|
+
const startPlayPromise = media.play();
|
|
495
|
+
if ('undefined' !== typeof startPlayPromise) {
|
|
496
|
+
startPlayPromise.catch(error => this.handleError(error));
|
|
497
|
+
}
|
|
319
498
|
}
|
|
320
499
|
},
|
|
321
500
|
|
|
@@ -323,6 +502,9 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
323
502
|
debug('api call', 'pause');
|
|
324
503
|
|
|
325
504
|
if (media) {
|
|
505
|
+
if (!media.paused) {
|
|
506
|
+
state.pausedViaApi = true;
|
|
507
|
+
}
|
|
326
508
|
media.pause();
|
|
327
509
|
}
|
|
328
510
|
},
|
|
@@ -330,16 +512,16 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
330
512
|
stop() {
|
|
331
513
|
debug('api call', 'stop');
|
|
332
514
|
|
|
333
|
-
if (media && playback) {
|
|
515
|
+
if (media && media.duration && state.playback && !state.stalled) {
|
|
334
516
|
media.currentTime = media.duration;
|
|
335
517
|
}
|
|
336
518
|
},
|
|
337
519
|
|
|
338
|
-
mute(
|
|
339
|
-
debug('api call', 'mute',
|
|
520
|
+
mute(muted) {
|
|
521
|
+
debug('api call', 'mute', muted);
|
|
340
522
|
|
|
341
523
|
if (media) {
|
|
342
|
-
media.muted = !!
|
|
524
|
+
media.muted = !!muted;
|
|
343
525
|
}
|
|
344
526
|
},
|
|
345
527
|
|
|
@@ -353,32 +535,30 @@ export default function html5PlayerFactory($container, config = {}) {
|
|
|
353
535
|
return mute;
|
|
354
536
|
},
|
|
355
537
|
|
|
356
|
-
addMedia(src,
|
|
357
|
-
debug('api call', 'addMedia', src,
|
|
538
|
+
addMedia(src, srcType) {
|
|
539
|
+
debug('api call', 'addMedia', src, srcType);
|
|
358
540
|
|
|
359
541
|
if (media) {
|
|
360
|
-
if (!support.checkSupport(media,
|
|
542
|
+
if (!support.checkSupport(media, srcType)) {
|
|
361
543
|
return false;
|
|
362
544
|
}
|
|
363
545
|
}
|
|
364
546
|
|
|
365
547
|
if (src && $media) {
|
|
366
|
-
$media.append(sourceTpl({ src, type }));
|
|
548
|
+
$media.append(sourceTpl({ src, type: srcType }));
|
|
367
549
|
return true;
|
|
368
550
|
}
|
|
369
551
|
return false;
|
|
370
552
|
},
|
|
371
553
|
|
|
372
|
-
setMedia(src,
|
|
373
|
-
debug('api call', 'setMedia', src,
|
|
554
|
+
setMedia(src, srcType) {
|
|
555
|
+
debug('api call', 'setMedia', src, srcType);
|
|
374
556
|
|
|
375
557
|
if ($media) {
|
|
376
558
|
$media.empty();
|
|
377
|
-
return this.addMedia(src,
|
|
559
|
+
return this.addMedia(src, srcType);
|
|
378
560
|
}
|
|
379
561
|
return false;
|
|
380
562
|
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
return eventifier(player);
|
|
563
|
+
});
|
|
384
564
|
}
|
|
@@ -343,6 +343,43 @@ $controlsHeight: 36px;
|
|
|
343
343
|
bottom: 0;
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
|
+
|
|
347
|
+
&.stalled {
|
|
348
|
+
.player {
|
|
349
|
+
.player-overlay {
|
|
350
|
+
[data-control="reload"] {
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
background-color: #000;
|
|
354
|
+
margin: 0;
|
|
355
|
+
flex-wrap: wrap;
|
|
356
|
+
padding: 5px 5px 5px 50px;
|
|
357
|
+
text-align: left;
|
|
358
|
+
line-height: 2.3rem;
|
|
359
|
+
&.reload {
|
|
360
|
+
width: calc(100% + 2px);
|
|
361
|
+
font-size: 20px;
|
|
362
|
+
line-height: 20px;
|
|
363
|
+
min-height: 36px;
|
|
364
|
+
|
|
365
|
+
.icon {
|
|
366
|
+
text-shadow: none;
|
|
367
|
+
position: absolute;
|
|
368
|
+
left: 0;
|
|
369
|
+
font-size: 2rem;
|
|
370
|
+
font-weight: bold;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.message {
|
|
374
|
+
text-shadow: none;
|
|
375
|
+
font-size: 1.3rem;
|
|
376
|
+
margin-right: 5px;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
346
383
|
}
|
|
347
384
|
|
|
348
385
|
&.ready {
|
|
@@ -355,9 +392,11 @@ $controlsHeight: 36px;
|
|
|
355
392
|
cursor: pointer;
|
|
356
393
|
}
|
|
357
394
|
|
|
358
|
-
.
|
|
359
|
-
|
|
360
|
-
|
|
395
|
+
&:not(.audio) {
|
|
396
|
+
.player:hover {
|
|
397
|
+
[data-control="play"] {
|
|
398
|
+
display: inline-block;
|
|
399
|
+
}
|
|
361
400
|
}
|
|
362
401
|
}
|
|
363
402
|
|
|
@@ -375,9 +414,11 @@ $controlsHeight: 36px;
|
|
|
375
414
|
cursor: pointer;
|
|
376
415
|
}
|
|
377
416
|
|
|
378
|
-
.
|
|
379
|
-
|
|
380
|
-
|
|
417
|
+
&:not(.audio) {
|
|
418
|
+
.player:hover {
|
|
419
|
+
[data-control="pause"] {
|
|
420
|
+
display: inline-block;
|
|
421
|
+
}
|
|
381
422
|
}
|
|
382
423
|
}
|
|
383
424
|
}
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
/**
|
|
20
20
|
* A Regex to detect Apple mobile browsers
|
|
21
21
|
* @type {RegExp}
|
|
22
|
+
* @private
|
|
22
23
|
*/
|
|
23
24
|
const reAppleMobiles = /ip(hone|od)/i;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* A list of MIME types with codec declaration
|
|
27
28
|
* @type {Object}
|
|
29
|
+
* @private
|
|
28
30
|
*/
|
|
29
31
|
const supportedMimeTypes = {
|
|
30
32
|
// video
|
|
@@ -38,6 +40,15 @@ const supportedMimeTypes = {
|
|
|
38
40
|
'audio/wav': 'audio/wav; codecs="1"'
|
|
39
41
|
};
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Checks support for a MIME type.
|
|
45
|
+
* @param {HTMLMediaElement} media The media element on which check support
|
|
46
|
+
* @param {String} mimeType A MIME type to check the support for
|
|
47
|
+
* @returns {string}
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
const findSupport = (media, mimeType) => media.canPlayType(mimeType).replace(/no/, '');
|
|
51
|
+
|
|
41
52
|
/**
|
|
42
53
|
* Support detection
|
|
43
54
|
* @type {Object}
|
|
@@ -51,13 +62,15 @@ export default {
|
|
|
51
62
|
* @private
|
|
52
63
|
*/
|
|
53
64
|
checkSupport(media, mimeType) {
|
|
54
|
-
const support =
|
|
55
|
-
|
|
65
|
+
const support = media.canPlayType;
|
|
56
66
|
if (support && mimeType) {
|
|
57
|
-
return !!
|
|
67
|
+
return !!(
|
|
68
|
+
(supportedMimeTypes[mimeType] && findSupport(media, supportedMimeTypes[mimeType])) ||
|
|
69
|
+
findSupport(media, mimeType)
|
|
70
|
+
);
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
return support;
|
|
73
|
+
return !!support;
|
|
61
74
|
},
|
|
62
75
|
|
|
63
76
|
/**
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
</a>
|
|
10
10
|
<a class="action reload" data-control="reload">
|
|
11
11
|
<div class="icon icon-reload" title="{{__ 'Reload'}}"></div>
|
|
12
|
-
<div class="message">{{__ 'You are encountering a prolonged connectivity loss.'}}</div>
|
|
13
|
-
<div class="message">{{__ 'Click to reload.'}}</div>
|
|
12
|
+
<div class="message">{{__ 'You are encountering a prolonged connectivity loss.'}} {{__ 'Click to reload.'}}</div>
|
|
14
13
|
</a>
|
|
15
14
|
</div>
|
|
16
15
|
</div>
|