unified-video-framework 1.4.212 → 1.4.213

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.
@@ -1328,9 +1328,57 @@ export class WebPlayer extends BasePlayer {
1328
1328
  }
1329
1329
  }
1330
1330
 
1331
+ /**
1332
+ * Safely set video currentTime with validation to prevent non-finite value errors
1333
+ */
1334
+ private safeSetCurrentTime(time: number): boolean {
1335
+ if (!this.video) {
1336
+ this.debugWarn('Cannot set currentTime: video element not available');
1337
+ return false;
1338
+ }
1339
+
1340
+ // Validate the time value is finite
1341
+ if (!isFinite(time) || isNaN(time)) {
1342
+ this.debugWarn('Attempted to set invalid currentTime value:', time);
1343
+ return false;
1344
+ }
1345
+
1346
+ // Ensure time is non-negative
1347
+ const safeTime = Math.max(0, time);
1348
+
1349
+ // Additional validation: check if video duration is available and valid
1350
+ const duration = this.video.duration;
1351
+ if (isFinite(duration) && duration > 0) {
1352
+ // Clamp time to valid range [0, duration]
1353
+ const clampedTime = Math.min(safeTime, duration);
1354
+ try {
1355
+ this.video.currentTime = clampedTime;
1356
+ return true;
1357
+ } catch (error) {
1358
+ this.debugError('Error setting currentTime:', error);
1359
+ return false;
1360
+ }
1361
+ } else {
1362
+ // Duration not available yet, but still set time if it's valid
1363
+ try {
1364
+ this.video.currentTime = safeTime;
1365
+ return true;
1366
+ } catch (error) {
1367
+ this.debugError('Error setting currentTime:', error);
1368
+ return false;
1369
+ }
1370
+ }
1371
+ }
1372
+
1331
1373
  seek(time: number): void {
1332
1374
  if (!this.video) return;
1333
1375
 
1376
+ // Validate input time
1377
+ if (!isFinite(time) || isNaN(time)) {
1378
+ this.debugWarn('Invalid seek time:', time);
1379
+ return;
1380
+ }
1381
+
1334
1382
  // Security check: Prevent seeking beyond free preview limit
1335
1383
  const freeDuration = Number(this.config.freeDuration || 0);
1336
1384
  if (freeDuration > 0 && !this.paymentSuccessful) {
@@ -1339,17 +1387,13 @@ export class WebPlayer extends BasePlayer {
1339
1387
  this.debugWarn('Seek blocked - beyond free preview limit');
1340
1388
  this.enforcePaywallSecurity();
1341
1389
  // Reset to safe position
1342
- this.video.currentTime = Math.max(0, freeDuration - 1);
1390
+ this.safeSetCurrentTime(freeDuration - 1);
1343
1391
  return;
1344
1392
  }
1345
1393
  }
1346
1394
 
1347
- const d = this.video.duration;
1348
- if (typeof d === 'number' && isFinite(d) && d > 0) {
1349
- this.video.currentTime = Math.max(0, Math.min(time, d));
1350
- } else {
1351
- this.video.currentTime = Math.max(0, time);
1352
- }
1395
+ // Use safe setter with validated time
1396
+ this.safeSetCurrentTime(time);
1353
1397
  }
1354
1398
 
1355
1399
  setVolume(level: number): void {
@@ -4376,6 +4420,83 @@ export class WebPlayer extends BasePlayer {
4376
4420
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
4377
4421
  }
4378
4422
 
4423
+ /* Responsive shortcut/volume indicator for mobile and tablet */
4424
+ @media screen and (max-width: 767px) {
4425
+ .uvf-shortcut-indicator {
4426
+ padding: 16px 24px;
4427
+ font-size: 20px;
4428
+ max-width: calc(100vw - 40px);
4429
+ border-radius: 12px;
4430
+ backdrop-filter: blur(10px);
4431
+ }
4432
+
4433
+ /* Volume indicator - more compact on mobile */
4434
+ .uvf-shortcut-indicator .uvf-ki-volume {
4435
+ flex-direction: column;
4436
+ gap: 12px;
4437
+ }
4438
+
4439
+ .uvf-shortcut-indicator .uvf-ki-vol-icon svg {
4440
+ width: 32px;
4441
+ height: 32px;
4442
+ }
4443
+
4444
+ .uvf-shortcut-indicator .uvf-ki-vol-bar {
4445
+ width: clamp(200px, 70vw, 280px);
4446
+ height: 10px;
4447
+ border-radius: 5px;
4448
+ }
4449
+
4450
+ .uvf-shortcut-indicator .uvf-ki-vol-text {
4451
+ font-size: 22px;
4452
+ font-weight: 700;
4453
+ min-width: auto;
4454
+ text-align: center;
4455
+ width: 100%;
4456
+ }
4457
+
4458
+ /* Skip indicators - slightly smaller */
4459
+ .uvf-shortcut-indicator .uvf-ki-skip {
4460
+ width: 90px;
4461
+ height: 90px;
4462
+ }
4463
+
4464
+ .uvf-shortcut-indicator .uvf-ki-skip svg {
4465
+ width: 90px;
4466
+ height: 90px;
4467
+ }
4468
+
4469
+ .uvf-shortcut-indicator .uvf-ki-skip .uvf-ki-skip-num {
4470
+ font-size: 19px;
4471
+ }
4472
+
4473
+ /* Icon indicators */
4474
+ .uvf-shortcut-indicator .uvf-ki svg {
4475
+ width: 56px;
4476
+ height: 56px;
4477
+ }
4478
+
4479
+ .uvf-shortcut-indicator .uvf-ki-text {
4480
+ font-size: 16px;
4481
+ }
4482
+ }
4483
+
4484
+ @media screen and (min-width: 768px) and (max-width: 1023px) {
4485
+ /* Tablet optimization */
4486
+ .uvf-shortcut-indicator .uvf-ki-vol-bar {
4487
+ width: 160px;
4488
+ }
4489
+
4490
+ .uvf-shortcut-indicator .uvf-ki-vol-icon svg {
4491
+ width: 34px;
4492
+ height: 34px;
4493
+ }
4494
+
4495
+ .uvf-shortcut-indicator .uvf-ki-vol-text {
4496
+ font-size: 15px;
4497
+ }
4498
+ }
4499
+
4379
4500
  /* Hide top bar when no cursor */
4380
4501
  .uvf-player-wrapper.no-cursor .uvf-top-bar {
4381
4502
  opacity: 0 !important;
@@ -5990,9 +6111,17 @@ export class WebPlayer extends BasePlayer {
5990
6111
  }
5991
6112
  });
5992
6113
 
5993
- // Skip buttons
5994
- skipBackBtn?.addEventListener('click', () => this.seek(this.video!.currentTime - 10));
5995
- skipForwardBtn?.addEventListener('click', () => this.seek(this.video!.currentTime + 10));
6114
+ // Skip buttons with null safety
6115
+ skipBackBtn?.addEventListener('click', () => {
6116
+ if (this.video && !isNaN(this.video.duration)) {
6117
+ this.seek(Math.max(0, this.video.currentTime - 10));
6118
+ }
6119
+ });
6120
+ skipForwardBtn?.addEventListener('click', () => {
6121
+ if (this.video && !isNaN(this.video.duration)) {
6122
+ this.seek(Math.min(this.video.duration, this.video.currentTime + 10));
6123
+ }
6124
+ });
5996
6125
 
5997
6126
  // Volume control
5998
6127
  volumeBtn?.addEventListener('click', (e) => {
@@ -6045,13 +6174,13 @@ export class WebPlayer extends BasePlayer {
6045
6174
 
6046
6175
  // Progress bar interactions
6047
6176
  progressBar?.addEventListener('click', (e) => {
6048
- this.handleProgressChange(e as MouseEvent);
6177
+ this.seekToPosition(e as MouseEvent);
6049
6178
  });
6050
6179
 
6051
6180
  progressBar?.addEventListener('mousedown', (e) => {
6052
6181
  this.isDragging = true;
6053
6182
  this.showTimeTooltip = true;
6054
- this.handleProgressChange(e as MouseEvent);
6183
+ this.seekToPosition(e as MouseEvent);
6055
6184
  this.updateTimeTooltip(e as MouseEvent);
6056
6185
  });
6057
6186
 
@@ -6078,7 +6207,11 @@ export class WebPlayer extends BasePlayer {
6078
6207
  e.preventDefault(); // Prevent scrolling
6079
6208
  this.isDragging = true;
6080
6209
  const touch = e.touches[0];
6081
- this.handleProgressChange(touch);
6210
+ const mouseEvent = new MouseEvent('mousedown', {
6211
+ clientX: touch.clientX,
6212
+ clientY: touch.clientY
6213
+ });
6214
+ this.seekToPosition(mouseEvent);
6082
6215
  }, { passive: false });
6083
6216
 
6084
6217
  // Global mouse and touch events for enhanced dragging
@@ -6087,7 +6220,7 @@ export class WebPlayer extends BasePlayer {
6087
6220
  this.handleVolumeChange(e);
6088
6221
  }
6089
6222
  if (this.isDragging && progressBar) {
6090
- this.handleProgressChange(e);
6223
+ this.seekToPosition(e);
6091
6224
  // Update tooltip position during dragging
6092
6225
  this.updateTimeTooltip(e);
6093
6226
  }
@@ -6097,7 +6230,11 @@ export class WebPlayer extends BasePlayer {
6097
6230
  if (this.isDragging && progressBar) {
6098
6231
  e.preventDefault(); // Prevent scrolling
6099
6232
  const touch = e.touches[0];
6100
- this.handleProgressChange(touch);
6233
+ const mouseEvent = new MouseEvent('mousemove', {
6234
+ clientX: touch.clientX,
6235
+ clientY: touch.clientY
6236
+ });
6237
+ this.seekToPosition(mouseEvent);
6101
6238
  }
6102
6239
  }, { passive: false });
6103
6240
 
@@ -6435,14 +6572,18 @@ export class WebPlayer extends BasePlayer {
6435
6572
  case 'ArrowLeft':
6436
6573
  e.preventDefault();
6437
6574
  e.stopImmediatePropagation(); // Prevent duplicate handler triggers
6438
- this.seek(Math.max(0, this.video!.currentTime - 10));
6439
- shortcutText = '-10s';
6575
+ if (this.video && !isNaN(this.video.duration)) {
6576
+ this.seek(Math.max(0, this.video.currentTime - 10));
6577
+ shortcutText = '-10s';
6578
+ }
6440
6579
  break;
6441
6580
  case 'ArrowRight':
6442
6581
  e.preventDefault();
6443
6582
  e.stopImmediatePropagation(); // Prevent duplicate handler triggers
6444
- this.seek(Math.min(this.video!.duration, this.video!.currentTime + 10));
6445
- shortcutText = '+10s';
6583
+ if (this.video && !isNaN(this.video.duration)) {
6584
+ this.seek(Math.min(this.video.duration, this.video.currentTime + 10));
6585
+ shortcutText = '+10s';
6586
+ }
6446
6587
  break;
6447
6588
  case 'ArrowUp':
6448
6589
  e.preventDefault();
@@ -6507,8 +6648,9 @@ export class WebPlayer extends BasePlayer {
6507
6648
  case '8':
6508
6649
  case '9':
6509
6650
  e.preventDefault();
6510
- const percent = parseInt(e.key) * 10;
6511
- if (this.video) {
6651
+ // Only jump to position if video is loaded and duration is valid
6652
+ if (this.video && !isNaN(this.video.duration) && this.video.duration > 0) {
6653
+ const percent = parseInt(e.key) * 10;
6512
6654
  this.video.currentTime = (this.video.duration * percent) / 100;
6513
6655
  shortcutText = `${percent}%`;
6514
6656
  }
@@ -6834,7 +6976,7 @@ export class WebPlayer extends BasePlayer {
6834
6976
  // Use deferred pause to avoid race conditions
6835
6977
  this.requestPause();
6836
6978
  if (fromSeek || ((this.video.currentTime || 0) > lim)) {
6837
- this.video.currentTime = Math.max(0, lim - 0.1);
6979
+ this.safeSetCurrentTime(lim - 0.1);
6838
6980
  }
6839
6981
  } catch (_) {}
6840
6982
  }
@@ -7022,16 +7164,29 @@ export class WebPlayer extends BasePlayer {
7022
7164
  }
7023
7165
  }
7024
7166
 
7025
- private handleProgressChange(e: MouseEvent | Touch): void {
7026
- const progressBar = document.getElementById('uvf-progress-bar');
7167
+ private seekToPosition(e: MouseEvent): void {
7168
+ const progressBar = document.querySelector('.uvf-progress-bar') as HTMLElement;
7027
7169
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
7028
7170
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
7029
7171
  if (!progressBar || !this.video) return;
7030
7172
 
7173
+ const duration = this.video.duration;
7174
+ // Validate duration before calculating seek time
7175
+ if (!isFinite(duration) || isNaN(duration) || duration <= 0) {
7176
+ this.debugWarn('Invalid video duration, cannot seek via progress bar');
7177
+ return;
7178
+ }
7179
+
7031
7180
  const rect = progressBar.getBoundingClientRect();
7032
7181
  const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
7033
7182
  const percent = (x / rect.width) * 100;
7034
- const time = (percent / 100) * this.video.duration;
7183
+ const time = (percent / 100) * duration;
7184
+
7185
+ // Validate calculated time
7186
+ if (!isFinite(time) || isNaN(time)) {
7187
+ this.debugWarn('Calculated seek time is invalid:', time);
7188
+ return;
7189
+ }
7035
7190
 
7036
7191
  // Update UI immediately for responsive feedback
7037
7192
  if (progressFilled) {
@@ -7270,15 +7425,24 @@ export class WebPlayer extends BasePlayer {
7270
7425
  const wrapperWidth = wrapperRect.width;
7271
7426
  const isLeftSide = tapPosition < wrapperWidth / 2;
7272
7427
 
7428
+ const currentTime = this.video.currentTime;
7429
+ const duration = this.video.duration;
7430
+
7431
+ // Validate current time and duration before calculating new time
7432
+ if (!isFinite(currentTime) || isNaN(currentTime) || !isFinite(duration) || isNaN(duration)) {
7433
+ this.debugWarn('Invalid video time values, skipping double-tap action');
7434
+ return;
7435
+ }
7436
+
7273
7437
  if (isLeftSide) {
7274
7438
  // Skip backward
7275
- const newTime = Math.max(0, this.video.currentTime - SKIP_SECONDS);
7439
+ const newTime = Math.max(0, currentTime - SKIP_SECONDS);
7276
7440
  this.seek(newTime);
7277
7441
  this.showShortcutIndicator(`-${SKIP_SECONDS}s`);
7278
7442
  this.debugLog('Double tap left - skip backward');
7279
7443
  } else {
7280
7444
  // Skip forward
7281
- const newTime = Math.min(this.video.duration, this.video.currentTime + SKIP_SECONDS);
7445
+ const newTime = Math.min(duration, currentTime + SKIP_SECONDS);
7282
7446
  this.seek(newTime);
7283
7447
  this.showShortcutIndicator(`+${SKIP_SECONDS}s`);
7284
7448
  this.debugLog('Double tap right - skip forward');
@@ -7344,8 +7508,11 @@ export class WebPlayer extends BasePlayer {
7344
7508
 
7345
7509
  this.fastBackwardInterval = setInterval(() => {
7346
7510
  if (this.video) {
7347
- const newTime = Math.max(0, this.video.currentTime - 0.1); // Go back 0.1s every frame
7348
- this.video.currentTime = newTime;
7511
+ const currentTime = this.video.currentTime;
7512
+ if (isFinite(currentTime) && !isNaN(currentTime)) {
7513
+ const newTime = Math.max(0, currentTime - 0.1); // Go back 0.1s every frame
7514
+ this.safeSetCurrentTime(newTime);
7515
+ }
7349
7516
  }
7350
7517
  }, 50); // Update every 50ms for smooth backward motion
7351
7518
  }
@@ -7862,8 +8029,8 @@ export class WebPlayer extends BasePlayer {
7862
8029
  }
7863
8030
 
7864
8031
  const chapter = this.coreChapterManager.seekToChapter(chapterId);
7865
- if (chapter) {
7866
- this.video.currentTime = chapter.startTime;
8032
+ if (chapter && isFinite(chapter.startTime) && !isNaN(chapter.startTime)) {
8033
+ this.safeSetCurrentTime(chapter.startTime);
7867
8034
  this.debugLog('Seeked to chapter:', chapter.title);
7868
8035
  }
7869
8036
  }
@@ -8592,18 +8759,10 @@ export class WebPlayer extends BasePlayer {
8592
8759
  });
8593
8760
  }
8594
8761
 
8595
- // Auto-close settings menu on mobile after a short delay
8596
- if (this.isMobileDevice()) {
8597
- setTimeout(() => {
8598
- this.hideSettingsMenu();
8599
- }, 300);
8600
- } else {
8601
- // Desktop: just refresh the menu to update current values
8602
- setTimeout(() => {
8603
- this.generateAccordionMenu();
8604
- this.setupSettingsEventListeners();
8605
- }, 100);
8606
- }
8762
+ // Close settings menu after selection on all devices
8763
+ setTimeout(() => {
8764
+ this.hideSettingsMenu();
8765
+ }, 200);
8607
8766
  }
8608
8767
 
8609
8768
  /**
@@ -9240,12 +9399,12 @@ export class WebPlayer extends BasePlayer {
9240
9399
 
9241
9400
  // Additional check: ensure video is paused if not authenticated
9242
9401
  if (this.video && !this.video.paused && !this.paymentSuccessful) {
9243
- this.debugWarn('Unauthorized playbook detected, pausing video');
9402
+ this.debugWarn('Unauthorized playback detected, pausing video');
9244
9403
  try {
9245
9404
  this.video.pause();
9246
9405
  const freeDuration = Number(this.config.freeDuration || 0);
9247
- if (freeDuration > 0) {
9248
- this.video.currentTime = Math.max(0, freeDuration - 1);
9406
+ if (freeDuration > 0 && isFinite(freeDuration)) {
9407
+ this.safeSetCurrentTime(freeDuration - 1);
9249
9408
  }
9250
9409
  } catch (_) {}
9251
9410
  }
@@ -9261,7 +9420,7 @@ export class WebPlayer extends BasePlayer {
9261
9420
  // Disable video completely
9262
9421
  if (this.video) {
9263
9422
  this.video.pause();
9264
- this.video.currentTime = 0;
9423
+ this.safeSetCurrentTime(0);
9265
9424
  this.video.src = ''; // Clear video source
9266
9425
  this.video.style.display = 'none';
9267
9426
  }