unified-video-framework 1.4.176 → 1.4.177

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.
@@ -96,6 +96,23 @@ export class WebPlayer extends BasePlayer {
96
96
 
97
97
  // Progress bar tooltip state
98
98
  private showTimeTooltip: boolean = false;
99
+
100
+ // Advanced tap handling state
101
+ private tapStartTime: number = 0;
102
+ private tapStartX: number = 0;
103
+ private tapStartY: number = 0;
104
+ private lastTapTime: number = 0;
105
+ private lastTapX: number = 0;
106
+ private tapCount: number = 0;
107
+ private longPressTimer: NodeJS.Timeout | null = null;
108
+ private isLongPressing: boolean = false;
109
+ private longPressPlaybackRate: number = 1;
110
+ private tapResetTimer: NodeJS.Timeout | null = null;
111
+ private fastBackwardInterval: NodeJS.Timeout | null = null;
112
+ private handleSingleTap: () => void = () => {};
113
+ private handleDoubleTap: (tapX: number) => void = () => {};
114
+ private handleLongPress: (tapX: number) => void = () => {};
115
+ private handleLongPressEnd: () => void = () => {};
99
116
 
100
117
  // Autoplay enhancement state
101
118
  private autoplayCapabilities: {
@@ -5922,25 +5939,13 @@ export class WebPlayer extends BasePlayer {
5922
5939
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
5923
5940
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
5924
5941
 
5925
- // Video click behavior - toggle controls on mobile, play/pause on desktop
5926
- this.video.addEventListener('click', (e) => {
5927
- if (this.isMobileDevice()) {
5928
- // Mobile: toggle controls visibility
5929
- e.stopPropagation();
5930
- const wrapper = this.container?.querySelector('.uvf-player-wrapper');
5931
- if (wrapper?.classList.contains('controls-visible')) {
5932
- this.hideControls();
5933
- } else {
5934
- this.showControls();
5935
- if (this.state.isPlaying) {
5936
- this.scheduleHideControls();
5937
- }
5938
- }
5939
- } else {
5940
- // Desktop: toggle play/pause
5942
+ // Video click behavior will be handled by the comprehensive tap system below
5943
+ // Desktop click for play/pause
5944
+ if (!this.isMobileDevice()) {
5945
+ this.video.addEventListener('click', (e) => {
5941
5946
  this.togglePlayPause();
5942
- }
5943
- });
5947
+ });
5948
+ }
5944
5949
 
5945
5950
  // Update play/pause icons
5946
5951
  this.video.addEventListener('play', () => {
@@ -6538,22 +6543,8 @@ export class WebPlayer extends BasePlayer {
6538
6543
  this.lastUserInteraction = Date.now();
6539
6544
  });
6540
6545
 
6541
- // Add double-click to fullscreen support
6542
- this.playerWrapper.addEventListener('dblclick', (e) => {
6543
- // Don't trigger if double-clicking on controls
6544
- const target = e.target as HTMLElement;
6545
- if (!target.closest('.uvf-controls')) {
6546
- e.preventDefault();
6547
- this.debugLog('Double-click detected - attempting fullscreen');
6548
-
6549
- if (!document.fullscreenElement) {
6550
- // Always use the fullscreen button for maximum reliability
6551
- this.triggerFullscreenButton();
6552
- } else {
6553
- this.exitFullscreen();
6554
- }
6555
- }
6556
- });
6546
+ // Advanced tap handling system for mobile
6547
+ this.setupAdvancedTapHandling();
6557
6548
  }
6558
6549
 
6559
6550
  // Add to the video element
@@ -7106,6 +7097,241 @@ export class WebPlayer extends BasePlayer {
7106
7097
  }
7107
7098
  }, timeout);
7108
7099
  }
7100
+
7101
+ /**
7102
+ * Setup advanced tap handling for mobile with:
7103
+ * - Single tap: toggle controls
7104
+ * - Double tap left: skip backward 10s
7105
+ * - Double tap right: skip forward 10s
7106
+ * - Long press left: 2x speed backward
7107
+ * - Long press right: 2x speed forward
7108
+ */
7109
+ private setupAdvancedTapHandling(): void {
7110
+ if (!this.video || !this.playerWrapper) return;
7111
+
7112
+ const DOUBLE_TAP_DELAY = 300; // ms
7113
+ const LONG_PRESS_DELAY = 500; // ms
7114
+ const TAP_MOVEMENT_THRESHOLD = 10; // pixels
7115
+ const SKIP_SECONDS = 10;
7116
+ const FAST_PLAYBACK_RATE = 2;
7117
+
7118
+ const videoElement = this.video;
7119
+ const wrapper = this.playerWrapper;
7120
+
7121
+ // Touch start handler
7122
+ const handleTouchStart = (e: TouchEvent) => {
7123
+ // Ignore if touching controls
7124
+ const target = e.target as HTMLElement;
7125
+ if (target.closest('.uvf-controls')) {
7126
+ return;
7127
+ }
7128
+
7129
+ const touch = e.touches[0];
7130
+ this.tapStartTime = Date.now();
7131
+ this.tapStartX = touch.clientX;
7132
+ this.tapStartY = touch.clientY;
7133
+
7134
+ // Start long press timer
7135
+ this.longPressTimer = setTimeout(() => {
7136
+ this.isLongPressing = true;
7137
+ this.handleLongPress(this.tapStartX);
7138
+ }, LONG_PRESS_DELAY);
7139
+ };
7140
+
7141
+ // Touch move handler
7142
+ const handleTouchMove = (e: TouchEvent) => {
7143
+ const touch = e.touches[0];
7144
+ const deltaX = Math.abs(touch.clientX - this.tapStartX);
7145
+ const deltaY = Math.abs(touch.clientY - this.tapStartY);
7146
+
7147
+ // Cancel long press if moved too much
7148
+ if (deltaX > TAP_MOVEMENT_THRESHOLD || deltaY > TAP_MOVEMENT_THRESHOLD) {
7149
+ if (this.longPressTimer) {
7150
+ clearTimeout(this.longPressTimer);
7151
+ this.longPressTimer = null;
7152
+ }
7153
+ }
7154
+ };
7155
+
7156
+ // Touch end handler
7157
+ const handleTouchEnd = (e: TouchEvent) => {
7158
+ // Clear long press timer
7159
+ if (this.longPressTimer) {
7160
+ clearTimeout(this.longPressTimer);
7161
+ this.longPressTimer = null;
7162
+ }
7163
+
7164
+ // Handle long press end
7165
+ if (this.isLongPressing) {
7166
+ this.handleLongPressEnd();
7167
+ this.isLongPressing = false;
7168
+ return;
7169
+ }
7170
+
7171
+ // Ignore if touching controls
7172
+ const target = e.target as HTMLElement;
7173
+ if (target.closest('.uvf-controls')) {
7174
+ return;
7175
+ }
7176
+
7177
+ const touch = e.changedTouches[0];
7178
+ const touchEndX = touch.clientX;
7179
+ const touchEndY = touch.clientY;
7180
+ const tapDuration = Date.now() - this.tapStartTime;
7181
+
7182
+ // Check if it was a tap (not a drag)
7183
+ const deltaX = Math.abs(touchEndX - this.tapStartX);
7184
+ const deltaY = Math.abs(touchEndY - this.tapStartY);
7185
+
7186
+ if (deltaX > TAP_MOVEMENT_THRESHOLD || deltaY > TAP_MOVEMENT_THRESHOLD) {
7187
+ // It was a drag, not a tap
7188
+ return;
7189
+ }
7190
+
7191
+ // Check if it was a quick tap (not a long press)
7192
+ if (tapDuration > LONG_PRESS_DELAY) {
7193
+ return;
7194
+ }
7195
+
7196
+ // Determine if this is a double tap
7197
+ const now = Date.now();
7198
+ const timeSinceLastTap = now - this.lastTapTime;
7199
+
7200
+ if (timeSinceLastTap < DOUBLE_TAP_DELAY && Math.abs(touchEndX - this.lastTapX) < 100) {
7201
+ // Double tap detected
7202
+ this.tapCount = 2;
7203
+ if (this.tapResetTimer) {
7204
+ clearTimeout(this.tapResetTimer);
7205
+ this.tapResetTimer = null;
7206
+ }
7207
+ this.handleDoubleTap(touchEndX);
7208
+ } else {
7209
+ // First tap or single tap
7210
+ this.tapCount = 1;
7211
+ this.lastTapTime = now;
7212
+ this.lastTapX = touchEndX;
7213
+
7214
+ // Wait to see if there's a second tap
7215
+ if (this.tapResetTimer) {
7216
+ clearTimeout(this.tapResetTimer);
7217
+ }
7218
+ this.tapResetTimer = setTimeout(() => {
7219
+ if (this.tapCount === 1) {
7220
+ // Single tap confirmed
7221
+ this.handleSingleTap();
7222
+ }
7223
+ this.tapCount = 0;
7224
+ }, DOUBLE_TAP_DELAY);
7225
+ }
7226
+ };
7227
+
7228
+ // Single tap: toggle controls
7229
+ const handleSingleTap = () => {
7230
+ this.debugLog('Single tap detected - toggling controls');
7231
+ const wrapper = this.container?.querySelector('.uvf-player-wrapper');
7232
+ if (wrapper?.classList.contains('controls-visible')) {
7233
+ this.hideControls();
7234
+ } else {
7235
+ this.showControls();
7236
+ if (this.state.isPlaying) {
7237
+ this.scheduleHideControls();
7238
+ }
7239
+ }
7240
+ };
7241
+
7242
+ // Double tap: skip backward/forward based on screen side
7243
+ const handleDoubleTap = (tapX: number) => {
7244
+ if (!this.video || !wrapper) return;
7245
+
7246
+ const wrapperRect = wrapper.getBoundingClientRect();
7247
+ const tapPosition = tapX - wrapperRect.left;
7248
+ const wrapperWidth = wrapperRect.width;
7249
+ const isLeftSide = tapPosition < wrapperWidth / 2;
7250
+
7251
+ if (isLeftSide) {
7252
+ // Skip backward
7253
+ const newTime = Math.max(0, this.video.currentTime - SKIP_SECONDS);
7254
+ this.seek(newTime);
7255
+ this.showShortcutIndicator(`-${SKIP_SECONDS}s`);
7256
+ this.debugLog('Double tap left - skip backward');
7257
+ } else {
7258
+ // Skip forward
7259
+ const newTime = Math.min(this.video.duration, this.video.currentTime + SKIP_SECONDS);
7260
+ this.seek(newTime);
7261
+ this.showShortcutIndicator(`+${SKIP_SECONDS}s`);
7262
+ this.debugLog('Double tap right - skip forward');
7263
+ }
7264
+ };
7265
+
7266
+ // Long press: fast forward/rewind based on screen side
7267
+ const handleLongPress = (tapX: number) => {
7268
+ if (!this.video || !wrapper) return;
7269
+
7270
+ const wrapperRect = wrapper.getBoundingClientRect();
7271
+ const tapPosition = tapX - wrapperRect.left;
7272
+ const wrapperWidth = wrapperRect.width;
7273
+ const isLeftSide = tapPosition < wrapperWidth / 2;
7274
+
7275
+ // Save original playback rate
7276
+ this.longPressPlaybackRate = this.video.playbackRate;
7277
+
7278
+ if (isLeftSide) {
7279
+ // Fast backward by setting negative time interval
7280
+ this.showShortcutIndicator(`⏪ 2x`);
7281
+ this.debugLog('Long press left - fast backward');
7282
+ this.startFastBackward();
7283
+ } else {
7284
+ // Fast forward
7285
+ this.video.playbackRate = FAST_PLAYBACK_RATE;
7286
+ this.showShortcutIndicator(`⏩ 2x`);
7287
+ this.debugLog('Long press right - fast forward');
7288
+ }
7289
+ };
7290
+
7291
+ // Long press end: restore normal playback
7292
+ const handleLongPressEnd = () => {
7293
+ if (!this.video) return;
7294
+
7295
+ // Stop fast backward if active
7296
+ this.stopFastBackward();
7297
+
7298
+ // Restore original playback rate
7299
+ this.video.playbackRate = this.longPressPlaybackRate || 1;
7300
+ this.debugLog('Long press ended - restored playback rate');
7301
+ };
7302
+
7303
+ // Bind handlers
7304
+ this.handleSingleTap = handleSingleTap.bind(this);
7305
+ this.handleDoubleTap = handleDoubleTap.bind(this);
7306
+ this.handleLongPress = handleLongPress.bind(this);
7307
+ this.handleLongPressEnd = handleLongPressEnd.bind(this);
7308
+
7309
+ // Attach event listeners
7310
+ videoElement.addEventListener('touchstart', handleTouchStart, { passive: true });
7311
+ videoElement.addEventListener('touchmove', handleTouchMove, { passive: true });
7312
+ videoElement.addEventListener('touchend', handleTouchEnd, { passive: true });
7313
+
7314
+ this.debugLog('Advanced tap handling initialized');
7315
+ }
7316
+
7317
+ // Fast backward using interval-based seeking
7318
+ private startFastBackward(): void {
7319
+ if (!this.video || this.fastBackwardInterval) return;
7320
+
7321
+ this.fastBackwardInterval = setInterval(() => {
7322
+ if (this.video) {
7323
+ const newTime = Math.max(0, this.video.currentTime - 0.1); // Go back 0.1s every frame
7324
+ this.video.currentTime = newTime;
7325
+ }
7326
+ }, 50); // Update every 50ms for smooth backward motion
7327
+ }
7328
+
7329
+ private stopFastBackward(): void {
7330
+ if (this.fastBackwardInterval) {
7331
+ clearInterval(this.fastBackwardInterval);
7332
+ this.fastBackwardInterval = null;
7333
+ }
7334
+ }
7109
7335
 
7110
7336
  private isFullscreen(): boolean {
7111
7337
  return !!(document.fullscreenElement ||