@dialtribe/react-sdk 0.1.0-alpha.20 → 0.1.0-alpha.21
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/{dialtribe-streamer-DGDbUw0Z.d.ts → dialtribe-streamer-D9ulVBVb.d.ts} +33 -43
- package/dist/{dialtribe-streamer-DHr_bgtO.d.mts → dialtribe-streamer-DH23BseY.d.mts} +33 -43
- package/dist/dialtribe-streamer.d.mts +1 -1
- package/dist/dialtribe-streamer.d.ts +1 -1
- package/dist/dialtribe-streamer.js +240 -98
- package/dist/dialtribe-streamer.js.map +1 -1
- package/dist/dialtribe-streamer.mjs +240 -98
- package/dist/dialtribe-streamer.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +240 -98
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +240 -98
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { HelloWorld, HelloWorldProps } from './hello-world.mjs';
|
|
2
2
|
export { A as ApiClientConfig, h as AudioWaveform, B as Broadcast, C as CDN_DOMAIN, d as DIALTRIBE_API_BASE, c as DialtribeClient, a as DialtribeContextValue, i as DialtribeOverlay, k as DialtribeOverlayMode, j as DialtribeOverlayProps, e as DialtribePlayer, g as DialtribePlayerErrorBoundary, f as DialtribePlayerProps, D as DialtribeProvider, b as DialtribeProviderProps, E as ENDPOINTS, H as HTTP_STATUS, q as HttpStatusCode, L as LoadingSpinner, m as TranscriptData, l as TranscriptSegment, T as TranscriptWord, o as buildBroadcastCdnUrl, p as buildBroadcastS3KeyPrefix, n as formatTime, u as useDialtribe } from './dialtribe-player-CNriUtNi.mjs';
|
|
3
|
-
export { j as DEFAULT_ENCODER_SERVER_URL, D as DialtribeStreamer, a as DialtribeStreamerProps, M as MediaConstraintsOptions, O as OpenDialtribeStreamerPopupOptions, P as PopupDimensions, w as PopupFallbackMode, l as StreamDiagnostics, f as StreamKeyDisplay, g as StreamKeyDisplayProps, h as StreamKeyInput, i as StreamKeyInputProps, e as StreamingControlState, c as StreamingControls, d as StreamingControlsProps, S as StreamingPreview, b as StreamingPreviewProps, t as UseDialtribeStreamerLauncherOptions, v as UseDialtribeStreamerLauncherReturn, U as UseDialtribeStreamerPopupReturn, W as WebSocketStreamer, k as WebSocketStreamerOptions, r as calculatePopupDimensions, o as checkBrowserCompatibility, m as getMediaConstraints, n as getMediaRecorderOptions, q as openBroadcastPopup, p as openDialtribeStreamerPopup, s as useDialtribeStreamerLauncher, u as useDialtribeStreamerPopup } from './dialtribe-streamer-
|
|
3
|
+
export { j as DEFAULT_ENCODER_SERVER_URL, D as DialtribeStreamer, a as DialtribeStreamerProps, M as MediaConstraintsOptions, O as OpenDialtribeStreamerPopupOptions, P as PopupDimensions, w as PopupFallbackMode, l as StreamDiagnostics, f as StreamKeyDisplay, g as StreamKeyDisplayProps, h as StreamKeyInput, i as StreamKeyInputProps, e as StreamingControlState, c as StreamingControls, d as StreamingControlsProps, S as StreamingPreview, b as StreamingPreviewProps, t as UseDialtribeStreamerLauncherOptions, v as UseDialtribeStreamerLauncherReturn, U as UseDialtribeStreamerPopupReturn, W as WebSocketStreamer, k as WebSocketStreamerOptions, r as calculatePopupDimensions, o as checkBrowserCompatibility, m as getMediaConstraints, n as getMediaRecorderOptions, q as openBroadcastPopup, p as openDialtribeStreamerPopup, s as useDialtribeStreamerLauncher, u as useDialtribeStreamerPopup } from './dialtribe-streamer-DH23BseY.mjs';
|
|
4
4
|
import 'react/jsx-runtime';
|
|
5
5
|
import 'react';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { HelloWorld, HelloWorldProps } from './hello-world.js';
|
|
2
2
|
export { A as ApiClientConfig, h as AudioWaveform, B as Broadcast, C as CDN_DOMAIN, d as DIALTRIBE_API_BASE, c as DialtribeClient, a as DialtribeContextValue, i as DialtribeOverlay, k as DialtribeOverlayMode, j as DialtribeOverlayProps, e as DialtribePlayer, g as DialtribePlayerErrorBoundary, f as DialtribePlayerProps, D as DialtribeProvider, b as DialtribeProviderProps, E as ENDPOINTS, H as HTTP_STATUS, q as HttpStatusCode, L as LoadingSpinner, m as TranscriptData, l as TranscriptSegment, T as TranscriptWord, o as buildBroadcastCdnUrl, p as buildBroadcastS3KeyPrefix, n as formatTime, u as useDialtribe } from './dialtribe-player-CNriUtNi.js';
|
|
3
|
-
export { j as DEFAULT_ENCODER_SERVER_URL, D as DialtribeStreamer, a as DialtribeStreamerProps, M as MediaConstraintsOptions, O as OpenDialtribeStreamerPopupOptions, P as PopupDimensions, w as PopupFallbackMode, l as StreamDiagnostics, f as StreamKeyDisplay, g as StreamKeyDisplayProps, h as StreamKeyInput, i as StreamKeyInputProps, e as StreamingControlState, c as StreamingControls, d as StreamingControlsProps, S as StreamingPreview, b as StreamingPreviewProps, t as UseDialtribeStreamerLauncherOptions, v as UseDialtribeStreamerLauncherReturn, U as UseDialtribeStreamerPopupReturn, W as WebSocketStreamer, k as WebSocketStreamerOptions, r as calculatePopupDimensions, o as checkBrowserCompatibility, m as getMediaConstraints, n as getMediaRecorderOptions, q as openBroadcastPopup, p as openDialtribeStreamerPopup, s as useDialtribeStreamerLauncher, u as useDialtribeStreamerPopup } from './dialtribe-streamer-
|
|
3
|
+
export { j as DEFAULT_ENCODER_SERVER_URL, D as DialtribeStreamer, a as DialtribeStreamerProps, M as MediaConstraintsOptions, O as OpenDialtribeStreamerPopupOptions, P as PopupDimensions, w as PopupFallbackMode, l as StreamDiagnostics, f as StreamKeyDisplay, g as StreamKeyDisplayProps, h as StreamKeyInput, i as StreamKeyInputProps, e as StreamingControlState, c as StreamingControls, d as StreamingControlsProps, S as StreamingPreview, b as StreamingPreviewProps, t as UseDialtribeStreamerLauncherOptions, v as UseDialtribeStreamerLauncherReturn, U as UseDialtribeStreamerPopupReturn, W as WebSocketStreamer, k as WebSocketStreamerOptions, r as calculatePopupDimensions, o as checkBrowserCompatibility, m as getMediaConstraints, n as getMediaRecorderOptions, q as openBroadcastPopup, p as openDialtribeStreamerPopup, s as useDialtribeStreamerLauncher, u as useDialtribeStreamerPopup } from './dialtribe-streamer-D9ulVBVb.js';
|
|
4
4
|
import 'react/jsx-runtime';
|
|
5
5
|
import 'react';
|
package/dist/index.js
CHANGED
|
@@ -2273,6 +2273,9 @@ var WebSocketStreamer = class {
|
|
|
2273
2273
|
this.isHotSwapping = false;
|
|
2274
2274
|
// Track if we're swapping media streams
|
|
2275
2275
|
this.startTime = 0;
|
|
2276
|
+
// Canvas-based rendering for seamless camera flips
|
|
2277
|
+
// MediaRecorder records from canvas stream, so track changes don't affect it
|
|
2278
|
+
this.canvasState = null;
|
|
2276
2279
|
this.streamKey = options.streamKey;
|
|
2277
2280
|
this.mediaStream = options.mediaStream;
|
|
2278
2281
|
this.isVideo = options.isVideo;
|
|
@@ -2281,6 +2284,36 @@ var WebSocketStreamer = class {
|
|
|
2281
2284
|
this.onStateChange = options.onStateChange;
|
|
2282
2285
|
this.onError = options.onError;
|
|
2283
2286
|
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Calculate scaled dimensions for fitting video into canvas.
|
|
2289
|
+
* @param mode - "contain" fits video inside canvas, "cover" fills canvas (cropping)
|
|
2290
|
+
*/
|
|
2291
|
+
calculateScaledDimensions(videoWidth, videoHeight, canvasWidth, canvasHeight, mode) {
|
|
2292
|
+
const videoAspect = videoWidth / videoHeight;
|
|
2293
|
+
const canvasAspect = canvasWidth / canvasHeight;
|
|
2294
|
+
const useWidthBased = mode === "contain" ? videoAspect > canvasAspect : videoAspect <= canvasAspect;
|
|
2295
|
+
if (useWidthBased) {
|
|
2296
|
+
const width = canvasWidth;
|
|
2297
|
+
const height = canvasWidth / videoAspect;
|
|
2298
|
+
return { x: 0, y: (canvasHeight - height) / 2, width, height };
|
|
2299
|
+
} else {
|
|
2300
|
+
const height = canvasHeight;
|
|
2301
|
+
const width = canvasHeight * videoAspect;
|
|
2302
|
+
return { x: (canvasWidth - width) / 2, y: 0, width, height };
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Invalidate cached scaling dimensions (call when video source changes)
|
|
2307
|
+
*/
|
|
2308
|
+
invalidateScalingCache() {
|
|
2309
|
+
if (this.canvasState) {
|
|
2310
|
+
this.canvasState.cachedContain = null;
|
|
2311
|
+
this.canvasState.cachedCover = null;
|
|
2312
|
+
this.canvasState.cachedNeedsBackground = false;
|
|
2313
|
+
this.canvasState.lastVideoWidth = 0;
|
|
2314
|
+
this.canvasState.lastVideoHeight = 0;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2284
2317
|
/**
|
|
2285
2318
|
* Validate stream key format
|
|
2286
2319
|
* Stream keys must follow format: {tierCode}{foreignId}_{randomKey}
|
|
@@ -2307,6 +2340,130 @@ var WebSocketStreamer = class {
|
|
|
2307
2340
|
isVIP: tierCode === "b" || tierCode === "w"
|
|
2308
2341
|
});
|
|
2309
2342
|
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Set up canvas-based rendering pipeline for video streams.
|
|
2345
|
+
* This allows seamless camera flips by changing the video source
|
|
2346
|
+
* without affecting MediaRecorder (which records from the canvas).
|
|
2347
|
+
*/
|
|
2348
|
+
setupCanvasRendering() {
|
|
2349
|
+
console.log("\u{1F3A8} Setting up canvas-based rendering for seamless camera flips");
|
|
2350
|
+
const videoTrack = this.mediaStream.getVideoTracks()[0];
|
|
2351
|
+
const settings = videoTrack?.getSettings() || {};
|
|
2352
|
+
const width = settings.width || 1280;
|
|
2353
|
+
const height = settings.height || 720;
|
|
2354
|
+
console.log(`\u{1F4D0} Video dimensions: ${width}x${height}`);
|
|
2355
|
+
const canvas = document.createElement("canvas");
|
|
2356
|
+
canvas.width = width;
|
|
2357
|
+
canvas.height = height;
|
|
2358
|
+
const ctx = canvas.getContext("2d");
|
|
2359
|
+
if (!ctx) {
|
|
2360
|
+
throw new Error("Failed to get 2D canvas context - canvas rendering unavailable");
|
|
2361
|
+
}
|
|
2362
|
+
const videoElement = document.createElement("video");
|
|
2363
|
+
videoElement.srcObject = this.mediaStream;
|
|
2364
|
+
videoElement.muted = true;
|
|
2365
|
+
videoElement.playsInline = true;
|
|
2366
|
+
videoElement.play().catch((e) => console.warn("Video autoplay warning:", e));
|
|
2367
|
+
const frameRate = settings.frameRate || 30;
|
|
2368
|
+
const stream = canvas.captureStream(frameRate);
|
|
2369
|
+
const audioTracks = this.mediaStream.getAudioTracks();
|
|
2370
|
+
audioTracks.forEach((track) => {
|
|
2371
|
+
stream.addTrack(track);
|
|
2372
|
+
});
|
|
2373
|
+
console.log(`\u{1F3AC} Canvas stream created with ${frameRate}fps video + ${audioTracks.length} audio track(s)`);
|
|
2374
|
+
this.canvasState = {
|
|
2375
|
+
canvas,
|
|
2376
|
+
ctx,
|
|
2377
|
+
videoElement,
|
|
2378
|
+
stream,
|
|
2379
|
+
renderLoopId: 0,
|
|
2380
|
+
// Will be set below
|
|
2381
|
+
useBlurBackground: true,
|
|
2382
|
+
slowFrameCount: 0,
|
|
2383
|
+
cachedContain: null,
|
|
2384
|
+
cachedCover: null,
|
|
2385
|
+
cachedNeedsBackground: false,
|
|
2386
|
+
lastVideoWidth: 0,
|
|
2387
|
+
lastVideoHeight: 0
|
|
2388
|
+
};
|
|
2389
|
+
const state = this.canvasState;
|
|
2390
|
+
const renderFrame = () => {
|
|
2391
|
+
if (!this.canvasState || state !== this.canvasState) return;
|
|
2392
|
+
const { ctx: ctx2, canvas: canvas2, videoElement: videoElement2 } = state;
|
|
2393
|
+
if (videoElement2.paused) {
|
|
2394
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const canvasWidth = canvas2.width;
|
|
2398
|
+
const canvasHeight = canvas2.height;
|
|
2399
|
+
const videoWidth = videoElement2.videoWidth;
|
|
2400
|
+
const videoHeight = videoElement2.videoHeight;
|
|
2401
|
+
if (videoWidth === 0 || videoHeight === 0) {
|
|
2402
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (videoWidth !== state.lastVideoWidth || videoHeight !== state.lastVideoHeight) {
|
|
2406
|
+
state.lastVideoWidth = videoWidth;
|
|
2407
|
+
state.lastVideoHeight = videoHeight;
|
|
2408
|
+
state.cachedContain = this.calculateScaledDimensions(
|
|
2409
|
+
videoWidth,
|
|
2410
|
+
videoHeight,
|
|
2411
|
+
canvasWidth,
|
|
2412
|
+
canvasHeight,
|
|
2413
|
+
"contain"
|
|
2414
|
+
);
|
|
2415
|
+
state.cachedCover = this.calculateScaledDimensions(
|
|
2416
|
+
videoWidth,
|
|
2417
|
+
videoHeight,
|
|
2418
|
+
canvasWidth,
|
|
2419
|
+
canvasHeight,
|
|
2420
|
+
"cover"
|
|
2421
|
+
);
|
|
2422
|
+
state.cachedNeedsBackground = Math.abs(state.cachedContain.width - canvasWidth) > 1 || Math.abs(state.cachedContain.height - canvasHeight) > 1;
|
|
2423
|
+
console.log(`\u{1F4D0} Video dimensions changed: ${videoWidth}x${videoHeight}, needsBackground: ${state.cachedNeedsBackground}`);
|
|
2424
|
+
}
|
|
2425
|
+
const contain = state.cachedContain;
|
|
2426
|
+
const cover = state.cachedCover;
|
|
2427
|
+
const frameStart = performance.now();
|
|
2428
|
+
if (state.cachedNeedsBackground && state.useBlurBackground) {
|
|
2429
|
+
ctx2.save();
|
|
2430
|
+
ctx2.filter = "blur(20px)";
|
|
2431
|
+
ctx2.drawImage(videoElement2, cover.x, cover.y, cover.width, cover.height);
|
|
2432
|
+
ctx2.restore();
|
|
2433
|
+
ctx2.fillStyle = "rgba(0, 0, 0, 0.5)";
|
|
2434
|
+
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
2435
|
+
} else if (state.cachedNeedsBackground) {
|
|
2436
|
+
ctx2.fillStyle = "#000";
|
|
2437
|
+
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
2438
|
+
}
|
|
2439
|
+
ctx2.drawImage(videoElement2, contain.x, contain.y, contain.width, contain.height);
|
|
2440
|
+
const frameDuration = performance.now() - frameStart;
|
|
2441
|
+
if (frameDuration > 16 && state.useBlurBackground) {
|
|
2442
|
+
state.slowFrameCount++;
|
|
2443
|
+
if (state.slowFrameCount > 5) {
|
|
2444
|
+
console.log("\u26A1 Disabling blur background for performance");
|
|
2445
|
+
state.useBlurBackground = false;
|
|
2446
|
+
}
|
|
2447
|
+
} else if (frameDuration <= 16) {
|
|
2448
|
+
state.slowFrameCount = 0;
|
|
2449
|
+
}
|
|
2450
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2451
|
+
};
|
|
2452
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2453
|
+
console.log("\u2705 Canvas rendering pipeline ready (with adaptive blur background)");
|
|
2454
|
+
return stream;
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Clean up canvas rendering resources
|
|
2458
|
+
*/
|
|
2459
|
+
cleanupCanvasRendering() {
|
|
2460
|
+
if (!this.canvasState) return;
|
|
2461
|
+
cancelAnimationFrame(this.canvasState.renderLoopId);
|
|
2462
|
+
this.canvasState.videoElement.pause();
|
|
2463
|
+
this.canvasState.videoElement.srcObject = null;
|
|
2464
|
+
this.canvasState.stream.getTracks().forEach((track) => track.stop());
|
|
2465
|
+
this.canvasState = null;
|
|
2466
|
+
}
|
|
2310
2467
|
/**
|
|
2311
2468
|
* Build WebSocket URL from stream key
|
|
2312
2469
|
*/
|
|
@@ -2326,6 +2483,10 @@ var WebSocketStreamer = class {
|
|
|
2326
2483
|
*/
|
|
2327
2484
|
async start() {
|
|
2328
2485
|
try {
|
|
2486
|
+
this.userStopped = false;
|
|
2487
|
+
this.chunksSent = 0;
|
|
2488
|
+
this.bytesSent = 0;
|
|
2489
|
+
this.startTime = 0;
|
|
2329
2490
|
this.validateStreamKeyFormat();
|
|
2330
2491
|
this.onStateChange?.("connecting");
|
|
2331
2492
|
const wsUrl = this.buildWebSocketUrl();
|
|
@@ -2336,8 +2497,15 @@ var WebSocketStreamer = class {
|
|
|
2336
2497
|
reject(new Error("WebSocket not initialized"));
|
|
2337
2498
|
return;
|
|
2338
2499
|
}
|
|
2339
|
-
|
|
2500
|
+
const timeoutId = setTimeout(() => {
|
|
2501
|
+
reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
|
|
2502
|
+
}, 1e4);
|
|
2503
|
+
this.websocket.addEventListener("open", () => {
|
|
2504
|
+
clearTimeout(timeoutId);
|
|
2505
|
+
resolve();
|
|
2506
|
+
}, { once: true });
|
|
2340
2507
|
this.websocket.addEventListener("error", (event) => {
|
|
2508
|
+
clearTimeout(timeoutId);
|
|
2341
2509
|
console.error("\u274C WebSocket error event:", event);
|
|
2342
2510
|
console.error("\u{1F50D} Connection diagnostics:", {
|
|
2343
2511
|
url: wsUrl.replace(this.streamKey, "***"),
|
|
@@ -2356,16 +2524,17 @@ Common causes:
|
|
|
2356
2524
|
Please check encoder server logs and DATABASE_URL configuration.`
|
|
2357
2525
|
));
|
|
2358
2526
|
}, { once: true });
|
|
2359
|
-
setTimeout(() => {
|
|
2360
|
-
reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
|
|
2361
|
-
}, 1e4);
|
|
2362
2527
|
});
|
|
2363
2528
|
console.log("\u2705 WebSocket connected");
|
|
2364
2529
|
this.setupWebSocketHandlers();
|
|
2530
|
+
const streamToRecord = this.isVideo ? this.setupCanvasRendering() : this.mediaStream;
|
|
2365
2531
|
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2366
2532
|
this.mimeType = recorderOptions.mimeType;
|
|
2367
|
-
this.mediaRecorder = new MediaRecorder(
|
|
2533
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
2368
2534
|
console.log("\u{1F399}\uFE0F MediaRecorder created with options:", recorderOptions);
|
|
2535
|
+
if (this.isVideo) {
|
|
2536
|
+
console.log("\u{1F3A8} Recording from canvas stream (enables seamless camera flips)");
|
|
2537
|
+
}
|
|
2369
2538
|
this.setupMediaRecorderHandlers();
|
|
2370
2539
|
this.mediaRecorder.start(300);
|
|
2371
2540
|
this.startTime = Date.now();
|
|
@@ -2402,9 +2571,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2402
2571
|
} else {
|
|
2403
2572
|
console.log("\u26A0\uFE0F No WebSocket to close");
|
|
2404
2573
|
}
|
|
2574
|
+
this.cleanupCanvasRendering();
|
|
2405
2575
|
this.mediaRecorder = null;
|
|
2406
2576
|
this.websocket = null;
|
|
2407
|
-
this.bytesSent = 0;
|
|
2408
2577
|
this.onStateChange?.("stopped");
|
|
2409
2578
|
}
|
|
2410
2579
|
/**
|
|
@@ -2413,6 +2582,13 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2413
2582
|
getBytesSent() {
|
|
2414
2583
|
return this.bytesSent;
|
|
2415
2584
|
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Get the current source media stream.
|
|
2587
|
+
* This may change after replaceVideoTrack() is called.
|
|
2588
|
+
*/
|
|
2589
|
+
getMediaStream() {
|
|
2590
|
+
return this.mediaStream;
|
|
2591
|
+
}
|
|
2416
2592
|
/**
|
|
2417
2593
|
* Get current diagnostics
|
|
2418
2594
|
*/
|
|
@@ -2427,25 +2603,41 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2427
2603
|
};
|
|
2428
2604
|
}
|
|
2429
2605
|
/**
|
|
2430
|
-
* Replace the video track
|
|
2431
|
-
* This avoids sending a new WebM header, which FFmpeg can't handle mid-stream.
|
|
2606
|
+
* Replace the video track for camera flips.
|
|
2432
2607
|
*
|
|
2433
|
-
*
|
|
2434
|
-
*
|
|
2608
|
+
* When using canvas-based rendering (video streams), this updates the video
|
|
2609
|
+
* element source. The canvas continues drawing, and MediaRecorder is unaffected.
|
|
2435
2610
|
*
|
|
2436
2611
|
* @param newVideoTrack - The new video track from the flipped camera
|
|
2437
2612
|
*/
|
|
2438
2613
|
replaceVideoTrack(newVideoTrack) {
|
|
2439
|
-
console.log("\u{1F504} Replacing video track
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
this.mediaStream.
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2614
|
+
console.log("\u{1F504} Replacing video track");
|
|
2615
|
+
if (this.canvasState) {
|
|
2616
|
+
console.log("\u{1F3A8} Using canvas-based swap (MediaRecorder unaffected)");
|
|
2617
|
+
const audioTracks = this.mediaStream.getAudioTracks();
|
|
2618
|
+
const newStream = new MediaStream([newVideoTrack, ...audioTracks]);
|
|
2619
|
+
this.mediaStream.getVideoTracks().forEach((track) => track.stop());
|
|
2620
|
+
this.canvasState.videoElement.srcObject = newStream;
|
|
2621
|
+
this.canvasState.videoElement.play().catch((e) => console.warn("Video play warning:", e));
|
|
2622
|
+
this.mediaStream = newStream;
|
|
2623
|
+
this.invalidateScalingCache();
|
|
2624
|
+
const settings = newVideoTrack.getSettings();
|
|
2625
|
+
if (settings.width && settings.height) {
|
|
2626
|
+
console.log(`\u{1F4D0} New camera resolution: ${settings.width}x${settings.height}`);
|
|
2627
|
+
}
|
|
2628
|
+
console.log("\u2705 Video source swapped - canvas continues seamlessly");
|
|
2629
|
+
} else {
|
|
2630
|
+
console.warn("\u26A0\uFE0F Canvas not available - attempting direct track replacement");
|
|
2631
|
+
const oldVideoTracks = this.mediaStream.getVideoTracks();
|
|
2632
|
+
this.mediaStream.addTrack(newVideoTrack);
|
|
2633
|
+
console.log("\u2795 New video track added");
|
|
2634
|
+
oldVideoTracks.forEach((track) => {
|
|
2635
|
+
this.mediaStream.removeTrack(track);
|
|
2636
|
+
track.stop();
|
|
2637
|
+
});
|
|
2638
|
+
console.log("\u2796 Old video track(s) removed");
|
|
2639
|
+
console.log("\u2705 Video track replaced");
|
|
2640
|
+
}
|
|
2449
2641
|
}
|
|
2450
2642
|
/**
|
|
2451
2643
|
* Replace the audio track in the current MediaStream without stopping MediaRecorder.
|
|
@@ -2456,88 +2648,28 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2456
2648
|
console.log("\u{1F504} Replacing audio track (no MediaRecorder restart)");
|
|
2457
2649
|
const oldAudioTracks = this.mediaStream.getAudioTracks();
|
|
2458
2650
|
this.mediaStream.addTrack(newAudioTrack);
|
|
2459
|
-
console.log("\u2795 New audio track added");
|
|
2651
|
+
console.log("\u2795 New audio track added to source stream");
|
|
2460
2652
|
oldAudioTracks.forEach((track) => {
|
|
2461
2653
|
this.mediaStream.removeTrack(track);
|
|
2462
2654
|
track.stop();
|
|
2463
2655
|
});
|
|
2464
|
-
console.log("\u2796 Old audio track(s) removed");
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
* Prepare for a hot-swap by stopping the MediaRecorder
|
|
2469
|
-
* Call this BEFORE requesting a new camera stream on iOS
|
|
2470
|
-
* This prevents iOS Safari from interfering with the active MediaRecorder
|
|
2471
|
-
*
|
|
2472
|
-
* IMPORTANT: This is async because we must wait for the MediaRecorder's 'stop'
|
|
2473
|
-
* event to fire BEFORE proceeding. Otherwise there's a race condition where
|
|
2474
|
-
* the stop event fires after isHotSwapping is set to false, causing the
|
|
2475
|
-
* WebSocket to close unexpectedly.
|
|
2476
|
-
*
|
|
2477
|
-
* @deprecated Prefer replaceVideoTrack() which doesn't restart MediaRecorder
|
|
2478
|
-
*/
|
|
2479
|
-
async prepareForHotSwap() {
|
|
2480
|
-
console.log("\u{1F504} Preparing for hot-swap (stopping MediaRecorder first)");
|
|
2481
|
-
this.isHotSwapping = true;
|
|
2482
|
-
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
2483
|
-
await new Promise((resolve) => {
|
|
2484
|
-
const currentRecorder = this.mediaRecorder;
|
|
2485
|
-
const handleStop = () => {
|
|
2486
|
-
currentRecorder.removeEventListener("stop", handleStop);
|
|
2487
|
-
console.log("\u23F9\uFE0F MediaRecorder stopped - ready for camera switch");
|
|
2488
|
-
resolve();
|
|
2489
|
-
};
|
|
2490
|
-
currentRecorder.addEventListener("stop", handleStop);
|
|
2491
|
-
currentRecorder.stop();
|
|
2656
|
+
console.log("\u2796 Old audio track(s) removed from source stream");
|
|
2657
|
+
if (this.canvasState) {
|
|
2658
|
+
this.canvasState.stream.getAudioTracks().forEach((track) => {
|
|
2659
|
+
this.canvasState.stream.removeTrack(track);
|
|
2492
2660
|
});
|
|
2661
|
+
this.canvasState.stream.addTrack(newAudioTrack);
|
|
2662
|
+
console.log("\u{1F3A8} Audio track synced to canvas stream");
|
|
2493
2663
|
}
|
|
2664
|
+
console.log("\u2705 Audio track replaced - streaming continues seamlessly");
|
|
2494
2665
|
}
|
|
2495
2666
|
/**
|
|
2496
|
-
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
* @deprecated Prefer replaceVideoTrack() which doesn't restart MediaRecorder
|
|
2500
|
-
*/
|
|
2501
|
-
cancelHotSwap() {
|
|
2502
|
-
console.log("\u21A9\uFE0F Canceling hot-swap - restarting with original stream");
|
|
2503
|
-
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2504
|
-
this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
|
|
2505
|
-
this.setupMediaRecorderHandlers();
|
|
2506
|
-
this.mediaRecorder.start(300);
|
|
2507
|
-
this.isHotSwapping = false;
|
|
2508
|
-
console.log("\u2705 Original stream restored");
|
|
2509
|
-
}
|
|
2510
|
-
/**
|
|
2511
|
-
* Complete the hot-swap with a new media stream
|
|
2512
|
-
* Call this AFTER successfully obtaining a new camera stream
|
|
2513
|
-
*
|
|
2514
|
-
* Note: Errors are thrown to the caller, not sent to onError callback
|
|
2515
|
-
* This allows the caller to handle camera flip failures gracefully
|
|
2516
|
-
*
|
|
2517
|
-
* @deprecated Prefer replaceVideoTrack() which doesn't restart MediaRecorder
|
|
2518
|
-
*/
|
|
2519
|
-
completeHotSwap(newMediaStream) {
|
|
2520
|
-
console.log("\u{1F504} Completing hot-swap with new stream");
|
|
2521
|
-
this.mediaStream = newMediaStream;
|
|
2522
|
-
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2523
|
-
this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
|
|
2524
|
-
console.log("\u{1F399}\uFE0F New MediaRecorder created");
|
|
2525
|
-
this.setupMediaRecorderHandlers();
|
|
2526
|
-
this.mediaRecorder.start(300);
|
|
2527
|
-
this.isHotSwapping = false;
|
|
2528
|
-
console.log("\u2705 Media stream updated - streaming continues");
|
|
2529
|
-
}
|
|
2530
|
-
/**
|
|
2531
|
-
* Update the media stream (e.g., when flipping camera)
|
|
2532
|
-
* This keeps the WebSocket connection alive while swapping the media source
|
|
2533
|
-
*
|
|
2534
|
-
* Note: On iOS, prefer using prepareForHotSwap() + completeHotSwap() to avoid
|
|
2535
|
-
* issues where getUserMedia interferes with the active MediaRecorder.
|
|
2536
|
-
*
|
|
2537
|
-
* Note: Errors are thrown to the caller, not sent to onError callback
|
|
2538
|
-
* This allows the caller to handle camera flip failures gracefully
|
|
2667
|
+
* Update the media stream (e.g., when switching devices from settings)
|
|
2668
|
+
* This keeps the WebSocket connection alive while swapping the media source.
|
|
2669
|
+
* Restarts the MediaRecorder with the new stream.
|
|
2539
2670
|
*
|
|
2540
|
-
*
|
|
2671
|
+
* Note: For camera flips, prefer replaceVideoTrack() which doesn't restart MediaRecorder.
|
|
2672
|
+
* Note: Errors are thrown to the caller, not sent to onError callback.
|
|
2541
2673
|
*/
|
|
2542
2674
|
async updateMediaStream(newMediaStream) {
|
|
2543
2675
|
console.log("\u{1F504} Updating media stream (hot-swap)");
|
|
@@ -2547,8 +2679,14 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2547
2679
|
console.log("\u23F9\uFE0F Old MediaRecorder stopped");
|
|
2548
2680
|
}
|
|
2549
2681
|
this.mediaStream = newMediaStream;
|
|
2682
|
+
let streamToRecord = this.mediaStream;
|
|
2683
|
+
if (this.isVideo) {
|
|
2684
|
+
this.cleanupCanvasRendering();
|
|
2685
|
+
streamToRecord = this.setupCanvasRendering();
|
|
2686
|
+
console.log("\u{1F3A8} Canvas rendering recreated for new stream");
|
|
2687
|
+
}
|
|
2550
2688
|
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2551
|
-
this.mediaRecorder = new MediaRecorder(
|
|
2689
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
2552
2690
|
console.log("\u{1F399}\uFE0F New MediaRecorder created");
|
|
2553
2691
|
this.setupMediaRecorderHandlers();
|
|
2554
2692
|
this.mediaRecorder.start(300);
|
|
@@ -2601,7 +2739,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2601
2739
|
this.bytesSent += event.data.size;
|
|
2602
2740
|
this.chunksSent += 1;
|
|
2603
2741
|
this.onBytesUpdate?.(this.bytesSent);
|
|
2604
|
-
|
|
2742
|
+
if (this.chunksSent % 10 === 0) {
|
|
2743
|
+
console.log(`\u{1F4E4} Sent ${this.chunksSent} chunks (${(this.bytesSent / 1024 / 1024).toFixed(2)} MB total)`);
|
|
2744
|
+
}
|
|
2605
2745
|
}
|
|
2606
2746
|
});
|
|
2607
2747
|
this.mediaRecorder.addEventListener("error", (event) => {
|
|
@@ -3508,7 +3648,7 @@ function DialtribeStreamer({
|
|
|
3508
3648
|
if (!isVideoKey || !hasMultipleCameras) return;
|
|
3509
3649
|
const newFacingMode = facingMode === "user" ? "environment" : "user";
|
|
3510
3650
|
if (state === "live" && streamer) {
|
|
3511
|
-
console.log("\u{1F504} Flipping camera during live broadcast (
|
|
3651
|
+
console.log("\u{1F504} Flipping camera during live broadcast (canvas-based swap)");
|
|
3512
3652
|
try {
|
|
3513
3653
|
const constraints = getMediaConstraints({
|
|
3514
3654
|
isVideo: true,
|
|
@@ -3520,9 +3660,11 @@ function DialtribeStreamer({
|
|
|
3520
3660
|
if (newVideoTrack) {
|
|
3521
3661
|
streamer.replaceVideoTrack(newVideoTrack);
|
|
3522
3662
|
}
|
|
3663
|
+
const updatedStream = streamer.getMediaStream();
|
|
3664
|
+
setMediaStream(updatedStream);
|
|
3523
3665
|
setFacingMode(newFacingMode);
|
|
3524
|
-
if (videoRef.current
|
|
3525
|
-
videoRef.current.srcObject =
|
|
3666
|
+
if (videoRef.current) {
|
|
3667
|
+
videoRef.current.srcObject = updatedStream;
|
|
3526
3668
|
}
|
|
3527
3669
|
console.log("\u2705 Camera flipped successfully - broadcast continues seamlessly");
|
|
3528
3670
|
} catch (err) {
|