@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/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-Bb6LLFG2.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-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-jxyxtG7Z.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-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
- this.websocket.addEventListener("open", () => resolve(), { once: true });
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(this.mediaStream, recorderOptions);
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,71 +2603,73 @@ Please check encoder server logs and DATABASE_URL configuration.`
2427
2603
  };
2428
2604
  }
2429
2605
  /**
2430
- * Prepare for a hot-swap by stopping the MediaRecorder
2431
- * Call this BEFORE requesting a new camera stream on iOS
2432
- * This prevents iOS Safari from interfering with the active MediaRecorder
2606
+ * Replace the video track for camera flips.
2607
+ *
2608
+ * When using canvas-based rendering (video streams), this updates the video
2609
+ * element source. The canvas continues drawing, and MediaRecorder is unaffected.
2433
2610
  *
2434
- * IMPORTANT: This is async because we must wait for the MediaRecorder's 'stop'
2435
- * event to fire BEFORE proceeding. Otherwise there's a race condition where
2436
- * the stop event fires after isHotSwapping is set to false, causing the
2437
- * WebSocket to close unexpectedly.
2611
+ * @param newVideoTrack - The new video track from the flipped camera
2438
2612
  */
2439
- async prepareForHotSwap() {
2440
- console.log("\u{1F504} Preparing for hot-swap (stopping MediaRecorder first)");
2441
- this.isHotSwapping = true;
2442
- if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
2443
- await new Promise((resolve) => {
2444
- const currentRecorder = this.mediaRecorder;
2445
- const handleStop = () => {
2446
- currentRecorder.removeEventListener("stop", handleStop);
2447
- console.log("\u23F9\uFE0F MediaRecorder stopped - ready for camera switch");
2448
- resolve();
2449
- };
2450
- currentRecorder.addEventListener("stop", handleStop);
2451
- currentRecorder.stop();
2613
+ replaceVideoTrack(newVideoTrack) {
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();
2452
2637
  });
2638
+ console.log("\u2796 Old video track(s) removed");
2639
+ console.log("\u2705 Video track replaced");
2453
2640
  }
2454
2641
  }
2455
2642
  /**
2456
- * Cancel a prepared hot-swap (e.g., if camera switch failed)
2457
- * Restarts the MediaRecorder with the existing stream
2458
- */
2459
- cancelHotSwap() {
2460
- console.log("\u21A9\uFE0F Canceling hot-swap - restarting with original stream");
2461
- const recorderOptions = getMediaRecorderOptions(this.isVideo);
2462
- this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
2463
- this.setupMediaRecorderHandlers();
2464
- this.mediaRecorder.start(300);
2465
- this.isHotSwapping = false;
2466
- console.log("\u2705 Original stream restored");
2467
- }
2468
- /**
2469
- * Complete the hot-swap with a new media stream
2470
- * Call this AFTER successfully obtaining a new camera stream
2643
+ * Replace the audio track in the current MediaStream without stopping MediaRecorder.
2471
2644
  *
2472
- * Note: Errors are thrown to the caller, not sent to onError callback
2473
- * This allows the caller to handle camera flip failures gracefully
2645
+ * @param newAudioTrack - The new audio track
2474
2646
  */
2475
- completeHotSwap(newMediaStream) {
2476
- console.log("\u{1F504} Completing hot-swap with new stream");
2477
- this.mediaStream = newMediaStream;
2478
- const recorderOptions = getMediaRecorderOptions(this.isVideo);
2479
- this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
2480
- console.log("\u{1F399}\uFE0F New MediaRecorder created");
2481
- this.setupMediaRecorderHandlers();
2482
- this.mediaRecorder.start(300);
2483
- this.isHotSwapping = false;
2484
- console.log("\u2705 Media stream updated - streaming continues");
2647
+ replaceAudioTrack(newAudioTrack) {
2648
+ console.log("\u{1F504} Replacing audio track (no MediaRecorder restart)");
2649
+ const oldAudioTracks = this.mediaStream.getAudioTracks();
2650
+ this.mediaStream.addTrack(newAudioTrack);
2651
+ console.log("\u2795 New audio track added to source stream");
2652
+ oldAudioTracks.forEach((track) => {
2653
+ this.mediaStream.removeTrack(track);
2654
+ track.stop();
2655
+ });
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);
2660
+ });
2661
+ this.canvasState.stream.addTrack(newAudioTrack);
2662
+ console.log("\u{1F3A8} Audio track synced to canvas stream");
2663
+ }
2664
+ console.log("\u2705 Audio track replaced - streaming continues seamlessly");
2485
2665
  }
2486
2666
  /**
2487
- * Update the media stream (e.g., when flipping camera)
2488
- * This keeps the WebSocket connection alive while swapping the media source
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.
2489
2670
  *
2490
- * Note: On iOS, prefer using prepareForHotSwap() + completeHotSwap() to avoid
2491
- * issues where getUserMedia interferes with the active MediaRecorder.
2492
- *
2493
- * Note: Errors are thrown to the caller, not sent to onError callback
2494
- * This allows the caller to handle camera flip failures gracefully
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.
2495
2673
  */
2496
2674
  async updateMediaStream(newMediaStream) {
2497
2675
  console.log("\u{1F504} Updating media stream (hot-swap)");
@@ -2501,8 +2679,14 @@ Please check encoder server logs and DATABASE_URL configuration.`
2501
2679
  console.log("\u23F9\uFE0F Old MediaRecorder stopped");
2502
2680
  }
2503
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
+ }
2504
2688
  const recorderOptions = getMediaRecorderOptions(this.isVideo);
2505
- this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
2689
+ this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
2506
2690
  console.log("\u{1F399}\uFE0F New MediaRecorder created");
2507
2691
  this.setupMediaRecorderHandlers();
2508
2692
  this.mediaRecorder.start(300);
@@ -2555,7 +2739,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
2555
2739
  this.bytesSent += event.data.size;
2556
2740
  this.chunksSent += 1;
2557
2741
  this.onBytesUpdate?.(this.bytesSent);
2558
- console.log(`\u{1F4E4} Sent chunk #${this.chunksSent}: ${(event.data.size / 1024).toFixed(2)} KB (Total: ${(this.bytesSent / 1024 / 1024).toFixed(2)} MB)`);
2742
+ if (this.chunksSent % 10 === 0) {
2743
+ console.log(`\u{1F4E4} Sent ${this.chunksSent} chunks (${(this.bytesSent / 1024 / 1024).toFixed(2)} MB total)`);
2744
+ }
2559
2745
  }
2560
2746
  });
2561
2747
  this.mediaRecorder.addEventListener("error", (event) => {
@@ -3461,41 +3647,32 @@ function DialtribeStreamer({
3461
3647
  const handleFlipCamera = async () => {
3462
3648
  if (!isVideoKey || !hasMultipleCameras) return;
3463
3649
  const newFacingMode = facingMode === "user" ? "environment" : "user";
3464
- setFacingMode(newFacingMode);
3465
3650
  if (state === "live" && streamer) {
3466
- console.log("\u{1F504} Hot-swapping camera during live broadcast");
3467
- await streamer.prepareForHotSwap();
3468
- if (mediaStream) {
3469
- mediaStream.getTracks().forEach((track) => track.stop());
3470
- }
3651
+ console.log("\u{1F504} Flipping camera during live broadcast (canvas-based swap)");
3471
3652
  try {
3472
3653
  const constraints = getMediaConstraints({
3473
3654
  isVideo: true,
3474
3655
  facingMode: newFacingMode
3475
3656
  });
3476
3657
  const newStream = await navigator.mediaDevices.getUserMedia(constraints);
3477
- console.log("\u{1F4F7} Camera flipped to:", newFacingMode);
3478
- streamer.completeHotSwap(newStream);
3479
- setMediaStream(newStream);
3480
- console.log("\u2705 Camera flipped successfully - broadcast continues");
3481
- } catch (err) {
3482
- console.error("\u274C Failed to get new camera stream:", err);
3483
- setFacingMode(facingMode);
3484
- try {
3485
- const originalConstraints = getMediaConstraints({
3486
- isVideo: true,
3487
- facingMode
3488
- });
3489
- const restoredStream = await navigator.mediaDevices.getUserMedia(originalConstraints);
3490
- streamer.completeHotSwap(restoredStream);
3491
- setMediaStream(restoredStream);
3492
- console.warn("\u26A0\uFE0F Camera flip failed - restored original camera");
3493
- } catch (restoreErr) {
3494
- console.error("\u274C Failed to restore original camera:", restoreErr);
3495
- streamer.cancelHotSwap();
3658
+ console.log("\u{1F4F7} Got new camera stream:", newFacingMode);
3659
+ const newVideoTrack = newStream.getVideoTracks()[0];
3660
+ if (newVideoTrack) {
3661
+ streamer.replaceVideoTrack(newVideoTrack);
3496
3662
  }
3663
+ const updatedStream = streamer.getMediaStream();
3664
+ setMediaStream(updatedStream);
3665
+ setFacingMode(newFacingMode);
3666
+ if (videoRef.current) {
3667
+ videoRef.current.srcObject = updatedStream;
3668
+ }
3669
+ console.log("\u2705 Camera flipped successfully - broadcast continues seamlessly");
3670
+ } catch (err) {
3671
+ console.error("\u274C Failed to flip camera:", err);
3672
+ console.warn("\u26A0\uFE0F Camera flip failed - continuing with current camera");
3497
3673
  }
3498
3674
  } else {
3675
+ setFacingMode(newFacingMode);
3499
3676
  try {
3500
3677
  const constraints = getMediaConstraints({
3501
3678
  isVideo: true,