@livepeer-frameworks/player-svelte 0.1.2 → 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Svelte wrapper for the FrameWorks player. Resolves endpoints via Gateway or Mist and renders the best available player (WebCodecs, HLS, etc).
4
4
 
5
- **Docs:** `docs.frameworks.network`
5
+ **Docs:** `logbook.frameworks.network`
6
6
 
7
7
  ## Install
8
8
 
@@ -35,7 +35,11 @@ Notes:
35
35
  ### Direct MistServer Node (mistUrl)
36
36
 
37
37
  ```svelte
38
- <Player contentType="live" contentId="pk_..." options={{ mistUrl: "https://edge.example.com" }} />
38
+ <Player
39
+ contentType="live"
40
+ contentId="pk_..."
41
+ options={{ mistUrl: "https://edge-egress.example.com" }}
42
+ />
39
43
  ```
40
44
 
41
45
  ### Styles
@@ -467,7 +467,8 @@
467
467
  {#if combo.compatible && combo.scoreBreakdown}
468
468
  <div class="fw-dev-tooltip-score">Score: {combo.score.toFixed(2)}</div>
469
469
  <div class="fw-dev-tooltip-row">
470
- Tracks: <span class="fw-dev-tooltip-value"
470
+ Tracks [{combo.scoreBreakdown.trackTypes.join(", ")}]:
471
+ <span class="fw-dev-tooltip-value"
471
472
  >{combo.scoreBreakdown.trackScore.toFixed(2)}</span
472
473
  >
473
474
  <span class="fw-dev-tooltip-weight"
@@ -490,6 +491,43 @@
490
491
  >x{combo.scoreBreakdown.weights.source}</span
491
492
  >
492
493
  </div>
494
+ {#if combo.scoreBreakdown.reliabilityScore !== undefined}
495
+ <div class="fw-dev-tooltip-row">
496
+ Reliability: <span class="fw-dev-tooltip-value"
497
+ >{combo.scoreBreakdown.reliabilityScore.toFixed(2)}</span
498
+ >
499
+ <span class="fw-dev-tooltip-weight"
500
+ >x{combo.scoreBreakdown.weights.reliability ?? 0}</span
501
+ >
502
+ </div>
503
+ {/if}
504
+ {#if combo.scoreBreakdown.modeBonus !== undefined && combo.scoreBreakdown.modeBonus !== 0}
505
+ <div class="fw-dev-tooltip-row">
506
+ Mode ({playbackMode}):
507
+ <span class="fw-dev-tooltip-bonus"
508
+ >+{combo.scoreBreakdown.modeBonus.toFixed(2)}</span
509
+ >
510
+ <span class="fw-dev-tooltip-weight"
511
+ >x{combo.scoreBreakdown.weights.mode ?? 0}</span
512
+ >
513
+ </div>
514
+ {/if}
515
+ {#if combo.scoreBreakdown.routingBonus !== undefined && combo.scoreBreakdown.routingBonus !== 0}
516
+ <div class="fw-dev-tooltip-row">
517
+ Routing: <span
518
+ class={combo.scoreBreakdown.routingBonus > 0
519
+ ? "fw-dev-tooltip-bonus"
520
+ : "fw-dev-tooltip-penalty"}
521
+ >
522
+ {combo.scoreBreakdown.routingBonus > 0
523
+ ? "+"
524
+ : ""}{combo.scoreBreakdown.routingBonus.toFixed(2)}
525
+ </span>
526
+ <span class="fw-dev-tooltip-weight"
527
+ >x{combo.scoreBreakdown.weights.routing ?? 0}</span
528
+ >
529
+ </div>
530
+ {/if}
493
531
  {:else}
494
532
  <div class="fw-dev-tooltip-error">
495
533
  {combo.incompatibleReason || "Incompatible"}
@@ -619,7 +619,7 @@
619
619
  }
620
620
  }
621
621
 
622
- .fw-player-root .idle-container {
622
+ .idle-container {
623
623
  position: absolute;
624
624
  inset: 0;
625
625
  z-index: 5;
@@ -644,7 +644,7 @@
644
644
  -webkit-user-select: none;
645
645
  }
646
646
 
647
- .fw-player-root .bubble {
647
+ .bubble {
648
648
  position: absolute;
649
649
  border-radius: 50%;
650
650
  transition: opacity 1s ease-in-out;
@@ -654,7 +654,7 @@
654
654
  user-select: none;
655
655
  }
656
656
 
657
- .fw-player-root .particle {
657
+ .particle {
658
658
  position: absolute;
659
659
  border-radius: 50%;
660
660
  opacity: 0;
@@ -665,7 +665,7 @@
665
665
  user-select: none;
666
666
  }
667
667
 
668
- .fw-player-root .center-logo {
668
+ .center-logo {
669
669
  position: absolute;
670
670
  top: 50%;
671
671
  left: 50%;
@@ -679,7 +679,7 @@
679
679
  user-select: none;
680
680
  }
681
681
 
682
- .fw-player-root .logo-pulse {
682
+ .logo-pulse {
683
683
  position: absolute;
684
684
  border-radius: 50%;
685
685
  background: rgba(122, 162, 247, 0.15);
@@ -691,18 +691,18 @@
691
691
  transition: transform 0.3s ease-out;
692
692
  }
693
693
 
694
- .fw-player-root .logo-pulse.hovered {
694
+ .logo-pulse.hovered {
695
695
  animation: logoPulse 1s ease-in-out infinite;
696
696
  transform: scale(1.2);
697
697
  }
698
698
 
699
- .fw-player-root .logo-button {
699
+ .logo-button {
700
700
  all: unset;
701
701
  cursor: pointer;
702
702
  display: block;
703
703
  }
704
704
 
705
- .fw-player-root .logo-image {
705
+ .logo-image {
706
706
  position: relative;
707
707
  z-index: 1;
708
708
  filter: drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3));
@@ -714,13 +714,13 @@
714
714
  -webkit-touch-callout: none;
715
715
  }
716
716
 
717
- .fw-player-root .logo-image.hovered {
717
+ .logo-image.hovered {
718
718
  filter: drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1);
719
719
  transform: scale(1.1);
720
720
  cursor: pointer;
721
721
  }
722
722
 
723
- .fw-player-root .overlay-texture {
723
+ .overlay-texture {
724
724
  position: absolute;
725
725
  top: 0;
726
726
  left: 0;
@@ -736,7 +736,7 @@
736
736
  user-select: none;
737
737
  }
738
738
 
739
- .fw-player-root .hitmarker {
739
+ .hitmarker {
740
740
  position: absolute;
741
741
  transform: translate(-50%, -50%);
742
742
  pointer-events: none;
@@ -745,7 +745,7 @@
745
745
  height: 40px;
746
746
  }
747
747
 
748
- .fw-player-root .hitmarker-line {
748
+ .hitmarker-line {
749
749
  position: absolute;
750
750
  width: 12px;
751
751
  height: 3px;
@@ -754,31 +754,31 @@
754
754
  border-radius: 1px;
755
755
  }
756
756
 
757
- .fw-player-root .hitmarker-line.tl {
757
+ .hitmarker-line.tl {
758
758
  top: 25%;
759
759
  left: 25%;
760
760
  animation: hitmarkerFade45 0.6s ease-out forwards;
761
761
  }
762
762
 
763
- .fw-player-root .hitmarker-line.tr {
763
+ .hitmarker-line.tr {
764
764
  top: 25%;
765
765
  left: 75%;
766
766
  animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
767
767
  }
768
768
 
769
- .fw-player-root .hitmarker-line.bl {
769
+ .hitmarker-line.bl {
770
770
  top: 75%;
771
771
  left: 25%;
772
772
  animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
773
773
  }
774
774
 
775
- .fw-player-root .hitmarker-line.br {
775
+ .hitmarker-line.br {
776
776
  top: 75%;
777
777
  left: 75%;
778
778
  animation: hitmarkerFade45 0.6s ease-out forwards;
779
779
  }
780
780
 
781
- .fw-player-root .status-overlay {
781
+ .status-overlay {
782
782
  position: absolute;
783
783
  bottom: 16px;
784
784
  left: 50%;
@@ -792,7 +792,7 @@
792
792
  text-align: center;
793
793
  }
794
794
 
795
- .fw-player-root .status-indicator {
795
+ .status-indicator {
796
796
  display: flex;
797
797
  align-items: center;
798
798
  gap: 8px;
@@ -801,16 +801,16 @@
801
801
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
802
802
  }
803
803
 
804
- .fw-player-root .status-icon {
804
+ .status-icon {
805
805
  width: 20px;
806
806
  height: 20px;
807
807
  }
808
808
 
809
- .fw-player-root .status-icon.spinning {
809
+ .status-icon.spinning {
810
810
  animation: spin 1s linear infinite;
811
811
  }
812
812
 
813
- .fw-player-root .progress-bar {
813
+ .progress-bar {
814
814
  width: 160px;
815
815
  height: 4px;
816
816
  background: rgba(65, 72, 104, 0.4);
@@ -818,13 +818,13 @@
818
818
  overflow: hidden;
819
819
  }
820
820
 
821
- .fw-player-root .progress-fill {
821
+ .progress-fill {
822
822
  height: 100%;
823
823
  background: hsl(var(--tn-cyan, 193 100% 75%));
824
824
  transition: width 0.3s ease-out;
825
825
  }
826
826
 
827
- .fw-player-root .retry-button {
827
+ .retry-button {
828
828
  padding: 6px 16px;
829
829
  background: transparent;
830
830
  border: 1px solid rgba(122, 162, 247, 0.4);
@@ -837,7 +837,7 @@
837
837
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
838
838
  }
839
839
 
840
- .fw-player-root .retry-button:hover {
840
+ .retry-button:hover {
841
841
  background: rgba(122, 162, 247, 0.1);
842
842
  }
843
843
  </style>
@@ -537,7 +537,7 @@
537
537
  }
538
538
  }
539
539
 
540
- .fw-player-root .loading-container {
540
+ .loading-container {
541
541
  position: relative;
542
542
  width: 100%;
543
543
  height: 100%;
@@ -563,7 +563,7 @@
563
563
  -webkit-user-select: none;
564
564
  }
565
565
 
566
- .fw-player-root .bubble {
566
+ .bubble {
567
567
  position: absolute;
568
568
  border-radius: 50%;
569
569
  transition: opacity 1s ease-in-out;
@@ -573,7 +573,7 @@
573
573
  user-select: none;
574
574
  }
575
575
 
576
- .fw-player-root .particle {
576
+ .particle {
577
577
  position: absolute;
578
578
  border-radius: 50%;
579
579
  opacity: 0;
@@ -584,7 +584,7 @@
584
584
  user-select: none;
585
585
  }
586
586
 
587
- .fw-player-root .center-logo {
587
+ .center-logo {
588
588
  position: absolute;
589
589
  top: 50%;
590
590
  left: 50%;
@@ -598,7 +598,7 @@
598
598
  user-select: none;
599
599
  }
600
600
 
601
- .fw-player-root .logo-pulse {
601
+ .logo-pulse {
602
602
  position: absolute;
603
603
  border-radius: 50%;
604
604
  background: rgba(122, 162, 247, 0.15);
@@ -610,18 +610,18 @@
610
610
  transition: transform 0.3s ease-out;
611
611
  }
612
612
 
613
- .fw-player-root .logo-pulse.hovered {
613
+ .logo-pulse.hovered {
614
614
  animation: logoPulse 1s ease-in-out infinite;
615
615
  transform: scale(1.2);
616
616
  }
617
617
 
618
- .fw-player-root .logo-button {
618
+ .logo-button {
619
619
  all: unset;
620
620
  cursor: pointer;
621
621
  display: block;
622
622
  }
623
623
 
624
- .fw-player-root .logo-image {
624
+ .logo-image {
625
625
  position: relative;
626
626
  z-index: 1;
627
627
  filter: drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3));
@@ -633,13 +633,13 @@
633
633
  -webkit-touch-callout: none;
634
634
  }
635
635
 
636
- .fw-player-root .logo-image.hovered {
636
+ .logo-image.hovered {
637
637
  filter: drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1);
638
638
  transform: scale(1.1);
639
639
  cursor: pointer;
640
640
  }
641
641
 
642
- .fw-player-root .message {
642
+ .message {
643
643
  position: absolute;
644
644
  bottom: 20%;
645
645
  left: 50%;
@@ -657,7 +657,7 @@
657
657
  pointer-events: none;
658
658
  }
659
659
 
660
- .fw-player-root .overlay-texture {
660
+ .overlay-texture {
661
661
  position: absolute;
662
662
  top: 0;
663
663
  left: 0;
@@ -673,7 +673,7 @@
673
673
  user-select: none;
674
674
  }
675
675
 
676
- .fw-player-root .hitmarker {
676
+ .hitmarker {
677
677
  position: absolute;
678
678
  transform: translate(-50%, -50%);
679
679
  pointer-events: none;
@@ -682,7 +682,7 @@
682
682
  height: 40px;
683
683
  }
684
684
 
685
- .fw-player-root .hitmarker-line {
685
+ .hitmarker-line {
686
686
  position: absolute;
687
687
  width: 12px;
688
688
  height: 3px;
@@ -691,25 +691,25 @@
691
691
  border-radius: 1px;
692
692
  }
693
693
 
694
- .fw-player-root .hitmarker-line.tl {
694
+ .hitmarker-line.tl {
695
695
  top: 25%;
696
696
  left: 25%;
697
697
  animation: hitmarkerFade45 0.6s ease-out forwards;
698
698
  }
699
699
 
700
- .fw-player-root .hitmarker-line.tr {
700
+ .hitmarker-line.tr {
701
701
  top: 25%;
702
702
  left: 75%;
703
703
  animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
704
704
  }
705
705
 
706
- .fw-player-root .hitmarker-line.bl {
706
+ .hitmarker-line.bl {
707
707
  top: 75%;
708
708
  left: 25%;
709
709
  animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
710
710
  }
711
711
 
712
- .fw-player-root .hitmarker-line.br {
712
+ .hitmarker-line.br {
713
713
  top: 75%;
714
714
  left: 75%;
715
715
  animation: hitmarkerFade45 0.6s ease-out forwards;
@@ -602,6 +602,11 @@
602
602
  onfocusout={(e) => {
603
603
  if (!e.currentTarget.contains(e.relatedTarget as Node)) isVolumeFocused = false;
604
604
  }}
605
+ onpointerup={(e) => {
606
+ if (hasAudio && e.target === e.currentTarget) {
607
+ handleMute();
608
+ }
609
+ }}
605
610
  >
606
611
  <button
607
612
  type="button"
@@ -177,7 +177,7 @@
177
177
  {/if}
178
178
 
179
179
  <style>
180
- .fw-player-root .overlay-backdrop {
180
+ .overlay-backdrop {
181
181
  position: absolute;
182
182
  inset: 0;
183
183
  z-index: 20;
@@ -188,14 +188,14 @@
188
188
  backdrop-filter: blur(4px);
189
189
  }
190
190
 
191
- .fw-player-root .slab {
191
+ .slab {
192
192
  width: 280px;
193
193
  max-width: 90%;
194
194
  background-color: hsl(var(--tn-bg, 233 23% 17%) / 0.95);
195
195
  border: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
196
196
  }
197
197
 
198
- .fw-player-root .slab-header {
198
+ .slab-header {
199
199
  display: flex;
200
200
  align-items: center;
201
201
  gap: 0.5rem;
@@ -208,15 +208,15 @@
208
208
  color: hsl(var(--tn-fg-dark, 233 23% 60%));
209
209
  }
210
210
 
211
- .fw-player-root .slab-body {
211
+ .slab-body {
212
212
  padding: 1rem;
213
213
  }
214
214
 
215
- .fw-player-root .slab-actions {
215
+ .slab-actions {
216
216
  border-top: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
217
217
  }
218
218
 
219
- .fw-player-root .btn-flush {
219
+ .btn-flush {
220
220
  width: 100%;
221
221
  padding: 0.625rem 1rem;
222
222
  background: none;
@@ -230,41 +230,41 @@
230
230
  transition: background-color 0.15s;
231
231
  }
232
232
 
233
- .fw-player-root .btn-flush:hover {
233
+ .btn-flush:hover {
234
234
  background-color: hsl(var(--tn-bg-visual, 233 23% 20%) / 0.5);
235
235
  }
236
236
 
237
- .fw-player-root .progress-bar {
237
+ .progress-bar {
238
238
  height: 0.375rem;
239
239
  width: 100%;
240
240
  overflow: hidden;
241
241
  background-color: hsl(var(--tn-bg-visual, 233 23% 20%));
242
242
  }
243
243
 
244
- .fw-player-root .progress-fill {
244
+ .progress-fill {
245
245
  height: 100%;
246
246
  transition: width 0.3s ease;
247
247
  background-color: hsl(var(--tn-yellow, 40 70% 64%));
248
248
  }
249
249
 
250
- .fw-player-root .icon {
250
+ .icon {
251
251
  width: 1.25rem;
252
252
  height: 1.25rem;
253
253
  }
254
254
 
255
- .fw-player-root .icon-online {
255
+ .icon-online {
256
256
  color: hsl(var(--tn-green, 115 54% 57%));
257
257
  }
258
258
 
259
- .fw-player-root .icon-offline {
259
+ .icon-offline {
260
260
  color: hsl(var(--tn-red, 355 68% 65%));
261
261
  }
262
262
 
263
- .fw-player-root .icon-warning {
263
+ .icon-warning {
264
264
  color: hsl(var(--tn-yellow, 40 70% 64%));
265
265
  }
266
266
 
267
- .fw-player-root .polling-indicator {
267
+ .polling-indicator {
268
268
  display: flex;
269
269
  align-items: center;
270
270
  gap: 0.5rem;
@@ -273,7 +273,7 @@
273
273
  color: hsl(var(--tn-fg-dark, 233 23% 60%));
274
274
  }
275
275
 
276
- .fw-player-root .polling-dot {
276
+ .polling-dot {
277
277
  width: 0.375rem;
278
278
  height: 0.375rem;
279
279
  background-color: hsl(var(--tn-cyan, 192 78% 73%));
@@ -296,7 +296,7 @@
296
296
  }
297
297
  }
298
298
 
299
- .fw-player-root .animate-spin {
299
+ .animate-spin {
300
300
  animation: spin 1s linear infinite;
301
301
  }
302
302
  </style>
@@ -221,7 +221,7 @@
221
221
  {/if}
222
222
 
223
223
  <style>
224
- .fw-player-root .subtitle-container {
224
+ .subtitle-container {
225
225
  position: absolute;
226
226
  left: 50%;
227
227
  transform: translateX(-50%);
@@ -230,7 +230,7 @@
230
230
  pointer-events: none;
231
231
  }
232
232
 
233
- .fw-player-root .subtitle-text {
233
+ .subtitle-text {
234
234
  display: inline-block;
235
235
  white-space: pre-wrap;
236
236
  }
@@ -98,6 +98,8 @@ export interface PlayerControllerStore extends Readable<PlayerControllerState> {
98
98
  seek: (time: number) => void;
99
99
  /** Seek by delta */
100
100
  seekBy: (delta: number) => void;
101
+ /** Jump to live edge (for live streams) */
102
+ jumpToLive: () => void;
101
103
  /** Set volume */
102
104
  setVolume: (volume: number) => void;
103
105
  /** Toggle mute */
@@ -267,6 +267,9 @@ export function createPlayerControllerStore(config) {
267
267
  function seekBy(delta) {
268
268
  controller?.seekBy(delta);
269
269
  }
270
+ function jumpToLive() {
271
+ controller?.jumpToLive();
272
+ }
270
273
  function setVolume(volume) {
271
274
  controller?.setVolume(volume);
272
275
  }
@@ -338,6 +341,7 @@ export function createPlayerControllerStore(config) {
338
341
  togglePlay,
339
342
  seek,
340
343
  seekBy,
344
+ jumpToLive,
341
345
  setVolume,
342
346
  toggleMute,
343
347
  toggleLoop,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livepeer-frameworks/player-svelte",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Svelte 5 components for FrameWorks streaming player",
6
6
  "svelte": "./dist/index.js",
@@ -20,17 +20,9 @@
20
20
  },
21
21
  "./player.css": "./src/player.css"
22
22
  },
23
- "scripts": {
24
- "build": "svelte-package -i src -o dist",
25
- "build:watch": "svelte-package -i src -o dist --watch",
26
- "type-check": "svelte-check",
27
- "test": "vitest run",
28
- "test:watch": "vitest",
29
- "test:coverage": "vitest run --coverage"
30
- },
31
23
  "dependencies": {
32
- "@livepeer-frameworks/player-core": "workspace:*",
33
- "bits-ui": "^2.15.5"
24
+ "bits-ui": "^2.15.5",
25
+ "@livepeer-frameworks/player-core": "0.1.3"
34
26
  },
35
27
  "peerDependencies": {
36
28
  "svelte": "^5.0.0"
@@ -55,5 +47,13 @@
55
47
  "video"
56
48
  ],
57
49
  "author": "Livepeer FrameWorks",
58
- "license": "Unlicense"
59
- }
50
+ "license": "Unlicense",
51
+ "scripts": {
52
+ "build": "svelte-package -i src -o dist",
53
+ "build:watch": "svelte-package -i src -o dist --watch",
54
+ "type-check": "svelte-check",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage"
58
+ }
59
+ }
@@ -467,7 +467,8 @@
467
467
  {#if combo.compatible && combo.scoreBreakdown}
468
468
  <div class="fw-dev-tooltip-score">Score: {combo.score.toFixed(2)}</div>
469
469
  <div class="fw-dev-tooltip-row">
470
- Tracks: <span class="fw-dev-tooltip-value"
470
+ Tracks [{combo.scoreBreakdown.trackTypes.join(", ")}]:
471
+ <span class="fw-dev-tooltip-value"
471
472
  >{combo.scoreBreakdown.trackScore.toFixed(2)}</span
472
473
  >
473
474
  <span class="fw-dev-tooltip-weight"
@@ -490,6 +491,43 @@
490
491
  >x{combo.scoreBreakdown.weights.source}</span
491
492
  >
492
493
  </div>
494
+ {#if combo.scoreBreakdown.reliabilityScore !== undefined}
495
+ <div class="fw-dev-tooltip-row">
496
+ Reliability: <span class="fw-dev-tooltip-value"
497
+ >{combo.scoreBreakdown.reliabilityScore.toFixed(2)}</span
498
+ >
499
+ <span class="fw-dev-tooltip-weight"
500
+ >x{combo.scoreBreakdown.weights.reliability ?? 0}</span
501
+ >
502
+ </div>
503
+ {/if}
504
+ {#if combo.scoreBreakdown.modeBonus !== undefined && combo.scoreBreakdown.modeBonus !== 0}
505
+ <div class="fw-dev-tooltip-row">
506
+ Mode ({playbackMode}):
507
+ <span class="fw-dev-tooltip-bonus"
508
+ >+{combo.scoreBreakdown.modeBonus.toFixed(2)}</span
509
+ >
510
+ <span class="fw-dev-tooltip-weight"
511
+ >x{combo.scoreBreakdown.weights.mode ?? 0}</span
512
+ >
513
+ </div>
514
+ {/if}
515
+ {#if combo.scoreBreakdown.routingBonus !== undefined && combo.scoreBreakdown.routingBonus !== 0}
516
+ <div class="fw-dev-tooltip-row">
517
+ Routing: <span
518
+ class={combo.scoreBreakdown.routingBonus > 0
519
+ ? "fw-dev-tooltip-bonus"
520
+ : "fw-dev-tooltip-penalty"}
521
+ >
522
+ {combo.scoreBreakdown.routingBonus > 0
523
+ ? "+"
524
+ : ""}{combo.scoreBreakdown.routingBonus.toFixed(2)}
525
+ </span>
526
+ <span class="fw-dev-tooltip-weight"
527
+ >x{combo.scoreBreakdown.weights.routing ?? 0}</span
528
+ >
529
+ </div>
530
+ {/if}
493
531
  {:else}
494
532
  <div class="fw-dev-tooltip-error">
495
533
  {combo.incompatibleReason || "Incompatible"}
@@ -602,6 +602,11 @@
602
602
  onfocusout={(e) => {
603
603
  if (!e.currentTarget.contains(e.relatedTarget as Node)) isVolumeFocused = false;
604
604
  }}
605
+ onpointerup={(e) => {
606
+ if (hasAudio && e.target === e.currentTarget) {
607
+ handleMute();
608
+ }
609
+ }}
605
610
  >
606
611
  <button
607
612
  type="button"
@@ -106,6 +106,8 @@ export interface PlayerControllerStore extends Readable<PlayerControllerState> {
106
106
  seek: (time: number) => void;
107
107
  /** Seek by delta */
108
108
  seekBy: (delta: number) => void;
109
+ /** Jump to live edge (for live streams) */
110
+ jumpToLive: () => void;
109
111
  /** Set volume */
110
112
  setVolume: (volume: number) => void;
111
113
  /** Toggle mute */
@@ -485,6 +487,10 @@ export function createPlayerControllerStore(
485
487
  controller?.seekBy(delta);
486
488
  }
487
489
 
490
+ function jumpToLive(): void {
491
+ controller?.jumpToLive();
492
+ }
493
+
488
494
  function setVolume(volume: number): void {
489
495
  controller?.setVolume(volume);
490
496
  }
@@ -579,6 +585,7 @@ export function createPlayerControllerStore(
579
585
  togglePlay,
580
586
  seek,
581
587
  seekBy,
588
+ jumpToLive,
582
589
  setVolume,
583
590
  toggleMute,
584
591
  toggleLoop,