videojs-mobile-ui 0.8.0 → 0.9.0-beta.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.
@@ -1,21 +1,34 @@
1
- /*! @name videojs-mobile-ui @version 0.8.0 @license MIT */
1
+ /*! @name videojs-mobile-ui @version 0.9.0-beta.3 @license MIT */
2
2
  import videojs from 'video.js';
3
- import _inheritsLoose from '@babel/runtime/helpers/inheritsLoose';
4
3
  import window from 'global/window';
5
4
 
6
- var version = "0.8.0";
5
+ var version = "0.9.0-beta.3";
6
+
7
+ /**
8
+ * @file touchOverlay.js
9
+ * Touch UI component
10
+ */
11
+
12
+ /** @import Player from 'video.js/dist/types/player' */
13
+
14
+ const Component = videojs.getComponent('Component');
15
+ const dom = videojs.dom || videojs;
16
+ const debounce = (callback, wait) => {
17
+ let timeoutId = null;
18
+ return (...args) => {
19
+ window.clearTimeout(timeoutId);
20
+ timeoutId = window.setTimeout(() => {
21
+ callback.apply(null, args);
22
+ }, wait);
23
+ };
24
+ };
7
25
 
8
- var Component = videojs.getComponent('Component');
9
- var dom = videojs.dom || videojs;
10
26
  /**
11
27
  * The `TouchOverlay` is an overlay to capture tap events.
12
28
  *
13
29
  * @extends Component
14
30
  */
15
-
16
- var TouchOverlay = /*#__PURE__*/function (_Component) {
17
- _inheritsLoose(TouchOverlay, _Component);
18
-
31
+ class TouchOverlay extends Component {
19
32
  /**
20
33
  * Creates an instance of the this class.
21
34
  *
@@ -25,46 +38,80 @@ var TouchOverlay = /*#__PURE__*/function (_Component) {
25
38
  * @param {Object} [options]
26
39
  * The key/value store of player options.
27
40
  */
28
- function TouchOverlay(player, options) {
29
- var _this;
30
-
31
- _this = _Component.call(this, player, options) || this;
32
- _this.seekSeconds = options.seekSeconds;
33
- _this.tapTimeout = options.tapTimeout; // Add play toggle overlay
34
-
35
- _this.addChild('playToggle', {}); // Clear overlay when playback starts or with control fade
36
-
37
-
38
- player.on(['playing', 'userinactive'], function (e) {
39
- _this.removeClass('show-play-toggle');
40
- }); // A 0 inactivity timeout won't work here
41
+ constructor(player, options) {
42
+ super(player, options);
43
+ this.seekSeconds = options.seekSeconds;
44
+ this.tapTimeout = options.tapTimeout;
45
+ this.taps = 0;
46
+
47
+ // Add play toggle overlay
48
+ this.addChild('playToggle', {});
49
+
50
+ // Clear overlay when playback starts or with control fade
51
+ player.on(['playing', 'userinactive'], e => {
52
+ this.removeClass('show-play-toggle');
53
+ });
41
54
 
42
- if (_this.player_.options_.inactivityTimeout === 0) {
43
- _this.player_.options_.inactivityTimeout = 5000;
55
+ // A 0 inactivity timeout won't work here
56
+ if (this.player_.options_.inactivityTimeout === 0) {
57
+ this.player_.options_.inactivityTimeout = 5000;
44
58
  }
45
59
 
46
- _this.enable();
60
+ /**
61
+ * Debounced tap handler.
62
+ * Seeks number of (taps - 1) * configured seconds to skip.
63
+ * One tap is a non-op
64
+ *
65
+ * @param {Event} event
66
+ */
67
+ this.handleTaps_ = debounce(event => {
68
+ const increment = (this.taps - 1) * this.seekSeconds;
69
+ this.taps = 0;
70
+ if (increment < 1) {
71
+ return;
72
+ }
73
+ const rect = this.el_.getBoundingClientRect();
74
+ const x = event.changedTouches[0].clientX - rect.left;
75
+
76
+ // Check if double tap is in left or right area
77
+ if (x < rect.width * 0.4) {
78
+ this.player_.currentTime(Math.max(0, this.player_.currentTime() - increment));
79
+ this.addClass('reverse');
80
+ } else if (x > rect.width - rect.width * 0.4) {
81
+ this.player_.currentTime(Math.min(this.player_.duration(), this.player_.currentTime() + increment));
82
+ this.removeClass('reverse');
83
+ } else {
84
+ return;
85
+ }
47
86
 
48
- return _this;
87
+ // Remove play toggle if showing
88
+ this.removeClass('show-play-toggle');
89
+
90
+ // Remove and readd class to trigger animation
91
+ this.setAttribute('data-skip-text', `${increment} ${this.localize('seconds')}`);
92
+ this.removeClass('skip');
93
+ window.requestAnimationFrame(() => {
94
+ this.addClass('skip');
95
+ });
96
+ }, this.tapTimeout);
97
+ this.enable();
49
98
  }
99
+
50
100
  /**
51
101
  * Builds the DOM element.
52
102
  *
53
103
  * @return {Element}
54
104
  * The DOM element.
55
105
  */
56
-
57
-
58
- var _proto = TouchOverlay.prototype;
59
-
60
- _proto.createEl = function createEl() {
61
- var el = dom.createEl('div', {
106
+ createEl() {
107
+ const el = dom.createEl('div', {
62
108
  className: 'vjs-touch-overlay',
63
109
  // Touch overlay is not tabbable.
64
110
  tabIndex: -1
65
111
  });
66
112
  return el;
67
113
  }
114
+
68
115
  /**
69
116
  * Debounces to either handle a delayed single tap, or a double tap
70
117
  *
@@ -72,111 +119,166 @@ var TouchOverlay = /*#__PURE__*/function (_Component) {
72
119
  * The touch event
73
120
  *
74
121
  */
75
- ;
76
-
77
- _proto.handleTap = function handleTap(event) {
78
- var _this2 = this;
79
-
122
+ handleTap(event) {
80
123
  // Don't handle taps on the play button
81
124
  if (event.target !== this.el_) {
82
125
  return;
83
126
  }
84
-
85
- event.preventDefault();
86
-
87
- if (this.firstTapCaptured) {
88
- this.firstTapCaptured = false;
89
-
90
- if (this.timeout) {
91
- window.clearTimeout(this.timeout);
92
- }
93
-
94
- this.handleDoubleTap(event);
95
- } else {
96
- this.firstTapCaptured = true;
97
- this.timeout = window.setTimeout(function () {
98
- _this2.firstTapCaptured = false;
99
-
100
- _this2.handleSingleTap(event);
101
- }, this.tapTimeout);
127
+ if (event.cancelable) {
128
+ event.preventDefault();
129
+ }
130
+ this.taps += 1;
131
+ if (this.taps === 1) {
132
+ this.removeClass('skip');
133
+ this.toggleClass('show-play-toggle');
102
134
  }
135
+ this.handleTaps_(event);
103
136
  }
137
+
104
138
  /**
105
- * Toggles display of play toggle
106
- *
107
- * @param {Event} event
108
- * The touch event
109
- *
139
+ * Enables touch handler
110
140
  */
111
- ;
112
-
113
- _proto.handleSingleTap = function handleSingleTap(event) {
114
- this.removeClass('skip');
115
- this.toggleClass('show-play-toggle');
141
+ enable() {
142
+ this.firstTapCaptured = false;
143
+ this.on('touchend', this.handleTap);
116
144
  }
145
+
117
146
  /**
118
- * Seeks by configured number of seconds if left or right part of video double tapped
119
- *
120
- * @param {Event} event
121
- * The touch event
122
- *
147
+ * Disables touch handler
123
148
  */
124
- ;
125
-
126
- _proto.handleDoubleTap = function handleDoubleTap(event) {
127
- var _this3 = this;
149
+ disable() {
150
+ this.off('touchend', this.handleTap);
151
+ }
152
+ }
153
+ Component.registerComponent('TouchOverlay', TouchOverlay);
128
154
 
129
- var rect = this.el_.getBoundingClientRect();
130
- var x = event.changedTouches[0].clientX - rect.left; // Check if double tap is in left or right area
155
+ /**
156
+ * Sets up swiping to enter and exit fullscreen.
157
+ *
158
+ * @param {Object} player
159
+ * The player to initialise on.
160
+ * @param {Object} pluginOptions
161
+ * The options used by the mobile ui plugin.
162
+ */
163
+ const initSwipe = (player, pluginOptions) => {
164
+ const {
165
+ swipeToFullscreen,
166
+ swipeFromFullscreen
167
+ } = pluginOptions.fullscreen;
168
+ if (swipeToFullscreen) {
169
+ player.addClass('using-fs-swipe-up');
170
+ }
171
+ if (swipeFromFullscreen) {
172
+ player.addClass('using-fs-swipe-down');
173
+ }
174
+ let touchStartY = 0;
175
+ let couldBeSwiping = false;
176
+ const swipeThreshold = 30;
131
177
 
132
- if (x < rect.width * 0.4) {
133
- this.player_.currentTime(Math.max(0, this.player_.currentTime() - this.seekSeconds));
134
- this.addClass('reverse');
135
- } else if (x > rect.width - rect.width * 0.4) {
136
- this.player_.currentTime(Math.min(this.player_.duration(), this.player_.currentTime() + this.seekSeconds));
137
- this.removeClass('reverse');
138
- } else {
178
+ /**
179
+ * Monitor the possible start of a swipe
180
+ *
181
+ * @param {TouchEvent} e Triggering touch event
182
+ */
183
+ const onStart = e => {
184
+ const isFullscreen = player.isFullscreen();
185
+ if (!isFullscreen && !swipeToFullscreen || isFullscreen && !swipeFromFullscreen) {
186
+ couldBeSwiping = false;
139
187
  return;
140
- } // Remove play toggle if showing
141
-
142
-
143
- this.removeClass('show-play-toggle'); // Remove and readd class to trigger animation
188
+ }
189
+ touchStartY = e.changedTouches[0].clientY;
190
+ couldBeSwiping = true;
191
+ player.tech_.el().style.transition = '';
192
+ };
144
193
 
145
- this.removeClass('skip');
146
- window.requestAnimationFrame(function () {
147
- _this3.addClass('skip');
148
- });
149
- }
150
194
  /**
151
- * Enables touch handler
195
+ * Monitor the movement of a swipe
196
+ *
197
+ * @param {TouchEvent} e Triggering touch event
152
198
  */
153
- ;
199
+ const onMove = e => {
200
+ if (!couldBeSwiping) {
201
+ return;
202
+ }
203
+ const currentY = e.touches[0].clientY;
204
+ const deltaY = touchStartY - currentY;
205
+ const isFullscreen = player.isFullscreen();
206
+ let scale = 1;
207
+ if (!isFullscreen && deltaY > 0) {
208
+ // Swiping up to enter fullscreen: Zoom in (Max 1.1)
209
+ scale = 1 + Math.min(0.1, deltaY / 500);
210
+ player.tech_.el().style.transform = `scale(${scale})`;
211
+ } else if (isFullscreen && deltaY < 0) {
212
+ // Swiping down to exit fullscreen: Zoom out (Min 0.9)
213
+ scale = 1 - Math.min(0.1, Math.abs(deltaY) / 500);
214
+ player.tech_.el().style.transform = `scale(${scale})`;
215
+ }
216
+ };
154
217
 
155
- _proto.enable = function enable() {
156
- this.firstTapCaptured = false;
157
- this.on('touchend', this.handleTap);
158
- }
159
218
  /**
160
- * Disables touch handler
219
+ * Monitor the touch end to determine a valid swipe
220
+ *
221
+ * @param {TouchEvent} e Triggering touch event
161
222
  */
162
- ;
163
-
164
- _proto.disable = function disable() {
165
- this.off('touchend', this.handleTap);
223
+ const onEnd = e => {
224
+ if (!couldBeSwiping) {
225
+ return;
226
+ }
227
+ couldBeSwiping = false;
228
+ player.tech_.el().style.transition = 'transform 0.3s ease-out';
229
+ player.tech_.el().style.transform = 'scale(1)';
230
+ if (e.type === 'touchcancel') {
231
+ return;
232
+ }
233
+ const touchEndY = e.changedTouches[0].clientY;
234
+ const deltaY = touchStartY - touchEndY;
235
+ if (deltaY > swipeThreshold && !player.isFullscreen()) {
236
+ player.requestFullscreen().catch(err => {
237
+ player.log.warn('Browser refused fullscreen', err);
238
+ });
239
+ } else if (deltaY < -swipeThreshold && player.isFullscreen()) {
240
+ player.exitFullscreen();
241
+ }
166
242
  };
243
+ player.el().addEventListener('touchstart', onStart, {
244
+ passive: true
245
+ });
246
+ player.el().addEventListener('touchmove', onMove, {
247
+ passive: true
248
+ });
249
+ player.el().addEventListener('touchend', onEnd, {
250
+ passive: true
251
+ });
252
+ player.el().addEventListener('touchcancel', onEnd, {
253
+ passive: true
254
+ });
255
+ player.on('dispose', () => {
256
+ player.el().removeEventListener('touchstart', onStart, {
257
+ passive: true
258
+ });
259
+ player.el().removeEventListener('touchmove', onMove, {
260
+ passive: true
261
+ });
262
+ player.el().removeEventListener('touchend', onEnd, {
263
+ passive: true
264
+ });
265
+ player.el().removeEventListener('touchcancel', onEnd, {
266
+ passive: true
267
+ });
268
+ player.tech_.el().style.transform = '';
269
+ player.tech_.el().style.transition = '';
270
+ });
271
+ };
167
272
 
168
- return TouchOverlay;
169
- }(Component);
170
-
171
- Component.registerComponent('TouchOverlay', TouchOverlay);
172
-
173
- var defaults = {
273
+ // Default options for the plugin.
274
+ const defaults = {
174
275
  fullscreen: {
175
276
  enterOnRotate: true,
176
277
  exitOnRotate: true,
177
278
  lockOnRotate: true,
178
279
  lockToLandscapeOnEnter: false,
179
- iOS: false,
280
+ swipeToFullscreen: false,
281
+ swipeFromFullscreen: false,
180
282
  disabled: false
181
283
  },
182
284
  touchControls: {
@@ -186,37 +288,33 @@ var defaults = {
186
288
  disabled: false
187
289
  }
188
290
  };
189
- var screen = window.screen;
291
+ const screen = window.screen;
292
+ const registerPlugin = videojs.registerPlugin || videojs.plugin;
293
+
190
294
  /**
191
295
  * Gets 'portrait' or 'lanscape' from the two orientation APIs
192
296
  *
193
297
  * @return {string} orientation
194
298
  */
195
-
196
- var getOrientation = function getOrientation() {
299
+ const getOrientation = () => {
197
300
  if (screen) {
198
301
  // Prefer the string over angle, as 0° can be landscape on some tablets
199
- var orientationString = ((screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation || '').split('-')[0];
200
-
302
+ const orientationString = ((screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation || '').split('-')[0];
201
303
  if (orientationString === 'landscape' || orientationString === 'portrait') {
202
304
  return orientationString;
203
305
  }
204
- } // iOS only supports window.orientation
205
-
306
+ }
206
307
 
308
+ // iOS only supports window.orientation
207
309
  if (typeof window.orientation === 'number') {
208
310
  if (window.orientation === 0 || window.orientation === 180) {
209
311
  return 'portrait';
210
312
  }
211
-
212
313
  return 'landscape';
213
314
  }
214
-
215
315
  return 'portrait';
216
- }; // Cross-compatibility for Video.js 5 and 6.
217
-
316
+ };
218
317
 
219
- var registerPlugin = videojs.registerPlugin || videojs.plugin;
220
318
  /**
221
319
  * Add UI and event listeners
222
320
  *
@@ -224,63 +322,49 @@ var registerPlugin = videojs.registerPlugin || videojs.plugin;
224
322
  * @param {Player} player
225
323
  * A Video.js player object.
226
324
  *
227
- * @param {Object} [options={}]
325
+ * @param {MobileUiOptions} [options={}]
228
326
  * A plain object containing options for the plugin.
229
327
  */
230
-
231
- var onPlayerReady = function onPlayerReady(player, options) {
328
+ const onPlayerReady = (player, options) => {
232
329
  player.addClass('vjs-mobile-ui');
233
-
234
- if (options.fullscreen.iOS) {
235
- videojs.log.warn('videojs-mobile-ui: `fullscreen.iOS` is deprecated. Use Video.js option `preferFullWindow` instead.');
236
-
237
- if (videojs.browser.IS_IOS && videojs.browser.IOS_VERSION > 9 && !player.el_.ownerDocument.querySelector('.bc-iframe')) {
238
- player.tech_.el_.setAttribute('playsinline', 'playsinline');
239
-
240
- player.tech_.supportsFullScreen = function () {
241
- return false;
242
- };
243
- }
244
- }
245
-
246
330
  if (!options.touchControls.disabled) {
247
331
  if (options.touchControls.disableOnEnd || typeof player.endscreen === 'function') {
248
332
  player.addClass('vjs-mobile-ui-disable-end');
249
- } // Insert before the control bar
250
-
333
+ }
251
334
 
252
- var controlBarIdx;
253
- var versionParts = videojs.VERSION.split('.');
254
- var major = parseInt(versionParts[0], 10);
255
- var minor = parseInt(versionParts[1], 10); // Video.js < 7.7.0 doesn't account for precedding components that don't have elements
335
+ // Insert before the control bar
336
+ let controlBarIdx;
337
+ const versionParts = videojs.VERSION.split('.');
338
+ const major = parseInt(versionParts[0], 10);
339
+ const minor = parseInt(versionParts[1], 10);
256
340
 
341
+ // Video.js < 7.7.0 doesn't account for precedding components that don't have elements
257
342
  if (major < 7 || major === 7 && minor < 7) {
258
343
  controlBarIdx = Array.prototype.indexOf.call(player.el_.children, player.getChild('ControlBar').el_);
259
344
  } else {
260
345
  controlBarIdx = player.children_.indexOf(player.getChild('ControlBar'));
261
346
  }
262
-
263
347
  player.touchOverlay = player.addChild('TouchOverlay', options.touchControls, controlBarIdx);
264
348
  }
265
-
266
349
  if (options.fullscreen.disabled) {
267
350
  return;
268
351
  }
269
-
270
- var locked = false;
271
-
272
- var rotationHandler = function rotationHandler() {
273
- var currentOrientation = getOrientation();
274
-
352
+ if (options.fullscreen.swipeToFullscreen || options.fullscreen.swipeFromFullscreen) {
353
+ initSwipe(player, options);
354
+ }
355
+ let locked = false;
356
+ const rotationHandler = () => {
357
+ const currentOrientation = getOrientation();
275
358
  if (currentOrientation === 'landscape' && options.fullscreen.enterOnRotate) {
276
- if (player.paused() === false) {
277
- player.requestFullscreen();
278
-
359
+ if (!player.paused() && !player.isFullscreen()) {
360
+ player.requestFullscreen().catch(err => {
361
+ player.log.warn('Browser refused fullscreen request:', err);
362
+ });
279
363
  if ((options.fullscreen.lockOnRotate || options.fullscreen.lockToLandscapeOnEnter) && screen.orientation && screen.orientation.lock) {
280
- screen.orientation.lock('landscape').then(function () {
364
+ screen.orientation.lock('landscape').then(() => {
281
365
  locked = true;
282
- }).catch(function (e) {
283
- videojs.log('Browser refused orientation lock:', e);
366
+ }).catch(err => {
367
+ videojs.log.warn('Browser refused orientation lock:', err);
284
368
  });
285
369
  }
286
370
  }
@@ -290,27 +374,25 @@ var onPlayerReady = function onPlayerReady(player, options) {
290
374
  }
291
375
  }
292
376
  };
293
-
294
377
  if (options.fullscreen.enterOnRotate || options.fullscreen.exitOnRotate) {
295
378
  if (videojs.browser.IS_IOS) {
296
379
  window.addEventListener('orientationchange', rotationHandler);
297
- player.on('dispose', function () {
380
+ player.on('dispose', () => {
298
381
  window.removeEventListener('orientationchange', rotationHandler);
299
382
  });
300
383
  } else if (screen.orientation) {
301
384
  // addEventListener('orientationchange') is not a user interaction on Android
302
385
  screen.orientation.onchange = rotationHandler;
303
- player.on('dispose', function () {
386
+ player.on('dispose', () => {
304
387
  screen.orientation.onchange = null;
305
388
  });
306
389
  }
307
390
  }
308
-
309
- player.on('fullscreenchange', function (_) {
391
+ player.on('fullscreenchange', _ => {
310
392
  if (player.isFullscreen() && options.fullscreen.lockToLandscapeOnEnter && getOrientation() === 'portrait') {
311
- screen.orientation.lock('landscape').then(function () {
393
+ screen.orientation.lock('landscape').then(() => {
312
394
  locked = true;
313
- }).catch(function (e) {
395
+ }).catch(e => {
314
396
  videojs.log('Browser refused orientation lock:', e);
315
397
  });
316
398
  } else if (!player.isFullscreen() && locked) {
@@ -318,70 +400,32 @@ var onPlayerReady = function onPlayerReady(player, options) {
318
400
  locked = false;
319
401
  }
320
402
  });
321
- player.on('ended', function (_) {
403
+ player.on('ended', _ => {
322
404
  if (locked === true) {
323
405
  screen.orientation.unlock();
324
406
  locked = false;
325
407
  }
326
408
  });
327
409
  };
410
+
328
411
  /**
329
- * A video.js plugin.
330
- *
331
- * Adds a monile UI for player control, and fullscreen orientation control
412
+ * Adds a mobile UI for player control, and fullscreen orientation control
332
413
  *
333
414
  * @function mobileUi
334
- * @param {Object} [options={}]
335
- * Plugin options.
336
- * @param {boolean} [options.forceForTesting=false]
337
- * Enables the display regardless of user agent, for testing purposes
338
- * @param {Object} [options.fullscreen={}]
339
- * Fullscreen options.
340
- * @param {boolean} [options.fullscreen.disabled=false]
341
- * If true no fullscreen handling except the *deprecated* iOS fullwindow hack
342
- * @param {boolean} [options.fullscreen.enterOnRotate=true]
343
- * Whether to go fullscreen when rotating to landscape
344
- * @param {boolean} [options.fullscreen.exitOnRotate=true]
345
- * Whether to leave fullscreen when rotating to portrait (if not locked)
346
- * @param {boolean} [options.fullscreen.lockOnRotate=true]
347
- * Whether to lock orientation when rotating to landscape
348
- * Unlocked when exiting fullscreen or on 'ended
349
- * @param {boolean} [options.fullscreen.lockToLandscapeOnEnter=false]
350
- * Whether to always lock orientation to landscape on fullscreen mode
351
- * Unlocked when exiting fullscreen or on 'ended'
352
- * @param {boolean} [options.fullscreen.iOS=false]
353
- * Deprecated: Whether to disable iOS's native fullscreen so controls can work
354
- * @param {Object} [options.touchControls={}]
355
- * Touch UI options.
356
- * @param {boolean} [options.touchControls.disabled=false]
357
- * If true no touch controls are added.
358
- * @param {int} [options.touchControls.seekSeconds=10]
359
- * Number of seconds to seek on double-tap
360
- * @param {int} [options.touchControls.tapTimeout=300]
361
- * Interval in ms to be considered a doubletap
362
- * @param {boolean} [options.touchControls.disableOnEnd=false]
363
- * Whether to disable when the video ends (e.g., if there is an endscreen)
364
- * Never shows if the endscreen plugin is present
415
+ * @param {Object} [options={}] Plugin options
365
416
  */
366
-
367
-
368
- var mobileUi = function mobileUi(options) {
369
- var _this = this;
370
-
371
- if (options === void 0) {
372
- options = {};
373
- }
374
-
417
+ const mobileUi = function (options = {}) {
375
418
  if (options.forceForTesting || videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
376
- this.ready(function () {
377
- onPlayerReady(_this, videojs.mergeOptions(defaults, options));
419
+ this.ready(() => {
420
+ onPlayerReady(this, videojs.mergeOptions(defaults, options));
378
421
  });
379
422
  }
380
- }; // Register the plugin with video.js.
381
-
423
+ };
382
424
 
383
- registerPlugin('mobileUi', mobileUi); // Include the version number.
425
+ // Register the plugin with video.js.
426
+ registerPlugin('mobileUi', mobileUi);
384
427
 
428
+ // Include the version number.
385
429
  mobileUi.VERSION = version;
386
430
 
387
- export default mobileUi;
431
+ export { mobileUi as default };