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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "videojs-mobile-ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0-beta.3",
4
4
  "description": "Mobile tap controls and fullscreen on rotate for Video.js",
5
5
  "main": "dist/videojs-mobile-ui.cjs.js",
6
6
  "module": "dist/videojs-mobile-ui.es.js",
@@ -31,7 +31,7 @@
31
31
  "watch": "npm-run-all -p watch:*",
32
32
  "watch:css": "npm run build:css -- -w",
33
33
  "watch:js": "npm run build:js -- -w",
34
- "prepublishOnly": "npm-run-all build-prod && vjsverify --verbose"
34
+ "prepublishOnly": "npm-run-all build-prod && vjsverify --verbose --skip-es-check"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=14",
@@ -74,21 +74,21 @@
74
74
  "video.js": "^6 || ^7"
75
75
  },
76
76
  "devDependencies": {
77
- "@babel/runtime": "^7.14.0",
78
- "@videojs/generator-helpers": "~2.0.2",
79
- "husky": "^6.0.0",
80
- "jsdoc": "^3.6.7",
81
- "karma": "^6.3.2",
82
- "postcss": "^8.2.13",
77
+ "@babel/runtime": "^7.28.6",
78
+ "@videojs/generator-helpers": "~3.2.0",
79
+ "husky": "^8.0.3",
80
+ "jsdoc": "^4.0.5",
81
+ "karma": "^6.4.4",
82
+ "postcss": "^8.5.6",
83
83
  "postcss-cli": "^8.3.1",
84
- "rollup": "^2.46.0",
85
- "sinon": "^9.1.0",
86
- "video.js": "^6 || ^7",
84
+ "rollup": "^2.79.2",
85
+ "sinon": "^14.0.1",
86
+ "video.js": "^7.21.7",
87
87
  "videojs-generate-karma-config": "~8.0.0",
88
- "videojs-generate-postcss-config": "~3.0.0",
89
- "videojs-generate-rollup-config": "~6.2.0",
90
- "videojs-generator-verify": "^4.1.0",
88
+ "videojs-generate-postcss-config": "^3.0.1",
89
+ "videojs-generate-rollup-config": "^7.0.2",
90
+ "videojs-generator-verify": "^4.1.3",
91
91
  "videojs-languages": "^2.0.0",
92
- "videojs-standard": "^8.0.4"
92
+ "videojs-standard": "^9.1.0"
93
93
  }
94
94
  }
package/src/plugin.css CHANGED
@@ -14,9 +14,9 @@
14
14
  }
15
15
  }
16
16
 
17
- .video-js {
17
+ .video-js.vjs-mobile-ui {
18
18
 
19
- &.vjs-has-started .vjs-touch-overlay {
19
+ &.vjs-has-started:not(.vjs-ad-playing) .vjs-touch-overlay {
20
20
  position: absolute;
21
21
  pointer-events: auto;
22
22
  top: 0;
@@ -30,16 +30,28 @@
30
30
 
31
31
  &.skip {
32
32
  opacity: 0;
33
- animation: fadeAndScale 0.6s linear;
33
+ animation: fadeAndScale 0.8s linear;
34
34
  background-repeat: no-repeat;
35
35
  background-position: 80% center;
36
36
  background-size: 10%;
37
37
  background-image: url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
38
+
39
+ &:after {
40
+ content: attr(data-skip-text);
41
+ position: absolute;
42
+ top: 60%;
43
+ left: 70%;
44
+ }
38
45
  }
39
46
 
40
47
  &.skip.reverse {
41
48
  background-position: 20% center;
42
49
  background-image: url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
50
+
51
+ &:after {
52
+ right: 70%;
53
+ left: unset;
54
+ }
43
55
  }
44
56
 
45
57
  .vjs-play-control {
@@ -83,4 +95,10 @@
83
95
  display: none;
84
96
  }
85
97
 
98
+ &:not(:fullscreen).using-fs-swipe-up,
99
+ &:fullscreen.using-fs-swipe-down {
100
+ touch-action: none;
101
+ overflow: hidden;
102
+ }
103
+
86
104
  }
package/src/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import videojs from 'video.js';
2
2
  import {version as VERSION} from '../package.json';
3
3
  import './touchOverlay.js';
4
+ import initSwipe from './swipeFullscreen.js';
4
5
  import window from 'global/window';
5
6
 
6
7
  // Default options for the plugin.
@@ -10,7 +11,8 @@ const defaults = {
10
11
  exitOnRotate: true,
11
12
  lockOnRotate: true,
12
13
  lockToLandscapeOnEnter: false,
13
- iOS: false,
14
+ swipeToFullscreen: false,
15
+ swipeFromFullscreen: false,
14
16
  disabled: false
15
17
  },
16
18
  touchControls: {
@@ -22,6 +24,7 @@ const defaults = {
22
24
  };
23
25
 
24
26
  const screen = window.screen;
27
+ const registerPlugin = videojs.registerPlugin || videojs.plugin;
25
28
 
26
29
  /**
27
30
  * Gets 'portrait' or 'lanscape' from the two orientation APIs
@@ -49,9 +52,6 @@ const getOrientation = () => {
49
52
  return 'portrait';
50
53
  };
51
54
 
52
- // Cross-compatibility for Video.js 5 and 6.
53
- const registerPlugin = videojs.registerPlugin || videojs.plugin;
54
-
55
55
  /**
56
56
  * Add UI and event listeners
57
57
  *
@@ -59,23 +59,12 @@ const registerPlugin = videojs.registerPlugin || videojs.plugin;
59
59
  * @param {Player} player
60
60
  * A Video.js player object.
61
61
  *
62
- * @param {Object} [options={}]
62
+ * @param {MobileUiOptions} [options={}]
63
63
  * A plain object containing options for the plugin.
64
64
  */
65
65
  const onPlayerReady = (player, options) => {
66
66
  player.addClass('vjs-mobile-ui');
67
67
 
68
- if (options.fullscreen.iOS) {
69
- videojs.log.warn('videojs-mobile-ui: `fullscreen.iOS` is deprecated. Use Video.js option `preferFullWindow` instead.');
70
- if (videojs.browser.IS_IOS && videojs.browser.IOS_VERSION > 9 &&
71
- !player.el_.ownerDocument.querySelector('.bc-iframe')) {
72
- player.tech_.el_.setAttribute('playsinline', 'playsinline');
73
- player.tech_.supportsFullScreen = function() {
74
- return false;
75
- };
76
- }
77
- }
78
-
79
68
  if (!options.touchControls.disabled) {
80
69
 
81
70
  if (options.touchControls.disableOnEnd || typeof player.endscreen === 'function') {
@@ -105,20 +94,26 @@ const onPlayerReady = (player, options) => {
105
94
  return;
106
95
  }
107
96
 
97
+ if (options.fullscreen.swipeToFullscreen || options.fullscreen.swipeFromFullscreen) {
98
+ initSwipe(player, options);
99
+ }
100
+
108
101
  let locked = false;
109
102
 
110
103
  const rotationHandler = () => {
111
104
  const currentOrientation = getOrientation();
112
105
 
113
106
  if (currentOrientation === 'landscape' && options.fullscreen.enterOnRotate) {
114
- if (player.paused() === false) {
115
- player.requestFullscreen();
107
+ if (!player.paused() && !player.isFullscreen()) {
108
+ player.requestFullscreen().catch((err) => {
109
+ player.log.warn('Browser refused fullscreen request:', err);
110
+ });
116
111
  if ((options.fullscreen.lockOnRotate || options.fullscreen.lockToLandscapeOnEnter) &&
117
112
  screen.orientation && screen.orientation.lock) {
118
113
  screen.orientation.lock('landscape').then(() => {
119
114
  locked = true;
120
- }).catch((e) => {
121
- videojs.log('Browser refused orientation lock:', e);
115
+ }).catch((err) => {
116
+ videojs.log.warn('Browser refused orientation lock:', err);
122
117
  });
123
118
  }
124
119
  }
@@ -168,42 +163,10 @@ const onPlayerReady = (player, options) => {
168
163
  };
169
164
 
170
165
  /**
171
- * A video.js plugin.
172
- *
173
- * Adds a monile UI for player control, and fullscreen orientation control
166
+ * Adds a mobile UI for player control, and fullscreen orientation control
174
167
  *
175
168
  * @function mobileUi
176
- * @param {Object} [options={}]
177
- * Plugin options.
178
- * @param {boolean} [options.forceForTesting=false]
179
- * Enables the display regardless of user agent, for testing purposes
180
- * @param {Object} [options.fullscreen={}]
181
- * Fullscreen options.
182
- * @param {boolean} [options.fullscreen.disabled=false]
183
- * If true no fullscreen handling except the *deprecated* iOS fullwindow hack
184
- * @param {boolean} [options.fullscreen.enterOnRotate=true]
185
- * Whether to go fullscreen when rotating to landscape
186
- * @param {boolean} [options.fullscreen.exitOnRotate=true]
187
- * Whether to leave fullscreen when rotating to portrait (if not locked)
188
- * @param {boolean} [options.fullscreen.lockOnRotate=true]
189
- * Whether to lock orientation when rotating to landscape
190
- * Unlocked when exiting fullscreen or on 'ended
191
- * @param {boolean} [options.fullscreen.lockToLandscapeOnEnter=false]
192
- * Whether to always lock orientation to landscape on fullscreen mode
193
- * Unlocked when exiting fullscreen or on 'ended'
194
- * @param {boolean} [options.fullscreen.iOS=false]
195
- * Deprecated: Whether to disable iOS's native fullscreen so controls can work
196
- * @param {Object} [options.touchControls={}]
197
- * Touch UI options.
198
- * @param {boolean} [options.touchControls.disabled=false]
199
- * If true no touch controls are added.
200
- * @param {int} [options.touchControls.seekSeconds=10]
201
- * Number of seconds to seek on double-tap
202
- * @param {int} [options.touchControls.tapTimeout=300]
203
- * Interval in ms to be considered a doubletap
204
- * @param {boolean} [options.touchControls.disableOnEnd=false]
205
- * Whether to disable when the video ends (e.g., if there is an endscreen)
206
- * Never shows if the endscreen plugin is present
169
+ * @param {Object} [options={}] Plugin options
207
170
  */
208
171
  const mobileUi = function(options = {}) {
209
172
  if (options.forceForTesting || videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
@@ -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
  /**