@livepeer-frameworks/player-react 0.0.4 → 0.1.1

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 (54) hide show
  1. package/README.md +16 -5
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/types/components/PlayerControls.d.ts +2 -0
  7. package/dist/types/components/StatsPanel.d.ts +2 -14
  8. package/dist/types/hooks/useMetaTrack.d.ts +1 -1
  9. package/dist/types/hooks/usePlayerController.d.ts +2 -0
  10. package/dist/types/hooks/useStreamState.d.ts +1 -1
  11. package/dist/types/hooks/useTelemetry.d.ts +1 -1
  12. package/dist/types/hooks/useViewerEndpoints.d.ts +2 -2
  13. package/dist/types/types.d.ts +1 -1
  14. package/dist/types/ui/button.d.ts +1 -1
  15. package/package.json +1 -1
  16. package/src/components/DevModePanel.tsx +249 -170
  17. package/src/components/Icons.tsx +105 -25
  18. package/src/components/IdleScreen.tsx +262 -142
  19. package/src/components/LoadingScreen.tsx +171 -153
  20. package/src/components/LogoOverlay.tsx +3 -6
  21. package/src/components/Player.tsx +86 -74
  22. package/src/components/PlayerControls.tsx +351 -263
  23. package/src/components/PlayerErrorBoundary.tsx +6 -13
  24. package/src/components/SeekBar.tsx +96 -88
  25. package/src/components/SkipIndicator.tsx +2 -12
  26. package/src/components/SpeedIndicator.tsx +2 -11
  27. package/src/components/StatsPanel.tsx +65 -34
  28. package/src/components/StreamStateOverlay.tsx +105 -49
  29. package/src/components/SubtitleRenderer.tsx +29 -29
  30. package/src/components/ThumbnailOverlay.tsx +5 -6
  31. package/src/components/TitleOverlay.tsx +2 -8
  32. package/src/components/players/DashJsPlayer.tsx +13 -11
  33. package/src/components/players/HlsJsPlayer.tsx +13 -11
  34. package/src/components/players/MewsWsPlayer/index.tsx +13 -11
  35. package/src/components/players/MistPlayer.tsx +13 -11
  36. package/src/components/players/MistWebRTCPlayer/index.tsx +19 -10
  37. package/src/components/players/NativePlayer.tsx +10 -12
  38. package/src/components/players/VideoJsPlayer.tsx +13 -11
  39. package/src/context/PlayerContext.tsx +4 -8
  40. package/src/context/index.ts +3 -3
  41. package/src/hooks/useMetaTrack.ts +28 -28
  42. package/src/hooks/usePlaybackQuality.ts +3 -3
  43. package/src/hooks/usePlayerController.ts +186 -140
  44. package/src/hooks/usePlayerSelection.ts +6 -6
  45. package/src/hooks/useStreamState.ts +53 -58
  46. package/src/hooks/useTelemetry.ts +19 -4
  47. package/src/hooks/useViewerEndpoints.ts +40 -30
  48. package/src/index.tsx +36 -28
  49. package/src/types.ts +9 -9
  50. package/src/ui/badge.tsx +6 -5
  51. package/src/ui/button.tsx +9 -8
  52. package/src/ui/context-menu.tsx +42 -61
  53. package/src/ui/select.tsx +13 -7
  54. package/src/ui/slider.tsx +18 -29
@@ -14,7 +14,7 @@ const SOURCE_TYPE_LABELS: Record<string, string> = {
14
14
  "dash/video/mp4": "DASH",
15
15
  "html5/video/mp4": "MP4",
16
16
  "html5/video/webm": "WebM",
17
- "whep": "WHEP",
17
+ whep: "WHEP",
18
18
  "mist/html": "Mist",
19
19
  "mist/legacy": "Auto",
20
20
  "ws/video/mp4": "MEWS",
@@ -66,7 +66,7 @@ export interface DevModePanelProps {
66
66
  */
67
67
  const DevModePanel: React.FC<DevModePanelProps> = ({
68
68
  onSettingsChange,
69
- playbackMode = 'auto',
69
+ playbackMode = "auto",
70
70
  onModeChange,
71
71
  onReload,
72
72
  streamInfo,
@@ -76,22 +76,25 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
76
76
  videoElement,
77
77
  protocol,
78
78
  nodeId,
79
- isVisible = true,
79
+ isVisible: _isVisible = true,
80
80
  isOpen: controlledIsOpen,
81
81
  onOpenChange,
82
82
  }) => {
83
83
  // Support both controlled and uncontrolled modes
84
84
  const [internalIsOpen, setInternalIsOpen] = useState(false);
85
85
  const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
86
- const setIsOpen = useCallback((value: boolean) => {
87
- if (onOpenChange) {
88
- onOpenChange(value);
89
- } else {
90
- setInternalIsOpen(value);
91
- }
92
- }, [onOpenChange]);
86
+ const setIsOpen = useCallback(
87
+ (value: boolean) => {
88
+ if (onOpenChange) {
89
+ onOpenChange(value);
90
+ } else {
91
+ setInternalIsOpen(value);
92
+ }
93
+ },
94
+ [onOpenChange]
95
+ );
93
96
  const [activeTab, setActiveTab] = useState<"config" | "stats">("config");
94
- const [currentComboIndex, setCurrentComboIndex] = useState(0);
97
+ const [, setCurrentComboIndex] = useState(0);
95
98
  const [hoveredComboIndex, setHoveredComboIndex] = useState<number | null>(null);
96
99
  const [tooltipAbove, setTooltipAbove] = useState(false);
97
100
  const [showDisabledPlayers, setShowDisabledPlayers] = useState(false);
@@ -147,7 +150,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
147
150
  setPlayerStats(stats);
148
151
  }
149
152
  }
150
- } catch (e) {
153
+ } catch {
151
154
  // Ignore errors
152
155
  }
153
156
  };
@@ -180,7 +183,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
180
183
 
181
184
  // For backward compatibility (Next Option only cycles compatible)
182
185
  const combinations = useMemo(() => {
183
- return allCombinations.filter(c => c.compatible);
186
+ return allCombinations.filter((c) => c.compatible);
184
187
  }, [allCombinations]);
185
188
 
186
189
  // Find current active combo index based on current player/source (in allCombinations)
@@ -220,18 +223,21 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
220
223
  });
221
224
  }, [combinations, activeCompatibleIndex, onSettingsChange]);
222
225
 
223
- const handleSelectCombo = useCallback((index: number) => {
224
- const combo = allCombinations[index];
225
- if (!combo) return;
226
-
227
- // Allow selecting even incompatible combos in dev mode (for testing)
228
- setCurrentComboIndex(index);
229
- onSettingsChange({
230
- forcePlayer: combo.player,
231
- forceType: combo.sourceType,
232
- forceSource: combo.sourceIndex,
233
- });
234
- }, [allCombinations, onSettingsChange]);
226
+ const handleSelectCombo = useCallback(
227
+ (index: number) => {
228
+ const combo = allCombinations[index];
229
+ if (!combo) return;
230
+
231
+ // Allow selecting even incompatible combos in dev mode (for testing)
232
+ setCurrentComboIndex(index);
233
+ onSettingsChange({
234
+ forcePlayer: combo.player,
235
+ forceType: combo.sourceType,
236
+ forceSource: combo.sourceIndex,
237
+ });
238
+ },
239
+ [allCombinations, onSettingsChange]
240
+ );
235
241
 
236
242
  // Video stats - poll periodically when stats tab is open
237
243
  const [stats, setStats] = useState<{
@@ -274,31 +280,9 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
274
280
  return () => clearInterval(interval);
275
281
  }, [isOpen, activeTab, videoElement]);
276
282
 
277
- // Toggle button (when closed)
283
+ // Panel is only rendered when open (no floating toggle button)
278
284
  if (!isOpen) {
279
- return (
280
- <button
281
- type="button"
282
- onClick={() => setIsOpen(true)}
283
- className={cn(
284
- "fw-dev-toggle",
285
- !isVisible && "fw-dev-toggle--hidden"
286
- )}
287
- title="Advanced Settings"
288
- aria-label="Open advanced settings panel"
289
- >
290
- <svg
291
- width="16"
292
- height="16"
293
- viewBox="0 0 24 24"
294
- fill="none"
295
- stroke="currentColor"
296
- strokeWidth="2"
297
- >
298
- <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
299
- </svg>
300
- </button>
301
- );
285
+ return null;
302
286
  }
303
287
 
304
288
  return (
@@ -346,20 +330,17 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
346
330
  <div className="fw-dev-section">
347
331
  <div className="fw-dev-label">Active</div>
348
332
  <div className="fw-dev-value">
349
- {currentPlayer?.name || "None"}{" "}
350
- <span className="fw-dev-value-arrow">→</span>{" "}
333
+ {currentPlayer?.name || "None"} <span className="fw-dev-value-arrow">→</span>{" "}
351
334
  {SOURCE_TYPE_LABELS[currentSource?.type || ""] || currentSource?.type || "—"}
352
335
  </div>
353
- {nodeId && (
354
- <div className="fw-dev-value-muted">Node: {nodeId}</div>
355
- )}
336
+ {nodeId && <div className="fw-dev-value-muted">Node: {nodeId}</div>}
356
337
  </div>
357
338
 
358
339
  {/* Playback Mode Selector */}
359
340
  <div className="fw-dev-section">
360
341
  <div className="fw-dev-label">Playback Mode</div>
361
342
  <div className="fw-dev-mode-group">
362
- {(['auto', 'low-latency', 'quality'] as const).map((mode) => (
343
+ {(["auto", "low-latency", "quality"] as const).map((mode) => (
363
344
  <button
364
345
  key={mode}
365
346
  type="button"
@@ -369,31 +350,25 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
369
350
  playbackMode === mode && "fw-dev-mode-btn--active"
370
351
  )}
371
352
  >
372
- {mode === 'low-latency' ? 'Low Lat' : mode.charAt(0).toUpperCase() + mode.slice(1)}
353
+ {mode === "low-latency"
354
+ ? "Low Lat"
355
+ : mode.charAt(0).toUpperCase() + mode.slice(1)}
373
356
  </button>
374
357
  ))}
375
358
  </div>
376
359
  <div className="fw-dev-mode-desc">
377
- {playbackMode === 'auto' && 'Balanced: MP4/WS → WHEP → HLS'}
378
- {playbackMode === 'low-latency' && 'WHEP/WebRTC first (<1s delay)'}
379
- {playbackMode === 'quality' && 'MP4/WS first, HLS fallback'}
360
+ {playbackMode === "auto" && "Balanced: MP4/WS → WHEP → HLS"}
361
+ {playbackMode === "low-latency" && "WHEP/WebRTC first (<1s delay)"}
362
+ {playbackMode === "quality" && "MP4/WS first, HLS fallback"}
380
363
  </div>
381
364
  </div>
382
365
 
383
366
  {/* Action buttons */}
384
367
  <div className="fw-dev-actions">
385
- <button
386
- type="button"
387
- onClick={handleReload}
388
- className="fw-dev-action-btn"
389
- >
368
+ <button type="button" onClick={handleReload} className="fw-dev-action-btn">
390
369
  Reload
391
370
  </button>
392
- <button
393
- type="button"
394
- onClick={handleNextCombo}
395
- className="fw-dev-action-btn"
396
- >
371
+ <button type="button" onClick={handleNextCombo} className="fw-dev-action-btn">
397
372
  Next Option
398
373
  </button>
399
374
  </div>
@@ -401,9 +376,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
401
376
  {/* Combo list */}
402
377
  <div className="fw-dev-section" style={{ padding: 0, borderBottom: 0 }}>
403
378
  <div className="fw-dev-list-header">
404
- <span className="fw-dev-list-title">
405
- Player Options ({combinations.length})
406
- </span>
379
+ <span className="fw-dev-list-title">Player Options ({combinations.length})</span>
407
380
  {allCombinations.length > combinations.length && (
408
381
  <button
409
382
  type="button"
@@ -421,14 +394,13 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
421
394
  >
422
395
  <path d="M6 9l6 6 6-6" />
423
396
  </svg>
424
- {showDisabledPlayers ? "Hide" : "Show"} disabled ({allCombinations.length - combinations.length})
397
+ {showDisabledPlayers ? "Hide" : "Show"} disabled (
398
+ {allCombinations.length - combinations.length})
425
399
  </button>
426
400
  )}
427
401
  </div>
428
402
  {allCombinations.length === 0 ? (
429
- <div className="fw-dev-list-empty">
430
- No stream info available
431
- </div>
403
+ <div className="fw-dev-list-empty">No stream info available</div>
432
404
  ) : (
433
405
  <div>
434
406
  {allCombinations.map((combo, index) => {
@@ -437,11 +409,13 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
437
409
  if (!combo.compatible && !isCodecIncompat && !showDisabledPlayers) return null;
438
410
 
439
411
  const isActive = activeComboIndex === index;
440
- const typeLabel = SOURCE_TYPE_LABELS[combo.sourceType] || combo.sourceType.split("/").pop();
412
+ const typeLabel =
413
+ SOURCE_TYPE_LABELS[combo.sourceType] || combo.sourceType.split("/").pop();
441
414
 
442
415
  // Determine score class
443
416
  const getScoreClass = () => {
444
- if (!combo.compatible && !isCodecIncompat) return "fw-dev-combo-score--disabled";
417
+ if (!combo.compatible && !isCodecIncompat)
418
+ return "fw-dev-combo-score--disabled";
445
419
  if (isCodecIncompat) return "fw-dev-combo-score--low";
446
420
  if (combo.score >= 2) return "fw-dev-combo-score--high";
447
421
  if (combo.score >= 1.5) return "fw-dev-combo-score--mid";
@@ -473,7 +447,8 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
473
447
  const row = e.currentTarget;
474
448
  const containerRect = container.getBoundingClientRect();
475
449
  const rowRect = row.getBoundingClientRect();
476
- const relativePosition = (rowRect.top - containerRect.top) / containerRect.height;
450
+ const relativePosition =
451
+ (rowRect.top - containerRect.top) / containerRect.height;
477
452
  setTooltipAbove(relativePosition > 0.6);
478
453
  }
479
454
  }}
@@ -496,9 +471,10 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
496
471
  </span>
497
472
  {/* Player + Protocol */}
498
473
  <span className="fw-dev-combo-name">
499
- {combo.playerName}{" "}
500
- <span className="fw-dev-combo-arrow">→</span>{" "}
501
- <span className={cn("fw-dev-combo-type", getTypeClass())}>{typeLabel}</span>
474
+ {combo.playerName} <span className="fw-dev-combo-arrow">→</span>{" "}
475
+ <span className={cn("fw-dev-combo-type", getTypeClass())}>
476
+ {typeLabel}
477
+ </span>
502
478
  </span>
503
479
  {/* Score */}
504
480
  <span className={cn("fw-dev-combo-score", getScoreClass())}>
@@ -508,50 +484,105 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
508
484
 
509
485
  {/* Score breakdown tooltip */}
510
486
  {hoveredComboIndex === index && (
511
- <div className={cn(
512
- "fw-dev-tooltip",
513
- tooltipAbove ? "fw-dev-tooltip--above" : "fw-dev-tooltip--below"
514
- )}>
487
+ <div
488
+ className={cn(
489
+ "fw-dev-tooltip",
490
+ tooltipAbove ? "fw-dev-tooltip--above" : "fw-dev-tooltip--below"
491
+ )}
492
+ >
515
493
  {/* Full player/source info */}
516
494
  <div className="fw-dev-tooltip-header">
517
495
  <div className="fw-dev-tooltip-title">{combo.playerName}</div>
518
496
  <div className="fw-dev-tooltip-subtitle">{combo.sourceType}</div>
519
- {combo.scoreBreakdown?.trackTypes && combo.scoreBreakdown.trackTypes.length > 0 && (
520
- <div className="fw-dev-tooltip-tracks">
521
- Tracks: <span className="fw-dev-tooltip-value">{combo.scoreBreakdown.trackTypes.join(', ')}</span>
522
- </div>
523
- )}
497
+ {combo.scoreBreakdown?.trackTypes &&
498
+ combo.scoreBreakdown.trackTypes.length > 0 && (
499
+ <div className="fw-dev-tooltip-tracks">
500
+ Tracks:{" "}
501
+ <span className="fw-dev-tooltip-value">
502
+ {combo.scoreBreakdown.trackTypes.join(", ")}
503
+ </span>
504
+ </div>
505
+ )}
524
506
  </div>
525
507
  {combo.compatible && combo.scoreBreakdown ? (
526
508
  <>
527
- <div className="fw-dev-tooltip-score">Score: {combo.score.toFixed(2)}</div>
509
+ <div className="fw-dev-tooltip-score">
510
+ Score: {combo.score.toFixed(2)}
511
+ </div>
528
512
  <div className="fw-dev-tooltip-row">
529
- Tracks [{combo.scoreBreakdown.trackTypes.join(', ')}]: <span className="fw-dev-tooltip-value">{combo.scoreBreakdown.trackScore.toFixed(2)}</span> <span className="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.tracks}</span>
513
+ Tracks [{combo.scoreBreakdown.trackTypes.join(", ")}]:{" "}
514
+ <span className="fw-dev-tooltip-value">
515
+ {combo.scoreBreakdown.trackScore.toFixed(2)}
516
+ </span>{" "}
517
+ <span className="fw-dev-tooltip-weight">
518
+ x{combo.scoreBreakdown.weights.tracks}
519
+ </span>
530
520
  </div>
531
521
  <div className="fw-dev-tooltip-row">
532
- Priority: <span className="fw-dev-tooltip-value">{combo.scoreBreakdown.priorityScore.toFixed(2)}</span> <span className="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.priority}</span>
522
+ Priority:{" "}
523
+ <span className="fw-dev-tooltip-value">
524
+ {combo.scoreBreakdown.priorityScore.toFixed(2)}
525
+ </span>{" "}
526
+ <span className="fw-dev-tooltip-weight">
527
+ x{combo.scoreBreakdown.weights.priority}
528
+ </span>
533
529
  </div>
534
530
  <div className="fw-dev-tooltip-row">
535
- Source: <span className="fw-dev-tooltip-value">{combo.scoreBreakdown.sourceScore.toFixed(2)}</span> <span className="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.source}</span>
531
+ Source:{" "}
532
+ <span className="fw-dev-tooltip-value">
533
+ {combo.scoreBreakdown.sourceScore.toFixed(2)}
534
+ </span>{" "}
535
+ <span className="fw-dev-tooltip-weight">
536
+ x{combo.scoreBreakdown.weights.source}
537
+ </span>
536
538
  </div>
537
539
  {combo.scoreBreakdown.reliabilityScore !== undefined && (
538
540
  <div className="fw-dev-tooltip-row">
539
- Reliability: <span className="fw-dev-tooltip-value">{combo.scoreBreakdown.reliabilityScore.toFixed(2)}</span> <span className="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.reliability ?? 0}</span>
540
- </div>
541
- )}
542
- {combo.scoreBreakdown.modeBonus !== undefined && combo.scoreBreakdown.modeBonus !== 0 && (
543
- <div className="fw-dev-tooltip-row">
544
- Mode ({playbackMode}): <span className="fw-dev-tooltip-bonus">+{combo.scoreBreakdown.modeBonus.toFixed(2)}</span> <span className="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.mode ?? 0}</span>
545
- </div>
546
- )}
547
- {combo.scoreBreakdown.routingBonus !== undefined && combo.scoreBreakdown.routingBonus !== 0 && (
548
- <div className="fw-dev-tooltip-row">
549
- Routing: <span className={combo.scoreBreakdown.routingBonus > 0 ? "fw-dev-tooltip-bonus" : "fw-dev-tooltip-penalty"}>{combo.scoreBreakdown.routingBonus > 0 ? '+' : ''}{combo.scoreBreakdown.routingBonus.toFixed(2)}</span> <span className="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.routing ?? 0}</span>
541
+ Reliability:{" "}
542
+ <span className="fw-dev-tooltip-value">
543
+ {combo.scoreBreakdown.reliabilityScore.toFixed(2)}
544
+ </span>{" "}
545
+ <span className="fw-dev-tooltip-weight">
546
+ x{combo.scoreBreakdown.weights.reliability ?? 0}
547
+ </span>
550
548
  </div>
551
549
  )}
550
+ {combo.scoreBreakdown.modeBonus !== undefined &&
551
+ combo.scoreBreakdown.modeBonus !== 0 && (
552
+ <div className="fw-dev-tooltip-row">
553
+ Mode ({playbackMode}):{" "}
554
+ <span className="fw-dev-tooltip-bonus">
555
+ +{combo.scoreBreakdown.modeBonus.toFixed(2)}
556
+ </span>{" "}
557
+ <span className="fw-dev-tooltip-weight">
558
+ x{combo.scoreBreakdown.weights.mode ?? 0}
559
+ </span>
560
+ </div>
561
+ )}
562
+ {combo.scoreBreakdown.routingBonus !== undefined &&
563
+ combo.scoreBreakdown.routingBonus !== 0 && (
564
+ <div className="fw-dev-tooltip-row">
565
+ Routing:{" "}
566
+ <span
567
+ className={
568
+ combo.scoreBreakdown.routingBonus > 0
569
+ ? "fw-dev-tooltip-bonus"
570
+ : "fw-dev-tooltip-penalty"
571
+ }
572
+ >
573
+ {combo.scoreBreakdown.routingBonus > 0 ? "+" : ""}
574
+ {combo.scoreBreakdown.routingBonus.toFixed(2)}
575
+ </span>{" "}
576
+ <span className="fw-dev-tooltip-weight">
577
+ x{combo.scoreBreakdown.weights.routing ?? 0}
578
+ </span>
579
+ </div>
580
+ )}
552
581
  </>
553
582
  ) : (
554
- <div className="fw-dev-tooltip-error">{combo.incompatibleReason || 'Incompatible'}</div>
583
+ <div className="fw-dev-tooltip-error">
584
+ {combo.incompatibleReason || "Incompatible"}
585
+ </div>
555
586
  )}
556
587
  </div>
557
588
  )}
@@ -571,30 +602,46 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
571
602
  <div className="fw-dev-section">
572
603
  <div className="fw-dev-label">Playback Rate</div>
573
604
  <div className="fw-dev-rate">
574
- <div className={cn(
575
- "fw-dev-rate-value",
576
- playbackScore >= 0.95 && playbackScore <= 1.05 ? "fw-dev-stat-value--good" :
577
- playbackScore > 1.05 ? "fw-dev-stat-value--accent" :
578
- playbackScore >= 0.75 ? "fw-dev-stat-value--warn" :
579
- "fw-dev-stat-value--bad"
580
- )}>
605
+ <div
606
+ className={cn(
607
+ "fw-dev-rate-value",
608
+ playbackScore >= 0.95 && playbackScore <= 1.05
609
+ ? "fw-dev-stat-value--good"
610
+ : playbackScore > 1.05
611
+ ? "fw-dev-stat-value--accent"
612
+ : playbackScore >= 0.75
613
+ ? "fw-dev-stat-value--warn"
614
+ : "fw-dev-stat-value--bad"
615
+ )}
616
+ >
581
617
  {playbackScore.toFixed(2)}×
582
618
  </div>
583
619
  <div className="fw-dev-rate-status">
584
- {playbackScore >= 0.95 && playbackScore <= 1.05 ? "realtime" :
585
- playbackScore > 1.05 ? "catching up" :
586
- playbackScore >= 0.75 ? "slightly slow" :
587
- "stalling"}
620
+ {playbackScore >= 0.95 && playbackScore <= 1.05
621
+ ? "realtime"
622
+ : playbackScore > 1.05
623
+ ? "catching up"
624
+ : playbackScore >= 0.75
625
+ ? "slightly slow"
626
+ : "stalling"}
588
627
  </div>
589
628
  </div>
590
629
  <div className="fw-dev-rate-stats">
591
- <span className={qualityScore >= 75 ? "fw-dev-stat-value--good" : "fw-dev-stat-value--bad"}>
630
+ <span
631
+ className={
632
+ qualityScore >= 75 ? "fw-dev-stat-value--good" : "fw-dev-stat-value--bad"
633
+ }
634
+ >
592
635
  Quality: {qualityScore}/100
593
636
  </span>
594
- <span className={stallCount === 0 ? "fw-dev-stat-value--good" : "fw-dev-stat-value--warn"}>
637
+ <span
638
+ className={stallCount === 0 ? "fw-dev-stat-value--good" : "fw-dev-stat-value--warn"}
639
+ >
595
640
  Stalls: {stallCount}
596
641
  </span>
597
- <span className={frameDropRate < 1 ? "fw-dev-stat-value--good" : "fw-dev-stat-value--bad"}>
642
+ <span
643
+ className={frameDropRate < 1 ? "fw-dev-stat-value--good" : "fw-dev-stat-value--bad"}
644
+ >
598
645
  Drops: {frameDropRate.toFixed(1)}%
599
646
  </span>
600
647
  </div>
@@ -638,16 +685,14 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
638
685
  {nodeId && (
639
686
  <div className="fw-dev-stat">
640
687
  <span className="fw-dev-stat-label">Node ID</span>
641
- <span className="fw-dev-stat-value truncate" style={{ maxWidth: '150px' }}>
688
+ <span className="fw-dev-stat-value truncate" style={{ maxWidth: "150px" }}>
642
689
  {nodeId}
643
690
  </span>
644
691
  </div>
645
692
  )}
646
693
  </div>
647
694
  ) : (
648
- <div className="fw-dev-list-empty">
649
- No video element available
650
- </div>
695
+ <div className="fw-dev-list-empty">No video element available</div>
651
696
  )}
652
697
 
653
698
  {/* Player-specific Stats (HLS.js / WebRTC) */}
@@ -655,20 +700,23 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
655
700
  <div>
656
701
  <div className="fw-dev-list-header fw-dev-section-header">
657
702
  <span className="fw-dev-list-title">
658
- {playerStats.type === 'hls' ? 'HLS.js Stats' :
659
- playerStats.type === 'webrtc' ? 'WebRTC Stats' : 'Player Stats'}
703
+ {playerStats.type === "hls"
704
+ ? "HLS.js Stats"
705
+ : playerStats.type === "webrtc"
706
+ ? "WebRTC Stats"
707
+ : "Player Stats"}
660
708
  </span>
661
709
  </div>
662
710
 
663
711
  {/* HLS-specific stats */}
664
- {playerStats.type === 'hls' && (
712
+ {playerStats.type === "hls" && (
665
713
  <>
666
714
  <div className="fw-dev-stat">
667
715
  <span className="fw-dev-stat-label">Bitrate</span>
668
716
  <span className="fw-dev-stat-value--accent">
669
717
  {playerStats.currentBitrate > 0
670
718
  ? `${Math.round(playerStats.currentBitrate / 1000)} kbps`
671
- : 'N/A'}
719
+ : "N/A"}
672
720
  </span>
673
721
  </div>
674
722
  <div className="fw-dev-stat">
@@ -676,19 +724,26 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
676
724
  <span className="fw-dev-stat-value">
677
725
  {playerStats.bandwidthEstimate > 0
678
726
  ? `${Math.round(playerStats.bandwidthEstimate / 1000)} kbps`
679
- : 'N/A'}
727
+ : "N/A"}
680
728
  </span>
681
729
  </div>
682
730
  <div className="fw-dev-stat">
683
731
  <span className="fw-dev-stat-label">Level</span>
684
732
  <span className="fw-dev-stat-value">
685
- {playerStats.currentLevel >= 0 ? playerStats.currentLevel : 'Auto'} / {playerStats.levels?.length || 0}
733
+ {playerStats.currentLevel >= 0 ? playerStats.currentLevel : "Auto"} /{" "}
734
+ {playerStats.levels?.length || 0}
686
735
  </span>
687
736
  </div>
688
737
  {playerStats.latency !== undefined && (
689
738
  <div className="fw-dev-stat">
690
739
  <span className="fw-dev-stat-label">Latency</span>
691
- <span className={playerStats.latency > 5000 ? "fw-dev-stat-value--warn" : "fw-dev-stat-value"}>
740
+ <span
741
+ className={
742
+ playerStats.latency > 5000
743
+ ? "fw-dev-stat-value--warn"
744
+ : "fw-dev-stat-value"
745
+ }
746
+ >
692
747
  {Math.round(playerStats.latency)} ms
693
748
  </span>
694
749
  </div>
@@ -697,7 +752,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
697
752
  )}
698
753
 
699
754
  {/* WebRTC-specific stats */}
700
- {playerStats.type === 'webrtc' && (
755
+ {playerStats.type === "webrtc" && (
701
756
  <>
702
757
  {playerStats.video && (
703
758
  <>
@@ -706,7 +761,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
706
761
  <span className="fw-dev-stat-value--accent">
707
762
  {playerStats.video.bitrate > 0
708
763
  ? `${Math.round(playerStats.video.bitrate / 1000)} kbps`
709
- : 'N/A'}
764
+ : "N/A"}
710
765
  </span>
711
766
  </div>
712
767
  <div className="fw-dev-stat">
@@ -718,21 +773,39 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
718
773
  <div className="fw-dev-stat">
719
774
  <span className="fw-dev-stat-label">Frames</span>
720
775
  <span className="fw-dev-stat-value">
721
- {playerStats.video.framesDecoded} decoded,{' '}
722
- <span className={playerStats.video.frameDropRate > 1 ? "fw-dev-stat-value--bad" : "fw-dev-stat-value--good"}>
776
+ {playerStats.video.framesDecoded} decoded,{" "}
777
+ <span
778
+ className={
779
+ playerStats.video.frameDropRate > 1
780
+ ? "fw-dev-stat-value--bad"
781
+ : "fw-dev-stat-value--good"
782
+ }
783
+ >
723
784
  {playerStats.video.framesDropped} dropped
724
785
  </span>
725
786
  </span>
726
787
  </div>
727
788
  <div className="fw-dev-stat">
728
789
  <span className="fw-dev-stat-label">Packet Loss</span>
729
- <span className={playerStats.video.packetLossRate > 1 ? "fw-dev-stat-value--bad" : "fw-dev-stat-value--good"}>
790
+ <span
791
+ className={
792
+ playerStats.video.packetLossRate > 1
793
+ ? "fw-dev-stat-value--bad"
794
+ : "fw-dev-stat-value--good"
795
+ }
796
+ >
730
797
  {playerStats.video.packetLossRate.toFixed(2)}%
731
798
  </span>
732
799
  </div>
733
800
  <div className="fw-dev-stat">
734
801
  <span className="fw-dev-stat-label">Jitter</span>
735
- <span className={playerStats.video.jitter > 30 ? "fw-dev-stat-value--warn" : "fw-dev-stat-value"}>
802
+ <span
803
+ className={
804
+ playerStats.video.jitter > 30
805
+ ? "fw-dev-stat-value--warn"
806
+ : "fw-dev-stat-value"
807
+ }
808
+ >
736
809
  {playerStats.video.jitter.toFixed(1)} ms
737
810
  </span>
738
811
  </div>
@@ -747,7 +820,13 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
747
820
  {playerStats.network && (
748
821
  <div className="fw-dev-stat">
749
822
  <span className="fw-dev-stat-label">RTT</span>
750
- <span className={playerStats.network.rtt > 200 ? "fw-dev-stat-value--warn" : "fw-dev-stat-value"}>
823
+ <span
824
+ className={
825
+ playerStats.network.rtt > 200
826
+ ? "fw-dev-stat-value--warn"
827
+ : "fw-dev-stat-value"
828
+ }
829
+ >
751
830
  {Math.round(playerStats.network.rtt)} ms
752
831
  </span>
753
832
  </div>
@@ -768,36 +847,32 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
768
847
  {Object.entries(mistStreamInfo.meta.tracks).map(([id, track]) => (
769
848
  <div key={id} className="fw-dev-track">
770
849
  <div className="fw-dev-track-header">
771
- <span className={cn(
772
- "fw-dev-track-badge",
773
- track.type === 'video' ? "fw-dev-track-badge--video" :
774
- track.type === 'audio' ? "fw-dev-track-badge--audio" :
775
- "fw-dev-track-badge--other"
776
- )}>
850
+ <span
851
+ className={cn(
852
+ "fw-dev-track-badge",
853
+ track.type === "video"
854
+ ? "fw-dev-track-badge--video"
855
+ : track.type === "audio"
856
+ ? "fw-dev-track-badge--audio"
857
+ : "fw-dev-track-badge--other"
858
+ )}
859
+ >
777
860
  {track.type}
778
861
  </span>
779
862
  <span className="fw-dev-track-codec">{track.codec}</span>
780
863
  <span className="fw-dev-track-id">#{id}</span>
781
864
  </div>
782
865
  <div className="fw-dev-track-meta">
783
- {track.type === 'video' && track.width && track.height && (
784
- <span>{track.width}×{track.height}</span>
785
- )}
786
- {track.bps && (
787
- <span>{Math.round(track.bps / 1000)} kbps</span>
788
- )}
789
- {track.fpks && (
790
- <span>{Math.round(track.fpks / 1000)} fps</span>
791
- )}
792
- {track.type === 'audio' && track.channels && (
793
- <span>{track.channels}ch</span>
794
- )}
795
- {track.type === 'audio' && track.rate && (
796
- <span>{track.rate} Hz</span>
797
- )}
798
- {track.lang && (
799
- <span>{track.lang}</span>
866
+ {track.type === "video" && track.width && track.height && (
867
+ <span>
868
+ {track.width}×{track.height}
869
+ </span>
800
870
  )}
871
+ {track.bps && <span>{Math.round(track.bps / 1000)} kbps</span>}
872
+ {track.fpks && <span>{Math.round(track.fpks / 1000)} fps</span>}
873
+ {track.type === "audio" && track.channels && <span>{track.channels}ch</span>}
874
+ {track.type === "audio" && track.rate && <span>{track.rate} Hz</span>}
875
+ {track.lang && <span>{track.lang}</span>}
801
876
  </div>
802
877
  </div>
803
878
  ))}
@@ -805,14 +880,18 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
805
880
  )}
806
881
 
807
882
  {/* Debug: Show if mistStreamInfo is missing tracks */}
808
- {mistStreamInfo && (!mistStreamInfo.meta?.tracks || Object.keys(mistStreamInfo.meta.tracks).length === 0) && (
809
- <div className="fw-dev-no-tracks">
810
- <span className="fw-dev-no-tracks-text">
811
- No track data available
812
- {mistStreamInfo.type && <span className="fw-dev-no-tracks-type">({mistStreamInfo.type})</span>}
813
- </span>
814
- </div>
815
- )}
883
+ {mistStreamInfo &&
884
+ (!mistStreamInfo.meta?.tracks ||
885
+ Object.keys(mistStreamInfo.meta.tracks).length === 0) && (
886
+ <div className="fw-dev-no-tracks">
887
+ <span className="fw-dev-no-tracks-text">
888
+ No track data available
889
+ {mistStreamInfo.type && (
890
+ <span className="fw-dev-no-tracks-type">({mistStreamInfo.type})</span>
891
+ )}
892
+ </span>
893
+ </div>
894
+ )}
816
895
  </div>
817
896
  )}
818
897
  </div>