unified-video-framework 1.4.175 → 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: {
@@ -1351,11 +1368,15 @@ export class WebPlayer extends BasePlayer {
1351
1368
  await (this.video as any).webkitEnterFullscreen();
1352
1369
  this.playerWrapper.classList.add('uvf-fullscreen');
1353
1370
  this.emit('onFullscreenChanged', true);
1371
+ // Lock to landscape orientation
1372
+ await this.lockOrientationLandscape();
1354
1373
  return;
1355
1374
  } else if ((this.video as any).webkitRequestFullscreen) {
1356
1375
  await (this.video as any).webkitRequestFullscreen();
1357
1376
  this.playerWrapper.classList.add('uvf-fullscreen');
1358
1377
  this.emit('onFullscreenChanged', true);
1378
+ // Lock to landscape orientation
1379
+ await this.lockOrientationLandscape();
1359
1380
  return;
1360
1381
  }
1361
1382
  } catch (iosError) {
@@ -1421,12 +1442,8 @@ export class WebPlayer extends BasePlayer {
1421
1442
  this.playerWrapper.classList.add('uvf-fullscreen');
1422
1443
  this.emit('onFullscreenChanged', true);
1423
1444
 
1424
- // On Android, suggest orientation for better experience
1425
- if (this.isAndroidDevice()) {
1426
- setTimeout(() => {
1427
- this.showShortcutIndicator('Rotate device to landscape for best experience');
1428
- }, 1000);
1429
- }
1445
+ // Lock to landscape orientation on mobile devices
1446
+ await this.lockOrientationLandscape();
1430
1447
  } else {
1431
1448
  this.debugWarn('All fullscreen methods failed');
1432
1449
 
@@ -1457,6 +1474,8 @@ export class WebPlayer extends BasePlayer {
1457
1474
  this.playerWrapper.classList.remove('uvf-fullscreen');
1458
1475
  }
1459
1476
  this.emit('onFullscreenChanged', false);
1477
+ // Unlock orientation
1478
+ await this.unlockOrientation();
1460
1479
  return;
1461
1480
  }
1462
1481
  } catch (iosError) {
@@ -1510,6 +1529,8 @@ export class WebPlayer extends BasePlayer {
1510
1529
  this.playerWrapper.classList.remove('uvf-fullscreen');
1511
1530
  }
1512
1531
  this.emit('onFullscreenChanged', false);
1532
+ // Unlock orientation
1533
+ await this.unlockOrientation();
1513
1534
  } else {
1514
1535
  this.debugWarn('All exit fullscreen methods failed');
1515
1536
  // Still remove the class to keep UI consistent
@@ -5918,25 +5939,13 @@ export class WebPlayer extends BasePlayer {
5918
5939
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
5919
5940
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
5920
5941
 
5921
- // Video click behavior - toggle controls on mobile, play/pause on desktop
5922
- this.video.addEventListener('click', (e) => {
5923
- if (this.isMobileDevice()) {
5924
- // Mobile: toggle controls visibility
5925
- e.stopPropagation();
5926
- const wrapper = this.container?.querySelector('.uvf-player-wrapper');
5927
- if (wrapper?.classList.contains('controls-visible')) {
5928
- this.hideControls();
5929
- } else {
5930
- this.showControls();
5931
- if (this.state.isPlaying) {
5932
- this.scheduleHideControls();
5933
- }
5934
- }
5935
- } else {
5936
- // 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) => {
5937
5946
  this.togglePlayPause();
5938
- }
5939
- });
5947
+ });
5948
+ }
5940
5949
 
5941
5950
  // Update play/pause icons
5942
5951
  this.video.addEventListener('play', () => {
@@ -6534,22 +6543,8 @@ export class WebPlayer extends BasePlayer {
6534
6543
  this.lastUserInteraction = Date.now();
6535
6544
  });
6536
6545
 
6537
- // Add double-click to fullscreen support
6538
- this.playerWrapper.addEventListener('dblclick', (e) => {
6539
- // Don't trigger if double-clicking on controls
6540
- const target = e.target as HTMLElement;
6541
- if (!target.closest('.uvf-controls')) {
6542
- e.preventDefault();
6543
- this.debugLog('Double-click detected - attempting fullscreen');
6544
-
6545
- if (!document.fullscreenElement) {
6546
- // Always use the fullscreen button for maximum reliability
6547
- this.triggerFullscreenButton();
6548
- } else {
6549
- this.exitFullscreen();
6550
- }
6551
- }
6552
- });
6546
+ // Advanced tap handling system for mobile
6547
+ this.setupAdvancedTapHandling();
6553
6548
  }
6554
6549
 
6555
6550
  // Add to the video element
@@ -6933,6 +6928,67 @@ export class WebPlayer extends BasePlayer {
6933
6928
  );
6934
6929
  }
6935
6930
 
6931
+ /**
6932
+ * Lock screen orientation to landscape when entering fullscreen
6933
+ */
6934
+ private async lockOrientationLandscape(): Promise<void> {
6935
+ try {
6936
+ // Only attempt orientation lock on mobile devices
6937
+ if (!this.isMobileDevice()) {
6938
+ this.debugLog('Skipping orientation lock - not a mobile device');
6939
+ return;
6940
+ }
6941
+
6942
+ // Check if Screen Orientation API is supported
6943
+ const screenOrientation = screen.orientation as any;
6944
+ if (screenOrientation && typeof screenOrientation.lock === 'function') {
6945
+ try {
6946
+ // Try to lock to landscape orientation
6947
+ await screenOrientation.lock('landscape');
6948
+ this.debugLog('Screen orientation locked to landscape');
6949
+ } catch (error) {
6950
+ this.debugWarn('Failed to lock orientation to landscape:', (error as Error).message);
6951
+ // Some browsers require fullscreen to be active before locking orientation
6952
+ // If it fails, we'll just show a message
6953
+ if (this.isAndroidDevice()) {
6954
+ this.showShortcutIndicator('Please rotate device to landscape');
6955
+ }
6956
+ }
6957
+ } else {
6958
+ // Fallback for older browsers or iOS (which doesn't support orientation lock)
6959
+ this.debugLog('Screen Orientation API not supported');
6960
+ if (this.isMobileDevice()) {
6961
+ // Show a subtle hint for devices that don't support orientation lock
6962
+ this.showShortcutIndicator('Rotate device to landscape for best experience');
6963
+ }
6964
+ }
6965
+ } catch (error) {
6966
+ this.debugWarn('Orientation lock error:', (error as Error).message);
6967
+ }
6968
+ }
6969
+
6970
+ /**
6971
+ * Unlock screen orientation when exiting fullscreen
6972
+ */
6973
+ private async unlockOrientation(): Promise<void> {
6974
+ try {
6975
+ // Check if Screen Orientation API is supported
6976
+ const screenOrientation = screen.orientation as any;
6977
+ if (screenOrientation && typeof screenOrientation.unlock === 'function') {
6978
+ try {
6979
+ screenOrientation.unlock();
6980
+ this.debugLog('Screen orientation unlocked');
6981
+ } catch (error) {
6982
+ this.debugWarn('Failed to unlock orientation:', (error as Error).message);
6983
+ }
6984
+ } else {
6985
+ this.debugLog('Screen Orientation API not supported for unlock');
6986
+ }
6987
+ } catch (error) {
6988
+ this.debugWarn('Orientation unlock error:', (error as Error).message);
6989
+ }
6990
+ }
6991
+
6936
6992
  private handleVolumeChange(e: MouseEvent): void {
6937
6993
  const slider = document.getElementById('uvf-volume-slider');
6938
6994
  if (!slider) return;
@@ -7041,6 +7097,241 @@ export class WebPlayer extends BasePlayer {
7041
7097
  }
7042
7098
  }, timeout);
7043
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
+ }
7044
7335
 
7045
7336
  private isFullscreen(): boolean {
7046
7337
  return !!(document.fullscreenElement ||