@dialtribe/react-sdk 0.1.0-alpha.19 → 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-jxyxtG7Z.d.ts → dialtribe-streamer-D9ulVBVb.d.ts} +39 -25
- package/dist/{dialtribe-streamer-Bb6LLFG2.d.mts → dialtribe-streamer-DH23BseY.d.mts} +39 -25
- package/dist/dialtribe-streamer.d.mts +1 -1
- package/dist/dialtribe-streamer.d.ts +1 -1
- package/dist/dialtribe-streamer.js +264 -87
- package/dist/dialtribe-streamer.js.map +1 -1
- package/dist/dialtribe-streamer.mjs +264 -87
- 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 +264 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +264 -87
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2266,6 +2266,9 @@ var WebSocketStreamer = class {
|
|
|
2266
2266
|
this.isHotSwapping = false;
|
|
2267
2267
|
// Track if we're swapping media streams
|
|
2268
2268
|
this.startTime = 0;
|
|
2269
|
+
// Canvas-based rendering for seamless camera flips
|
|
2270
|
+
// MediaRecorder records from canvas stream, so track changes don't affect it
|
|
2271
|
+
this.canvasState = null;
|
|
2269
2272
|
this.streamKey = options.streamKey;
|
|
2270
2273
|
this.mediaStream = options.mediaStream;
|
|
2271
2274
|
this.isVideo = options.isVideo;
|
|
@@ -2274,6 +2277,36 @@ var WebSocketStreamer = class {
|
|
|
2274
2277
|
this.onStateChange = options.onStateChange;
|
|
2275
2278
|
this.onError = options.onError;
|
|
2276
2279
|
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Calculate scaled dimensions for fitting video into canvas.
|
|
2282
|
+
* @param mode - "contain" fits video inside canvas, "cover" fills canvas (cropping)
|
|
2283
|
+
*/
|
|
2284
|
+
calculateScaledDimensions(videoWidth, videoHeight, canvasWidth, canvasHeight, mode) {
|
|
2285
|
+
const videoAspect = videoWidth / videoHeight;
|
|
2286
|
+
const canvasAspect = canvasWidth / canvasHeight;
|
|
2287
|
+
const useWidthBased = mode === "contain" ? videoAspect > canvasAspect : videoAspect <= canvasAspect;
|
|
2288
|
+
if (useWidthBased) {
|
|
2289
|
+
const width = canvasWidth;
|
|
2290
|
+
const height = canvasWidth / videoAspect;
|
|
2291
|
+
return { x: 0, y: (canvasHeight - height) / 2, width, height };
|
|
2292
|
+
} else {
|
|
2293
|
+
const height = canvasHeight;
|
|
2294
|
+
const width = canvasHeight * videoAspect;
|
|
2295
|
+
return { x: (canvasWidth - width) / 2, y: 0, width, height };
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Invalidate cached scaling dimensions (call when video source changes)
|
|
2300
|
+
*/
|
|
2301
|
+
invalidateScalingCache() {
|
|
2302
|
+
if (this.canvasState) {
|
|
2303
|
+
this.canvasState.cachedContain = null;
|
|
2304
|
+
this.canvasState.cachedCover = null;
|
|
2305
|
+
this.canvasState.cachedNeedsBackground = false;
|
|
2306
|
+
this.canvasState.lastVideoWidth = 0;
|
|
2307
|
+
this.canvasState.lastVideoHeight = 0;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2277
2310
|
/**
|
|
2278
2311
|
* Validate stream key format
|
|
2279
2312
|
* Stream keys must follow format: {tierCode}{foreignId}_{randomKey}
|
|
@@ -2300,6 +2333,130 @@ var WebSocketStreamer = class {
|
|
|
2300
2333
|
isVIP: tierCode === "b" || tierCode === "w"
|
|
2301
2334
|
});
|
|
2302
2335
|
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Set up canvas-based rendering pipeline for video streams.
|
|
2338
|
+
* This allows seamless camera flips by changing the video source
|
|
2339
|
+
* without affecting MediaRecorder (which records from the canvas).
|
|
2340
|
+
*/
|
|
2341
|
+
setupCanvasRendering() {
|
|
2342
|
+
console.log("\u{1F3A8} Setting up canvas-based rendering for seamless camera flips");
|
|
2343
|
+
const videoTrack = this.mediaStream.getVideoTracks()[0];
|
|
2344
|
+
const settings = videoTrack?.getSettings() || {};
|
|
2345
|
+
const width = settings.width || 1280;
|
|
2346
|
+
const height = settings.height || 720;
|
|
2347
|
+
console.log(`\u{1F4D0} Video dimensions: ${width}x${height}`);
|
|
2348
|
+
const canvas = document.createElement("canvas");
|
|
2349
|
+
canvas.width = width;
|
|
2350
|
+
canvas.height = height;
|
|
2351
|
+
const ctx = canvas.getContext("2d");
|
|
2352
|
+
if (!ctx) {
|
|
2353
|
+
throw new Error("Failed to get 2D canvas context - canvas rendering unavailable");
|
|
2354
|
+
}
|
|
2355
|
+
const videoElement = document.createElement("video");
|
|
2356
|
+
videoElement.srcObject = this.mediaStream;
|
|
2357
|
+
videoElement.muted = true;
|
|
2358
|
+
videoElement.playsInline = true;
|
|
2359
|
+
videoElement.play().catch((e) => console.warn("Video autoplay warning:", e));
|
|
2360
|
+
const frameRate = settings.frameRate || 30;
|
|
2361
|
+
const stream = canvas.captureStream(frameRate);
|
|
2362
|
+
const audioTracks = this.mediaStream.getAudioTracks();
|
|
2363
|
+
audioTracks.forEach((track) => {
|
|
2364
|
+
stream.addTrack(track);
|
|
2365
|
+
});
|
|
2366
|
+
console.log(`\u{1F3AC} Canvas stream created with ${frameRate}fps video + ${audioTracks.length} audio track(s)`);
|
|
2367
|
+
this.canvasState = {
|
|
2368
|
+
canvas,
|
|
2369
|
+
ctx,
|
|
2370
|
+
videoElement,
|
|
2371
|
+
stream,
|
|
2372
|
+
renderLoopId: 0,
|
|
2373
|
+
// Will be set below
|
|
2374
|
+
useBlurBackground: true,
|
|
2375
|
+
slowFrameCount: 0,
|
|
2376
|
+
cachedContain: null,
|
|
2377
|
+
cachedCover: null,
|
|
2378
|
+
cachedNeedsBackground: false,
|
|
2379
|
+
lastVideoWidth: 0,
|
|
2380
|
+
lastVideoHeight: 0
|
|
2381
|
+
};
|
|
2382
|
+
const state = this.canvasState;
|
|
2383
|
+
const renderFrame = () => {
|
|
2384
|
+
if (!this.canvasState || state !== this.canvasState) return;
|
|
2385
|
+
const { ctx: ctx2, canvas: canvas2, videoElement: videoElement2 } = state;
|
|
2386
|
+
if (videoElement2.paused) {
|
|
2387
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
const canvasWidth = canvas2.width;
|
|
2391
|
+
const canvasHeight = canvas2.height;
|
|
2392
|
+
const videoWidth = videoElement2.videoWidth;
|
|
2393
|
+
const videoHeight = videoElement2.videoHeight;
|
|
2394
|
+
if (videoWidth === 0 || videoHeight === 0) {
|
|
2395
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (videoWidth !== state.lastVideoWidth || videoHeight !== state.lastVideoHeight) {
|
|
2399
|
+
state.lastVideoWidth = videoWidth;
|
|
2400
|
+
state.lastVideoHeight = videoHeight;
|
|
2401
|
+
state.cachedContain = this.calculateScaledDimensions(
|
|
2402
|
+
videoWidth,
|
|
2403
|
+
videoHeight,
|
|
2404
|
+
canvasWidth,
|
|
2405
|
+
canvasHeight,
|
|
2406
|
+
"contain"
|
|
2407
|
+
);
|
|
2408
|
+
state.cachedCover = this.calculateScaledDimensions(
|
|
2409
|
+
videoWidth,
|
|
2410
|
+
videoHeight,
|
|
2411
|
+
canvasWidth,
|
|
2412
|
+
canvasHeight,
|
|
2413
|
+
"cover"
|
|
2414
|
+
);
|
|
2415
|
+
state.cachedNeedsBackground = Math.abs(state.cachedContain.width - canvasWidth) > 1 || Math.abs(state.cachedContain.height - canvasHeight) > 1;
|
|
2416
|
+
console.log(`\u{1F4D0} Video dimensions changed: ${videoWidth}x${videoHeight}, needsBackground: ${state.cachedNeedsBackground}`);
|
|
2417
|
+
}
|
|
2418
|
+
const contain = state.cachedContain;
|
|
2419
|
+
const cover = state.cachedCover;
|
|
2420
|
+
const frameStart = performance.now();
|
|
2421
|
+
if (state.cachedNeedsBackground && state.useBlurBackground) {
|
|
2422
|
+
ctx2.save();
|
|
2423
|
+
ctx2.filter = "blur(20px)";
|
|
2424
|
+
ctx2.drawImage(videoElement2, cover.x, cover.y, cover.width, cover.height);
|
|
2425
|
+
ctx2.restore();
|
|
2426
|
+
ctx2.fillStyle = "rgba(0, 0, 0, 0.5)";
|
|
2427
|
+
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
2428
|
+
} else if (state.cachedNeedsBackground) {
|
|
2429
|
+
ctx2.fillStyle = "#000";
|
|
2430
|
+
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
2431
|
+
}
|
|
2432
|
+
ctx2.drawImage(videoElement2, contain.x, contain.y, contain.width, contain.height);
|
|
2433
|
+
const frameDuration = performance.now() - frameStart;
|
|
2434
|
+
if (frameDuration > 16 && state.useBlurBackground) {
|
|
2435
|
+
state.slowFrameCount++;
|
|
2436
|
+
if (state.slowFrameCount > 5) {
|
|
2437
|
+
console.log("\u26A1 Disabling blur background for performance");
|
|
2438
|
+
state.useBlurBackground = false;
|
|
2439
|
+
}
|
|
2440
|
+
} else if (frameDuration <= 16) {
|
|
2441
|
+
state.slowFrameCount = 0;
|
|
2442
|
+
}
|
|
2443
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2444
|
+
};
|
|
2445
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
2446
|
+
console.log("\u2705 Canvas rendering pipeline ready (with adaptive blur background)");
|
|
2447
|
+
return stream;
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Clean up canvas rendering resources
|
|
2451
|
+
*/
|
|
2452
|
+
cleanupCanvasRendering() {
|
|
2453
|
+
if (!this.canvasState) return;
|
|
2454
|
+
cancelAnimationFrame(this.canvasState.renderLoopId);
|
|
2455
|
+
this.canvasState.videoElement.pause();
|
|
2456
|
+
this.canvasState.videoElement.srcObject = null;
|
|
2457
|
+
this.canvasState.stream.getTracks().forEach((track) => track.stop());
|
|
2458
|
+
this.canvasState = null;
|
|
2459
|
+
}
|
|
2303
2460
|
/**
|
|
2304
2461
|
* Build WebSocket URL from stream key
|
|
2305
2462
|
*/
|
|
@@ -2319,6 +2476,10 @@ var WebSocketStreamer = class {
|
|
|
2319
2476
|
*/
|
|
2320
2477
|
async start() {
|
|
2321
2478
|
try {
|
|
2479
|
+
this.userStopped = false;
|
|
2480
|
+
this.chunksSent = 0;
|
|
2481
|
+
this.bytesSent = 0;
|
|
2482
|
+
this.startTime = 0;
|
|
2322
2483
|
this.validateStreamKeyFormat();
|
|
2323
2484
|
this.onStateChange?.("connecting");
|
|
2324
2485
|
const wsUrl = this.buildWebSocketUrl();
|
|
@@ -2329,8 +2490,15 @@ var WebSocketStreamer = class {
|
|
|
2329
2490
|
reject(new Error("WebSocket not initialized"));
|
|
2330
2491
|
return;
|
|
2331
2492
|
}
|
|
2332
|
-
|
|
2493
|
+
const timeoutId = setTimeout(() => {
|
|
2494
|
+
reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
|
|
2495
|
+
}, 1e4);
|
|
2496
|
+
this.websocket.addEventListener("open", () => {
|
|
2497
|
+
clearTimeout(timeoutId);
|
|
2498
|
+
resolve();
|
|
2499
|
+
}, { once: true });
|
|
2333
2500
|
this.websocket.addEventListener("error", (event) => {
|
|
2501
|
+
clearTimeout(timeoutId);
|
|
2334
2502
|
console.error("\u274C WebSocket error event:", event);
|
|
2335
2503
|
console.error("\u{1F50D} Connection diagnostics:", {
|
|
2336
2504
|
url: wsUrl.replace(this.streamKey, "***"),
|
|
@@ -2349,16 +2517,17 @@ Common causes:
|
|
|
2349
2517
|
Please check encoder server logs and DATABASE_URL configuration.`
|
|
2350
2518
|
));
|
|
2351
2519
|
}, { once: true });
|
|
2352
|
-
setTimeout(() => {
|
|
2353
|
-
reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
|
|
2354
|
-
}, 1e4);
|
|
2355
2520
|
});
|
|
2356
2521
|
console.log("\u2705 WebSocket connected");
|
|
2357
2522
|
this.setupWebSocketHandlers();
|
|
2523
|
+
const streamToRecord = this.isVideo ? this.setupCanvasRendering() : this.mediaStream;
|
|
2358
2524
|
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2359
2525
|
this.mimeType = recorderOptions.mimeType;
|
|
2360
|
-
this.mediaRecorder = new MediaRecorder(
|
|
2526
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
2361
2527
|
console.log("\u{1F399}\uFE0F MediaRecorder created with options:", recorderOptions);
|
|
2528
|
+
if (this.isVideo) {
|
|
2529
|
+
console.log("\u{1F3A8} Recording from canvas stream (enables seamless camera flips)");
|
|
2530
|
+
}
|
|
2362
2531
|
this.setupMediaRecorderHandlers();
|
|
2363
2532
|
this.mediaRecorder.start(300);
|
|
2364
2533
|
this.startTime = Date.now();
|
|
@@ -2395,9 +2564,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2395
2564
|
} else {
|
|
2396
2565
|
console.log("\u26A0\uFE0F No WebSocket to close");
|
|
2397
2566
|
}
|
|
2567
|
+
this.cleanupCanvasRendering();
|
|
2398
2568
|
this.mediaRecorder = null;
|
|
2399
2569
|
this.websocket = null;
|
|
2400
|
-
this.bytesSent = 0;
|
|
2401
2570
|
this.onStateChange?.("stopped");
|
|
2402
2571
|
}
|
|
2403
2572
|
/**
|
|
@@ -2406,6 +2575,13 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2406
2575
|
getBytesSent() {
|
|
2407
2576
|
return this.bytesSent;
|
|
2408
2577
|
}
|
|
2578
|
+
/**
|
|
2579
|
+
* Get the current source media stream.
|
|
2580
|
+
* This may change after replaceVideoTrack() is called.
|
|
2581
|
+
*/
|
|
2582
|
+
getMediaStream() {
|
|
2583
|
+
return this.mediaStream;
|
|
2584
|
+
}
|
|
2409
2585
|
/**
|
|
2410
2586
|
* Get current diagnostics
|
|
2411
2587
|
*/
|
|
@@ -2420,71 +2596,73 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2420
2596
|
};
|
|
2421
2597
|
}
|
|
2422
2598
|
/**
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
2599
|
+
* Replace the video track for camera flips.
|
|
2600
|
+
*
|
|
2601
|
+
* When using canvas-based rendering (video streams), this updates the video
|
|
2602
|
+
* element source. The canvas continues drawing, and MediaRecorder is unaffected.
|
|
2426
2603
|
*
|
|
2427
|
-
*
|
|
2428
|
-
* event to fire BEFORE proceeding. Otherwise there's a race condition where
|
|
2429
|
-
* the stop event fires after isHotSwapping is set to false, causing the
|
|
2430
|
-
* WebSocket to close unexpectedly.
|
|
2604
|
+
* @param newVideoTrack - The new video track from the flipped camera
|
|
2431
2605
|
*/
|
|
2432
|
-
|
|
2433
|
-
console.log("\u{1F504}
|
|
2434
|
-
this.
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2606
|
+
replaceVideoTrack(newVideoTrack) {
|
|
2607
|
+
console.log("\u{1F504} Replacing video track");
|
|
2608
|
+
if (this.canvasState) {
|
|
2609
|
+
console.log("\u{1F3A8} Using canvas-based swap (MediaRecorder unaffected)");
|
|
2610
|
+
const audioTracks = this.mediaStream.getAudioTracks();
|
|
2611
|
+
const newStream = new MediaStream([newVideoTrack, ...audioTracks]);
|
|
2612
|
+
this.mediaStream.getVideoTracks().forEach((track) => track.stop());
|
|
2613
|
+
this.canvasState.videoElement.srcObject = newStream;
|
|
2614
|
+
this.canvasState.videoElement.play().catch((e) => console.warn("Video play warning:", e));
|
|
2615
|
+
this.mediaStream = newStream;
|
|
2616
|
+
this.invalidateScalingCache();
|
|
2617
|
+
const settings = newVideoTrack.getSettings();
|
|
2618
|
+
if (settings.width && settings.height) {
|
|
2619
|
+
console.log(`\u{1F4D0} New camera resolution: ${settings.width}x${settings.height}`);
|
|
2620
|
+
}
|
|
2621
|
+
console.log("\u2705 Video source swapped - canvas continues seamlessly");
|
|
2622
|
+
} else {
|
|
2623
|
+
console.warn("\u26A0\uFE0F Canvas not available - attempting direct track replacement");
|
|
2624
|
+
const oldVideoTracks = this.mediaStream.getVideoTracks();
|
|
2625
|
+
this.mediaStream.addTrack(newVideoTrack);
|
|
2626
|
+
console.log("\u2795 New video track added");
|
|
2627
|
+
oldVideoTracks.forEach((track) => {
|
|
2628
|
+
this.mediaStream.removeTrack(track);
|
|
2629
|
+
track.stop();
|
|
2445
2630
|
});
|
|
2631
|
+
console.log("\u2796 Old video track(s) removed");
|
|
2632
|
+
console.log("\u2705 Video track replaced");
|
|
2446
2633
|
}
|
|
2447
2634
|
}
|
|
2448
2635
|
/**
|
|
2449
|
-
*
|
|
2450
|
-
* Restarts the MediaRecorder with the existing stream
|
|
2451
|
-
*/
|
|
2452
|
-
cancelHotSwap() {
|
|
2453
|
-
console.log("\u21A9\uFE0F Canceling hot-swap - restarting with original stream");
|
|
2454
|
-
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2455
|
-
this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
|
|
2456
|
-
this.setupMediaRecorderHandlers();
|
|
2457
|
-
this.mediaRecorder.start(300);
|
|
2458
|
-
this.isHotSwapping = false;
|
|
2459
|
-
console.log("\u2705 Original stream restored");
|
|
2460
|
-
}
|
|
2461
|
-
/**
|
|
2462
|
-
* Complete the hot-swap with a new media stream
|
|
2463
|
-
* Call this AFTER successfully obtaining a new camera stream
|
|
2636
|
+
* Replace the audio track in the current MediaStream without stopping MediaRecorder.
|
|
2464
2637
|
*
|
|
2465
|
-
*
|
|
2466
|
-
* This allows the caller to handle camera flip failures gracefully
|
|
2638
|
+
* @param newAudioTrack - The new audio track
|
|
2467
2639
|
*/
|
|
2468
|
-
|
|
2469
|
-
console.log("\u{1F504}
|
|
2470
|
-
this.mediaStream
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
console.log("\
|
|
2640
|
+
replaceAudioTrack(newAudioTrack) {
|
|
2641
|
+
console.log("\u{1F504} Replacing audio track (no MediaRecorder restart)");
|
|
2642
|
+
const oldAudioTracks = this.mediaStream.getAudioTracks();
|
|
2643
|
+
this.mediaStream.addTrack(newAudioTrack);
|
|
2644
|
+
console.log("\u2795 New audio track added to source stream");
|
|
2645
|
+
oldAudioTracks.forEach((track) => {
|
|
2646
|
+
this.mediaStream.removeTrack(track);
|
|
2647
|
+
track.stop();
|
|
2648
|
+
});
|
|
2649
|
+
console.log("\u2796 Old audio track(s) removed from source stream");
|
|
2650
|
+
if (this.canvasState) {
|
|
2651
|
+
this.canvasState.stream.getAudioTracks().forEach((track) => {
|
|
2652
|
+
this.canvasState.stream.removeTrack(track);
|
|
2653
|
+
});
|
|
2654
|
+
this.canvasState.stream.addTrack(newAudioTrack);
|
|
2655
|
+
console.log("\u{1F3A8} Audio track synced to canvas stream");
|
|
2656
|
+
}
|
|
2657
|
+
console.log("\u2705 Audio track replaced - streaming continues seamlessly");
|
|
2478
2658
|
}
|
|
2479
2659
|
/**
|
|
2480
|
-
* Update the media stream (e.g., when
|
|
2481
|
-
* This keeps the WebSocket connection alive while swapping the media source
|
|
2660
|
+
* Update the media stream (e.g., when switching devices from settings)
|
|
2661
|
+
* This keeps the WebSocket connection alive while swapping the media source.
|
|
2662
|
+
* Restarts the MediaRecorder with the new stream.
|
|
2482
2663
|
*
|
|
2483
|
-
* Note:
|
|
2484
|
-
*
|
|
2485
|
-
*
|
|
2486
|
-
* Note: Errors are thrown to the caller, not sent to onError callback
|
|
2487
|
-
* This allows the caller to handle camera flip failures gracefully
|
|
2664
|
+
* Note: For camera flips, prefer replaceVideoTrack() which doesn't restart MediaRecorder.
|
|
2665
|
+
* Note: Errors are thrown to the caller, not sent to onError callback.
|
|
2488
2666
|
*/
|
|
2489
2667
|
async updateMediaStream(newMediaStream) {
|
|
2490
2668
|
console.log("\u{1F504} Updating media stream (hot-swap)");
|
|
@@ -2494,8 +2672,14 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2494
2672
|
console.log("\u23F9\uFE0F Old MediaRecorder stopped");
|
|
2495
2673
|
}
|
|
2496
2674
|
this.mediaStream = newMediaStream;
|
|
2675
|
+
let streamToRecord = this.mediaStream;
|
|
2676
|
+
if (this.isVideo) {
|
|
2677
|
+
this.cleanupCanvasRendering();
|
|
2678
|
+
streamToRecord = this.setupCanvasRendering();
|
|
2679
|
+
console.log("\u{1F3A8} Canvas rendering recreated for new stream");
|
|
2680
|
+
}
|
|
2497
2681
|
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
2498
|
-
this.mediaRecorder = new MediaRecorder(
|
|
2682
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
2499
2683
|
console.log("\u{1F399}\uFE0F New MediaRecorder created");
|
|
2500
2684
|
this.setupMediaRecorderHandlers();
|
|
2501
2685
|
this.mediaRecorder.start(300);
|
|
@@ -2548,7 +2732,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
2548
2732
|
this.bytesSent += event.data.size;
|
|
2549
2733
|
this.chunksSent += 1;
|
|
2550
2734
|
this.onBytesUpdate?.(this.bytesSent);
|
|
2551
|
-
|
|
2735
|
+
if (this.chunksSent % 10 === 0) {
|
|
2736
|
+
console.log(`\u{1F4E4} Sent ${this.chunksSent} chunks (${(this.bytesSent / 1024 / 1024).toFixed(2)} MB total)`);
|
|
2737
|
+
}
|
|
2552
2738
|
}
|
|
2553
2739
|
});
|
|
2554
2740
|
this.mediaRecorder.addEventListener("error", (event) => {
|
|
@@ -3454,41 +3640,32 @@ function DialtribeStreamer({
|
|
|
3454
3640
|
const handleFlipCamera = async () => {
|
|
3455
3641
|
if (!isVideoKey || !hasMultipleCameras) return;
|
|
3456
3642
|
const newFacingMode = facingMode === "user" ? "environment" : "user";
|
|
3457
|
-
setFacingMode(newFacingMode);
|
|
3458
3643
|
if (state === "live" && streamer) {
|
|
3459
|
-
console.log("\u{1F504}
|
|
3460
|
-
await streamer.prepareForHotSwap();
|
|
3461
|
-
if (mediaStream) {
|
|
3462
|
-
mediaStream.getTracks().forEach((track) => track.stop());
|
|
3463
|
-
}
|
|
3644
|
+
console.log("\u{1F504} Flipping camera during live broadcast (canvas-based swap)");
|
|
3464
3645
|
try {
|
|
3465
3646
|
const constraints = getMediaConstraints({
|
|
3466
3647
|
isVideo: true,
|
|
3467
3648
|
facingMode: newFacingMode
|
|
3468
3649
|
});
|
|
3469
3650
|
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
3470
|
-
console.log("\u{1F4F7}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
} catch (err) {
|
|
3475
|
-
console.error("\u274C Failed to get new camera stream:", err);
|
|
3476
|
-
setFacingMode(facingMode);
|
|
3477
|
-
try {
|
|
3478
|
-
const originalConstraints = getMediaConstraints({
|
|
3479
|
-
isVideo: true,
|
|
3480
|
-
facingMode
|
|
3481
|
-
});
|
|
3482
|
-
const restoredStream = await navigator.mediaDevices.getUserMedia(originalConstraints);
|
|
3483
|
-
streamer.completeHotSwap(restoredStream);
|
|
3484
|
-
setMediaStream(restoredStream);
|
|
3485
|
-
console.warn("\u26A0\uFE0F Camera flip failed - restored original camera");
|
|
3486
|
-
} catch (restoreErr) {
|
|
3487
|
-
console.error("\u274C Failed to restore original camera:", restoreErr);
|
|
3488
|
-
streamer.cancelHotSwap();
|
|
3651
|
+
console.log("\u{1F4F7} Got new camera stream:", newFacingMode);
|
|
3652
|
+
const newVideoTrack = newStream.getVideoTracks()[0];
|
|
3653
|
+
if (newVideoTrack) {
|
|
3654
|
+
streamer.replaceVideoTrack(newVideoTrack);
|
|
3489
3655
|
}
|
|
3656
|
+
const updatedStream = streamer.getMediaStream();
|
|
3657
|
+
setMediaStream(updatedStream);
|
|
3658
|
+
setFacingMode(newFacingMode);
|
|
3659
|
+
if (videoRef.current) {
|
|
3660
|
+
videoRef.current.srcObject = updatedStream;
|
|
3661
|
+
}
|
|
3662
|
+
console.log("\u2705 Camera flipped successfully - broadcast continues seamlessly");
|
|
3663
|
+
} catch (err) {
|
|
3664
|
+
console.error("\u274C Failed to flip camera:", err);
|
|
3665
|
+
console.warn("\u26A0\uFE0F Camera flip failed - continuing with current camera");
|
|
3490
3666
|
}
|
|
3491
3667
|
} else {
|
|
3668
|
+
setFacingMode(newFacingMode);
|
|
3492
3669
|
try {
|
|
3493
3670
|
const constraints = getMediaConstraints({
|
|
3494
3671
|
isVideo: true,
|