@livepeer-frameworks/streamcrafter-wc 0.1.1 → 0.1.2
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/dist/cjs/components/fw-sc-advanced.js +555 -71
- package/dist/cjs/components/fw-sc-advanced.js.map +1 -1
- package/dist/cjs/components/fw-streamcrafter.js +176 -1
- package/dist/cjs/components/fw-streamcrafter.js.map +1 -1
- package/dist/cjs/controllers/ingest-controller-host.js +34 -1
- package/dist/cjs/controllers/ingest-controller-host.js.map +1 -1
- package/dist/esm/components/fw-sc-advanced.js +556 -72
- package/dist/esm/components/fw-sc-advanced.js.map +1 -1
- package/dist/esm/components/fw-streamcrafter.js +177 -2
- package/dist/esm/components/fw-streamcrafter.js.map +1 -1
- package/dist/esm/controllers/ingest-controller-host.js +34 -1
- package/dist/esm/controllers/ingest-controller-host.js.map +1 -1
- package/dist/fw-streamcrafter.iife.js +646 -204
- package/dist/fw-streamcrafter.iife.js.map +1 -1
- package/dist/types/components/fw-sc-advanced.d.ts +12 -2
- package/dist/types/components/fw-streamcrafter.d.ts +12 -0
- package/dist/types/controllers/ingest-controller-host.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/fw-sc-advanced.ts +569 -93
- package/src/components/fw-streamcrafter.ts +178 -1
- package/src/controllers/ingest-controller-host.ts +36 -1
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
micMutedIcon,
|
|
18
18
|
videoIcon,
|
|
19
19
|
xIcon,
|
|
20
|
+
eyeIcon,
|
|
21
|
+
eyeOffIcon,
|
|
20
22
|
} from "../icons/index.js";
|
|
21
23
|
import { IngestControllerHost } from "../controllers/ingest-controller-host.js";
|
|
22
24
|
import type {
|
|
@@ -81,6 +83,7 @@ export class FwStreamCrafter extends LitElement {
|
|
|
81
83
|
@state() private _showSettings = false;
|
|
82
84
|
@state() private _showSources = true;
|
|
83
85
|
@state() private _isAdvancedPanelOpen = false;
|
|
86
|
+
@state() private _contextMenu: { x: number; y: number } | null = null;
|
|
84
87
|
|
|
85
88
|
@query(".fw-sc-preview video") private _videoEl!: HTMLVideoElement | null;
|
|
86
89
|
|
|
@@ -155,6 +158,45 @@ export class FwStreamCrafter extends LitElement {
|
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
// ---- Context Menu ----
|
|
162
|
+
|
|
163
|
+
private _boundDismissContextMenu = this._dismissContextMenu.bind(this);
|
|
164
|
+
|
|
165
|
+
private _handleContextMenu(e: MouseEvent) {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
this._contextMenu = { x: e.clientX, y: e.clientY };
|
|
168
|
+
document.addEventListener("click", this._boundDismissContextMenu, { once: true });
|
|
169
|
+
document.addEventListener("contextmenu", this._boundDismissContextMenu, { once: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private _dismissContextMenu() {
|
|
173
|
+
this._contextMenu = null;
|
|
174
|
+
document.removeEventListener("click", this._boundDismissContextMenu);
|
|
175
|
+
document.removeEventListener("contextmenu", this._boundDismissContextMenu);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private _copyWhipUrl() {
|
|
179
|
+
if (this.whipUrl) {
|
|
180
|
+
navigator.clipboard.writeText(this.whipUrl).catch(console.error);
|
|
181
|
+
}
|
|
182
|
+
this._contextMenu = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private _copyStreamInfo() {
|
|
186
|
+
const s = this.pc.s;
|
|
187
|
+
const profile = QUALITY_PROFILES.find((p) => p.id === s.qualityProfile);
|
|
188
|
+
const info = [
|
|
189
|
+
`Status: ${s.state}`,
|
|
190
|
+
`Quality: ${profile?.label ?? s.qualityProfile} (${profile?.description ?? ""})`,
|
|
191
|
+
`Sources: ${s.sources.length}`,
|
|
192
|
+
this.whipUrl ? `WHIP: ${this.whipUrl}` : null,
|
|
193
|
+
]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join("\n");
|
|
196
|
+
navigator.clipboard.writeText(info).catch(console.error);
|
|
197
|
+
this._contextMenu = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
158
200
|
// ---- Public API ----
|
|
159
201
|
|
|
160
202
|
async startCamera(options?: Parameters<IngestControllerHost["startCamera"]>[0]) {
|
|
@@ -205,6 +247,7 @@ export class FwStreamCrafter extends LitElement {
|
|
|
205
247
|
return html`
|
|
206
248
|
<div
|
|
207
249
|
class=${classMap({ root: true, "fw-sc-root": true, "fw-sc-root--devmode": this.devMode })}
|
|
250
|
+
@contextmenu=${(e: MouseEvent) => this._handleContextMenu(e)}
|
|
208
251
|
>
|
|
209
252
|
<div class="main fw-sc-main">
|
|
210
253
|
<!-- Header -->
|
|
@@ -279,6 +322,22 @@ export class FwStreamCrafter extends LitElement {
|
|
|
279
322
|
: nothing}
|
|
280
323
|
</div>
|
|
281
324
|
|
|
325
|
+
<!-- VU Meter -->
|
|
326
|
+
${s.isCapturing
|
|
327
|
+
? html`
|
|
328
|
+
<div class="fw-sc-vu-meter">
|
|
329
|
+
<div
|
|
330
|
+
class="fw-sc-vu-meter-fill"
|
|
331
|
+
style="width:${Math.min(s.audioLevel * 100, 100)}%"
|
|
332
|
+
></div>
|
|
333
|
+
<div
|
|
334
|
+
class="fw-sc-vu-meter-peak"
|
|
335
|
+
style="left:${Math.min(s.peakAudioLevel * 100, 100)}%"
|
|
336
|
+
></div>
|
|
337
|
+
</div>
|
|
338
|
+
`
|
|
339
|
+
: nothing}
|
|
340
|
+
|
|
282
341
|
<!-- Error -->
|
|
283
342
|
${s.error
|
|
284
343
|
? html`
|
|
@@ -360,11 +419,63 @@ export class FwStreamCrafter extends LitElement {
|
|
|
360
419
|
</div>
|
|
361
420
|
</div>
|
|
362
421
|
|
|
422
|
+
<!-- Context Menu -->
|
|
423
|
+
${this._contextMenu
|
|
424
|
+
? html`
|
|
425
|
+
<div
|
|
426
|
+
class="fw-sc-context-menu"
|
|
427
|
+
style="position:fixed;top:${this._contextMenu.y}px;left:${this._contextMenu.x}px;z-index:1000;background:#1a1b26;border:1px solid rgba(90,96,127,0.3);border-radius:6px;padding:4px;box-shadow:0 4px 12px rgba(0,0,0,0.5);min-width:160px"
|
|
428
|
+
>
|
|
429
|
+
${this.whipUrl
|
|
430
|
+
? html`
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
class="fw-sc-context-menu-item"
|
|
434
|
+
@click=${() => this._copyWhipUrl()}
|
|
435
|
+
>
|
|
436
|
+
Copy WHIP URL
|
|
437
|
+
</button>
|
|
438
|
+
`
|
|
439
|
+
: nothing}
|
|
440
|
+
<button
|
|
441
|
+
type="button"
|
|
442
|
+
class="fw-sc-context-menu-item"
|
|
443
|
+
@click=${() => this._copyStreamInfo()}
|
|
444
|
+
>
|
|
445
|
+
Copy Stream Info
|
|
446
|
+
</button>
|
|
447
|
+
${this.devMode
|
|
448
|
+
? html`
|
|
449
|
+
<div class="fw-sc-context-menu-separator"></div>
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
class="fw-sc-context-menu-item"
|
|
453
|
+
@click=${() => {
|
|
454
|
+
this._isAdvancedPanelOpen = !this._isAdvancedPanelOpen;
|
|
455
|
+
this._contextMenu = null;
|
|
456
|
+
}}
|
|
457
|
+
>
|
|
458
|
+
${settingsIcon(14)}
|
|
459
|
+
<span
|
|
460
|
+
>${this._isAdvancedPanelOpen ? "Hide Advanced" : "Advanced"}</span
|
|
461
|
+
>
|
|
462
|
+
</button>
|
|
463
|
+
`
|
|
464
|
+
: nothing}
|
|
465
|
+
</div>
|
|
466
|
+
`
|
|
467
|
+
: nothing}
|
|
468
|
+
|
|
363
469
|
<!-- Advanced Panel -->
|
|
364
470
|
${this.devMode && this._isAdvancedPanelOpen
|
|
365
471
|
? html`
|
|
366
472
|
<fw-sc-advanced
|
|
367
473
|
.ic=${this.pc}
|
|
474
|
+
.compositorEnabled=${this.enableCompositor}
|
|
475
|
+
.compositorRendererType=${this._getCompositorRendererType()}
|
|
476
|
+
.compositorStats=${this._getCompositorStats()}
|
|
477
|
+
.sceneCount=${this._getSceneCount()}
|
|
478
|
+
.layerCount=${this._getLayerCount()}
|
|
368
479
|
@fw-close=${() => {
|
|
369
480
|
this._isAdvancedPanelOpen = false;
|
|
370
481
|
}}
|
|
@@ -375,11 +486,77 @@ export class FwStreamCrafter extends LitElement {
|
|
|
375
486
|
`;
|
|
376
487
|
}
|
|
377
488
|
|
|
489
|
+
private _getSourceLayerVisibility(sourceId: string): boolean {
|
|
490
|
+
const ctrl = this.pc.getController();
|
|
491
|
+
if (!ctrl || !this.enableCompositor) return true;
|
|
492
|
+
const sm = ctrl.getSceneManager();
|
|
493
|
+
if (!sm) return true;
|
|
494
|
+
const scene = sm.getActiveScene();
|
|
495
|
+
if (!scene) return true;
|
|
496
|
+
const layer = scene.layers.find((l: { sourceId: string }) => l.sourceId === sourceId);
|
|
497
|
+
return layer?.visible ?? true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private _toggleSourceLayerVisibility(sourceId: string) {
|
|
501
|
+
const ctrl = this.pc.getController();
|
|
502
|
+
if (!ctrl) return;
|
|
503
|
+
const sm = ctrl.getSceneManager();
|
|
504
|
+
if (!sm) return;
|
|
505
|
+
const scene = sm.getActiveScene();
|
|
506
|
+
if (!scene) return;
|
|
507
|
+
const layer = scene.layers.find((l: { sourceId: string }) => l.sourceId === sourceId);
|
|
508
|
+
if (layer) {
|
|
509
|
+
sm.setLayerVisibility(scene.id, layer.id, !layer.visible);
|
|
510
|
+
this.requestUpdate();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ---- Compositor helpers (for advanced panel) ----
|
|
515
|
+
|
|
516
|
+
private _getCompositorRendererType() {
|
|
517
|
+
if (!this.enableCompositor) return null;
|
|
518
|
+
return this.pc.getController()?.getSceneManager()?.getRendererType() ?? null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private _getCompositorStats() {
|
|
522
|
+
if (!this.enableCompositor) return null;
|
|
523
|
+
return this.pc.getController()?.getSceneManager()?.getStats() ?? null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private _getSceneCount(): number {
|
|
527
|
+
if (!this.enableCompositor) return 0;
|
|
528
|
+
return this.pc.getController()?.getSceneManager()?.getAllScenes()?.length ?? 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private _getLayerCount(): number {
|
|
532
|
+
if (!this.enableCompositor) return 0;
|
|
533
|
+
const scene = this.pc.getController()?.getSceneManager()?.getActiveScene();
|
|
534
|
+
return scene?.layers?.length ?? 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
378
537
|
private _renderSourceRow(source: MediaSource) {
|
|
379
538
|
const s = this.pc.s;
|
|
380
539
|
const hasVideo = source.stream.getVideoTracks().length > 0;
|
|
540
|
+
const isVisible = this._getSourceLayerVisibility(source.id);
|
|
381
541
|
return html`
|
|
382
|
-
<div
|
|
542
|
+
<div
|
|
543
|
+
class=${classMap({ "fw-sc-source": true, "fw-sc-source--hidden": !isVisible })}
|
|
544
|
+
>
|
|
545
|
+
${this.enableCompositor
|
|
546
|
+
? html`
|
|
547
|
+
<button
|
|
548
|
+
type="button"
|
|
549
|
+
class=${classMap({
|
|
550
|
+
"fw-sc-icon-btn": true,
|
|
551
|
+
"fw-sc-icon-btn--muted": !isVisible,
|
|
552
|
+
})}
|
|
553
|
+
@click=${() => this._toggleSourceLayerVisibility(source.id)}
|
|
554
|
+
title=${isVisible ? "Hide from composition" : "Show in composition"}
|
|
555
|
+
>
|
|
556
|
+
${isVisible ? eyeIcon(14) : eyeOffIcon(14)}
|
|
557
|
+
</button>
|
|
558
|
+
`
|
|
559
|
+
: nothing}
|
|
383
560
|
<div class="fw-sc-source-icon">
|
|
384
561
|
${source.type === "camera" ? cameraIcon(16) : monitorIcon(16)}
|
|
385
562
|
</div>
|
|
@@ -52,6 +52,8 @@ export interface IngestControllerHostState {
|
|
|
52
52
|
isWebCodecsActive: boolean;
|
|
53
53
|
isWebCodecsAvailable: boolean;
|
|
54
54
|
encoderStats: EncoderStats | null;
|
|
55
|
+
audioLevel: number;
|
|
56
|
+
peakAudioLevel: number;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
type HostElement = ReactiveControllerHost & HTMLElement;
|
|
@@ -61,6 +63,7 @@ export class IngestControllerHost implements ReactiveController {
|
|
|
61
63
|
private controller: IngestControllerV2 | null = null;
|
|
62
64
|
private unsubs: Array<() => void> = [];
|
|
63
65
|
private encoderStatsCleanup: (() => void) | null = null;
|
|
66
|
+
private audioLevelCleanup: (() => void) | null = null;
|
|
64
67
|
|
|
65
68
|
s: IngestControllerHostState;
|
|
66
69
|
|
|
@@ -85,6 +88,8 @@ export class IngestControllerHost implements ReactiveController {
|
|
|
85
88
|
isWebCodecsActive: false,
|
|
86
89
|
isWebCodecsAvailable: isWebCodecsEncodingPathSupported(),
|
|
87
90
|
encoderStats: null,
|
|
91
|
+
audioLevel: 0,
|
|
92
|
+
peakAudioLevel: 0,
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -114,6 +119,7 @@ export class IngestControllerHost implements ReactiveController {
|
|
|
114
119
|
this.encoderStatsCleanup();
|
|
115
120
|
this.encoderStatsCleanup = null;
|
|
116
121
|
}
|
|
122
|
+
this.stopAudioLevelMonitoring();
|
|
117
123
|
this.controller?.destroy();
|
|
118
124
|
this.controller = null;
|
|
119
125
|
}
|
|
@@ -134,16 +140,22 @@ export class IngestControllerHost implements ReactiveController {
|
|
|
134
140
|
controller.on("stateChange", (event) => {
|
|
135
141
|
const state = event.state;
|
|
136
142
|
const ctx = (event.context ?? {}) as IngestStateContextV2;
|
|
143
|
+
const isCapturing = state === "capturing" || state === "streaming";
|
|
137
144
|
this.update({
|
|
138
145
|
state,
|
|
139
146
|
stateContext: ctx,
|
|
140
147
|
isStreaming: state === "streaming",
|
|
141
|
-
isCapturing
|
|
148
|
+
isCapturing,
|
|
142
149
|
isReconnecting: state === "reconnecting",
|
|
143
150
|
mediaStream: controller.getMediaStream(),
|
|
144
151
|
sources: controller.getSources(),
|
|
145
152
|
reconnectionState: ctx.reconnection ?? this.s.reconnectionState,
|
|
146
153
|
});
|
|
154
|
+
if (isCapturing) {
|
|
155
|
+
this.startAudioLevelMonitoring();
|
|
156
|
+
} else {
|
|
157
|
+
this.stopAudioLevelMonitoring();
|
|
158
|
+
}
|
|
147
159
|
this.dispatchEvent("fw-sc-state-change", { state, context: ctx });
|
|
148
160
|
})
|
|
149
161
|
);
|
|
@@ -242,6 +254,29 @@ export class IngestControllerHost implements ReactiveController {
|
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
256
|
|
|
257
|
+
private startAudioLevelMonitoring() {
|
|
258
|
+
if (this.audioLevelCleanup || !this.controller) return;
|
|
259
|
+
const audioMixer = this.controller.getAudioMixer();
|
|
260
|
+
if (!audioMixer) return;
|
|
261
|
+
|
|
262
|
+
const unsub = audioMixer.on("levelUpdate", (event: { level: number; peakLevel: number }) => {
|
|
263
|
+
this.update({ audioLevel: event.level, peakAudioLevel: event.peakLevel });
|
|
264
|
+
});
|
|
265
|
+
audioMixer.startLevelMonitoring();
|
|
266
|
+
this.audioLevelCleanup = () => {
|
|
267
|
+
unsub();
|
|
268
|
+
audioMixer.stopLevelMonitoring();
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private stopAudioLevelMonitoring() {
|
|
273
|
+
if (this.audioLevelCleanup) {
|
|
274
|
+
this.audioLevelCleanup();
|
|
275
|
+
this.audioLevelCleanup = null;
|
|
276
|
+
}
|
|
277
|
+
this.update({ audioLevel: 0, peakAudioLevel: 0 });
|
|
278
|
+
}
|
|
279
|
+
|
|
245
280
|
private dispatchEvent(name: string, detail: unknown) {
|
|
246
281
|
this.host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
247
282
|
}
|