videojs-mobile-ui 0.7.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.
Files changed (45) hide show
  1. package/README.md +114 -46
  2. package/dist/videojs-mobile-ui.cjs.js +275 -223
  3. package/dist/videojs-mobile-ui.css +2 -2
  4. package/dist/videojs-mobile-ui.es.js +270 -217
  5. package/dist/videojs-mobile-ui.js +278 -265
  6. package/dist/videojs-mobile-ui.min.js +2 -2
  7. package/docs/api/TouchOverlay.html +964 -0
  8. package/docs/api/fonts/OpenSans-Bold-webfont.eot +0 -0
  9. package/docs/api/fonts/OpenSans-Bold-webfont.svg +1830 -0
  10. package/docs/api/fonts/OpenSans-Bold-webfont.woff +0 -0
  11. package/docs/api/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  12. package/docs/api/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  13. package/docs/api/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  14. package/docs/api/fonts/OpenSans-Italic-webfont.eot +0 -0
  15. package/docs/api/fonts/OpenSans-Italic-webfont.svg +1830 -0
  16. package/docs/api/fonts/OpenSans-Italic-webfont.woff +0 -0
  17. package/docs/api/fonts/OpenSans-Light-webfont.eot +0 -0
  18. package/docs/api/fonts/OpenSans-Light-webfont.svg +1831 -0
  19. package/docs/api/fonts/OpenSans-Light-webfont.woff +0 -0
  20. package/docs/api/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  21. package/docs/api/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  22. package/docs/api/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  23. package/docs/api/fonts/OpenSans-Regular-webfont.eot +0 -0
  24. package/docs/api/fonts/OpenSans-Regular-webfont.svg +1831 -0
  25. package/docs/api/fonts/OpenSans-Regular-webfont.woff +0 -0
  26. package/docs/api/global.html +957 -0
  27. package/docs/api/index.html +159 -0
  28. package/docs/api/plugin.js.html +221 -0
  29. package/docs/api/scripts/linenumber.js +25 -0
  30. package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  31. package/docs/api/scripts/prettify/lang-css.js +2 -0
  32. package/docs/api/scripts/prettify/prettify.js +28 -0
  33. package/docs/api/styles/jsdoc-default.css +358 -0
  34. package/docs/api/styles/prettify-jsdoc.css +111 -0
  35. package/docs/api/styles/prettify-tomorrow.css +132 -0
  36. package/docs/api/swipeFullscreen.js.html +173 -0
  37. package/docs/api/touchOverlay.js.html +211 -0
  38. package/index.html +206 -65
  39. package/package.json +19 -16
  40. package/src/plugin.css +21 -3
  41. package/src/plugin.js +32 -59
  42. package/src/swipeFullscreen.js +117 -0
  43. package/src/touchOverlay.js +62 -56
  44. package/test/plugin.test.js +128 -20
  45. package/test/swipeFullscreen.test.js +365 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Sets up swiping to enter and exit fullscreen.
3
+ *
4
+ * @param {Object} player
5
+ * The player to initialise on.
6
+ * @param {Object} pluginOptions
7
+ * The options used by the mobile ui plugin.
8
+ */
9
+ const initSwipe = (player, pluginOptions) => {
10
+ const {swipeToFullscreen, swipeFromFullscreen} = pluginOptions.fullscreen;
11
+
12
+ if (swipeToFullscreen) {
13
+ player.addClass('using-fs-swipe-up');
14
+ }
15
+ if (swipeFromFullscreen) {
16
+ player.addClass('using-fs-swipe-down');
17
+ }
18
+
19
+ let touchStartY = 0;
20
+ let couldBeSwiping = false;
21
+ const swipeThreshold = 30;
22
+
23
+ /**
24
+ * Monitor the possible start of a swipe
25
+ *
26
+ * @param {TouchEvent} e Triggering touch event
27
+ */
28
+ const onStart = (e) => {
29
+ const isFullscreen = player.isFullscreen();
30
+
31
+ if (
32
+ (!isFullscreen && !swipeToFullscreen) ||
33
+ (isFullscreen && !swipeFromFullscreen)
34
+ ) {
35
+ couldBeSwiping = false;
36
+ return;
37
+ }
38
+
39
+ touchStartY = e.changedTouches[0].clientY;
40
+ couldBeSwiping = true;
41
+ player.tech_.el().style.transition = '';
42
+ };
43
+
44
+ /**
45
+ * Monitor the movement of a swipe
46
+ *
47
+ * @param {TouchEvent} e Triggering touch event
48
+ */
49
+ const onMove = (e) => {
50
+ if (!couldBeSwiping) {
51
+ return;
52
+ }
53
+
54
+ const currentY = e.touches[0].clientY;
55
+ const deltaY = touchStartY - currentY;
56
+ const isFullscreen = player.isFullscreen();
57
+
58
+ let scale = 1;
59
+
60
+ if (!isFullscreen && deltaY > 0) {
61
+ // Swiping up to enter fullscreen: Zoom in (Max 1.1)
62
+ scale = 1 + Math.min(0.1, deltaY / 500);
63
+ player.tech_.el().style.transform = `scale(${scale})`;
64
+ } else if (isFullscreen && deltaY < 0) {
65
+ // Swiping down to exit fullscreen: Zoom out (Min 0.9)
66
+ scale = 1 - Math.min(0.1, Math.abs(deltaY) / 500);
67
+ player.tech_.el().style.transform = `scale(${scale})`;
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Monitor the touch end to determine a valid swipe
73
+ *
74
+ * @param {TouchEvent} e Triggering touch event
75
+ */
76
+ const onEnd = (e) => {
77
+ if (!couldBeSwiping) {
78
+ return;
79
+ }
80
+ couldBeSwiping = false;
81
+
82
+ player.tech_.el().style.transition = 'transform 0.3s ease-out';
83
+ player.tech_.el().style.transform = 'scale(1)';
84
+
85
+ if (e.type === 'touchcancel') {
86
+ return;
87
+ }
88
+
89
+ const touchEndY = e.changedTouches[0].clientY;
90
+ const deltaY = touchStartY - touchEndY;
91
+
92
+ if (deltaY > swipeThreshold && !player.isFullscreen()) {
93
+ player.requestFullscreen().catch((err) => {
94
+ player.log.warn('Browser refused fullscreen', err);
95
+ });
96
+ } else if (deltaY < -swipeThreshold && player.isFullscreen()) {
97
+ player.exitFullscreen();
98
+ }
99
+ };
100
+
101
+ player.el().addEventListener('touchstart', onStart, { passive: true });
102
+ player.el().addEventListener('touchmove', onMove, { passive: true });
103
+ player.el().addEventListener('touchend', onEnd, { passive: true });
104
+ player.el().addEventListener('touchcancel', onEnd, { passive: true });
105
+
106
+ player.on('dispose', () => {
107
+ player.el().removeEventListener('touchstart', onStart, { passive: true });
108
+ player.el().removeEventListener('touchmove', onMove, { passive: true });
109
+ player.el().removeEventListener('touchend', onEnd, { passive: true });
110
+ player.el().removeEventListener('touchcancel', onEnd, { passive: true });
111
+ player.tech_.el().style.transform = '';
112
+ player.tech_.el().style.transition = '';
113
+ });
114
+
115
+ };
116
+
117
+ export default initSwipe;
@@ -6,9 +6,23 @@
6
6
  import videojs from 'video.js';
7
7
  import window from 'global/window';
8
8
 
9
+ /** @import Player from 'video.js/dist/types/player' */
10
+
9
11
  const Component = videojs.getComponent('Component');
10
12
  const dom = videojs.dom || videojs;
11
13
 
14
+ const debounce = (callback, wait) => {
15
+ let timeoutId = null;
16
+
17
+ return (...args) => {
18
+ window.clearTimeout(timeoutId);
19
+
20
+ timeoutId = window.setTimeout(() => {
21
+ callback.apply(null, args);
22
+ }, wait);
23
+ };
24
+ };
25
+
12
26
  /**
13
27
  * The `TouchOverlay` is an overlay to capture tap events.
14
28
  *
@@ -30,6 +44,7 @@ class TouchOverlay extends Component {
30
44
 
31
45
  this.seekSeconds = options.seekSeconds;
32
46
  this.tapTimeout = options.tapTimeout;
47
+ this.taps = 0;
33
48
 
34
49
  // Add play toggle overlay
35
50
  this.addChild('playToggle', {});
@@ -44,6 +59,46 @@ class TouchOverlay extends Component {
44
59
  this.player_.options_.inactivityTimeout = 5000;
45
60
  }
46
61
 
62
+ /**
63
+ * Debounced tap handler.
64
+ * Seeks number of (taps - 1) * configured seconds to skip.
65
+ * One tap is a non-op
66
+ *
67
+ * @param {Event} event
68
+ */
69
+ this.handleTaps_ = debounce(event => {
70
+ const increment = (this.taps - 1) * this.seekSeconds;
71
+
72
+ this.taps = 0;
73
+ if (increment < 1) {
74
+ return;
75
+ }
76
+
77
+ const rect = this.el_.getBoundingClientRect();
78
+ const x = event.changedTouches[0].clientX - rect.left;
79
+
80
+ // Check if double tap is in left or right area
81
+ if (x < rect.width * 0.4) {
82
+ this.player_.currentTime(Math.max(0, this.player_.currentTime() - increment));
83
+ this.addClass('reverse');
84
+ } else if (x > rect.width - (rect.width * 0.4)) {
85
+ this.player_.currentTime(Math.min(this.player_.duration(), this.player_.currentTime() + increment));
86
+ this.removeClass('reverse');
87
+ } else {
88
+ return;
89
+ }
90
+
91
+ // Remove play toggle if showing
92
+ this.removeClass('show-play-toggle');
93
+
94
+ // Remove and readd class to trigger animation
95
+ this.setAttribute('data-skip-text', `${increment} ${this.localize('seconds')}`);
96
+ this.removeClass('skip');
97
+ window.requestAnimationFrame(() => {
98
+ this.addClass('skip');
99
+ });
100
+ }, this.tapTimeout);
101
+
47
102
  this.enable();
48
103
  }
49
104
 
@@ -76,65 +131,16 @@ class TouchOverlay extends Component {
76
131
  return;
77
132
  }
78
133
 
79
- event.preventDefault();
80
-
81
- if (this.firstTapCaptured) {
82
- this.firstTapCaptured = false;
83
- if (this.timeout) {
84
- window.clearTimeout(this.timeout);
85
- }
86
- this.handleDoubleTap(event);
87
- } else {
88
- this.firstTapCaptured = true;
89
- this.timeout = window.setTimeout(() => {
90
- this.firstTapCaptured = false;
91
- this.handleSingleTap(event);
92
- }, this.tapTimeout);
134
+ if (event.cancelable) {
135
+ event.preventDefault();
93
136
  }
94
- }
95
-
96
- /**
97
- * Toggles display of play toggle
98
- *
99
- * @param {Event} event
100
- * The touch event
101
- *
102
- */
103
- handleSingleTap(event) {
104
- this.removeClass('skip');
105
- this.toggleClass('show-play-toggle');
106
- }
107
137
 
108
- /**
109
- * Seeks by configured number of seconds if left or right part of video double tapped
110
- *
111
- * @param {Event} event
112
- * The touch event
113
- *
114
- */
115
- handleDoubleTap(event) {
116
- const rect = this.el_.getBoundingClientRect();
117
- const x = event.changedTouches[0].clientX - rect.left;
118
-
119
- // Check if double tap is in left or right area
120
- if (x < rect.width * 0.4) {
121
- this.player_.currentTime(Math.max(0, this.player_.currentTime() - this.seekSeconds));
122
- this.addClass('reverse');
123
- } else if (x > rect.width - (rect.width * 0.4)) {
124
- this.player_.currentTime(Math.min(this.player_.duration(), this.player_.currentTime() + this.seekSeconds));
125
- this.removeClass('reverse');
126
- } else {
127
- return;
138
+ this.taps += 1;
139
+ if (this.taps === 1) {
140
+ this.removeClass('skip');
141
+ this.toggleClass('show-play-toggle');
128
142
  }
129
-
130
- // Remove play toggle if showing
131
- this.removeClass('show-play-toggle');
132
-
133
- // Remove and readd class to trigger animation
134
- this.removeClass('skip');
135
- window.requestAnimationFrame(() => {
136
- this.addClass('skip');
137
- });
143
+ this.handleTaps_(event);
138
144
  }
139
145
 
140
146
  /**
@@ -9,6 +9,8 @@ import plugin from '../src/plugin';
9
9
 
10
10
  const Player = videojs.getComponent('Player');
11
11
 
12
+ const merge = videojs.obj ? videojs.obj.merge : videojs.mergeOptions;
13
+
12
14
  QUnit.test('the environment is sane', function(assert) {
13
15
  assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists');
14
16
  assert.strictEqual(typeof sinon, 'object', 'sinon exists');
@@ -16,28 +18,31 @@ QUnit.test('the environment is sane', function(assert) {
16
18
  assert.strictEqual(typeof plugin, 'function', 'plugin is a function');
17
19
  });
18
20
 
19
- QUnit.module('videojs-mobile-ui', {
21
+ const beforeEach = function() {
20
22
 
21
- beforeEach() {
23
+ // Mock the environment's timers because certain things - particularly
24
+ // player readiness - are asynchronous in video.js 5. This MUST come
25
+ // before any player is created; otherwise, timers could get created
26
+ // with the actual timer methods!
27
+ this.clock = sinon.useFakeTimers();
22
28
 
23
- // Mock the environment's timers because certain things - particularly
24
- // player readiness - are asynchronous in video.js 5. This MUST come
25
- // before any player is created; otherwise, timers could get created
26
- // with the actual timer methods!
27
- this.clock = sinon.useFakeTimers();
29
+ this.fixture = document.getElementById('qunit-fixture');
30
+ this.video = document.createElement('video');
31
+ this.fixture.appendChild(this.video);
28
32
 
29
- this.fixture = document.getElementById('qunit-fixture');
30
- this.video = document.createElement('video');
31
- this.fixture.appendChild(this.video);
32
- this.player = videojs(this.video);
33
- },
33
+ this.player = videojs(this.video);
34
+ };
34
35
 
35
- afterEach() {
36
- if (!this.player.isDisposed()) {
37
- this.player.dispose();
38
- }
39
- this.clock.restore();
36
+ const afterEach = function() {
37
+ if (!this.player.isDisposed()) {
38
+ this.player.dispose();
40
39
  }
40
+ this.clock.restore();
41
+ };
42
+
43
+ QUnit.module('videojs-mobile-ui', {
44
+ beforeEach,
45
+ afterEach
41
46
  });
42
47
 
43
48
  QUnit.test('registers itself with video.js', function(assert) {
@@ -49,6 +54,14 @@ QUnit.test('registers itself with video.js', function(assert) {
49
54
  );
50
55
  });
51
56
 
57
+ QUnit.test('initialises without errors', function(assert) {
58
+ this.player.mobileUi({ forceForTesting: true });
59
+
60
+ this.clock.tick(1);
61
+
62
+ assert.expect(0);
63
+ });
64
+
52
65
  QUnit.test('inserts element before control bar', function(assert) {
53
66
 
54
67
  this.player.mobileUi({forceForTesting: true});
@@ -84,7 +97,7 @@ QUnit.test('iOS event listeners', function(assert) {
84
97
 
85
98
  const oldBrowser = videojs.browser;
86
99
 
87
- videojs.browser = videojs.mergeOptions(videojs.browser, {
100
+ videojs.browser = merge(videojs.browser, {
88
101
  IS_IOS: true,
89
102
  IS_ANDROID: false
90
103
  });
@@ -124,7 +137,7 @@ QUnit[testOrSkip]('Android event listeners', function(assert) {
124
137
 
125
138
  const oldBrowser = videojs.browser;
126
139
 
127
- videojs.browser = videojs.mergeOptions(videojs.browser, {
140
+ videojs.browser = merge(videojs.browser, {
128
141
  IS_IOS: false,
129
142
  IS_ANDROID: true
130
143
  });
@@ -156,7 +169,7 @@ QUnit[testOrSkip]('Android event listeners skipped if disabled', function(assert
156
169
 
157
170
  const oldBrowser = videojs.browser;
158
171
 
159
- videojs.browser = videojs.mergeOptions(videojs.browser, {
172
+ videojs.browser = merge(videojs.browser, {
160
173
  IS_IOS: false,
161
174
  IS_ANDROID: true
162
175
  });
@@ -178,3 +191,98 @@ QUnit[testOrSkip]('Android event listeners skipped if disabled', function(assert
178
191
 
179
192
  videojs.browser = oldBrowser;
180
193
  });
194
+
195
+ QUnit.test('Adds disable-end class if disableOnEnd option is true', function(assert) {
196
+ this.player.mobileUi({
197
+ forceForTesting: true,
198
+ touchControls: { disableOnEnd: true }
199
+ });
200
+
201
+ this.clock.tick(1);
202
+
203
+ assert.ok(this.player.hasClass('vjs-mobile-ui-disable-end'), 'Class added via option');
204
+ });
205
+
206
+ QUnit.test('Adds disable-end class if endscreen plugin is present', function(assert) {
207
+ this.player.endscreen = () => {};
208
+
209
+ this.player.mobileUi({
210
+ forceForTesting: true,
211
+ touchControls: { disableOnEnd: false }
212
+ });
213
+
214
+ this.clock.tick(1);
215
+
216
+ assert.ok(this.player.hasClass('vjs-mobile-ui-disable-end'), 'Class added via endscreen detection');
217
+ });
218
+
219
+ QUnit.module('TouchOverlay', {
220
+ beforeEach,
221
+ afterEach
222
+ });
223
+
224
+ QUnit.test('TouchOverlay: double tap right seeks forward', function(assert) {
225
+ // Setup
226
+ this.player.mobileUi({ forceForTesting: true });
227
+ this.clock.tick(1);
228
+
229
+ const touchOverlay = this.player.getChild('TouchOverlay');
230
+ const touchEl = touchOverlay.el_;
231
+ let currentTimeCache = 0;
232
+
233
+ // Mock bounding rect so clicks have a defined "right" side
234
+ // Width is 100, so > 60 is right side
235
+ sinon.stub(touchEl, 'getBoundingClientRect').returns({
236
+ left: 0,
237
+ width: 100
238
+ });
239
+
240
+ this.player.currentTime = (time) => {
241
+ if (time === undefined) {
242
+ return currentTimeCache;
243
+ }
244
+ currentTimeCache = time;
245
+ return currentTimeCache;
246
+ };
247
+
248
+ this.player.duration(60);
249
+ this.player.currentTime(10);
250
+
251
+ // Trigger first tap
252
+ touchOverlay.handleTap({
253
+ target: touchEl,
254
+ preventDefault: () => {},
255
+ changedTouches: [{ clientX: 90 }]
256
+ });
257
+
258
+ // Trigger second tap (double tap)
259
+ touchOverlay.handleTap({
260
+ target: touchEl,
261
+ preventDefault: () => {},
262
+ changedTouches: [{ clientX: 90 }]
263
+ });
264
+
265
+ // Fast forward debounce timer (default tapTimeout is 300ms)
266
+ this.clock.tick(310);
267
+ assert.equal(this.player.currentTime(), 20, 'Seeked forward 10 seconds (default)');
268
+
269
+ // Advance enough for requestAnimationFrame to trigger
270
+ this.clock.tick(50);
271
+ assert.ok(touchOverlay.hasClass('skip'), 'Skip animation class added');
272
+ });
273
+
274
+ QUnit.test('TouchOverlay: single tap toggles play/pause visibility', function(assert) {
275
+ this.player.mobileUi({ forceForTesting: true });
276
+ this.clock.tick(1);
277
+
278
+ const touchOverlay = this.player.getChild('TouchOverlay');
279
+
280
+ // Trigger single tap
281
+ touchOverlay.handleTap({
282
+ target: touchOverlay.el_,
283
+ preventDefault: () => {},
284
+ changedTouches: [{ clientX: 50 }]
285
+ });
286
+
287
+ assert.ok(touchOverlay.hasClass('show-play-toggle'), 'Play toggle is visible after single tap');
288
+ });