@dialtribe/react-sdk 0.1.0-alpha.21 → 0.1.0-alpha.23

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.mjs CHANGED
@@ -2337,8 +2337,11 @@ var WebSocketStreamer = class {
2337
2337
  * Set up canvas-based rendering pipeline for video streams.
2338
2338
  * This allows seamless camera flips by changing the video source
2339
2339
  * without affecting MediaRecorder (which records from the canvas).
2340
+ *
2341
+ * This is async to ensure the video is producing frames before returning,
2342
+ * which prevents black initial thumbnails.
2340
2343
  */
2341
- setupCanvasRendering() {
2344
+ async setupCanvasRendering() {
2342
2345
  console.log("\u{1F3A8} Setting up canvas-based rendering for seamless camera flips");
2343
2346
  const videoTrack = this.mediaStream.getVideoTracks()[0];
2344
2347
  const settings = videoTrack?.getSettings() || {};
@@ -2356,7 +2359,27 @@ var WebSocketStreamer = class {
2356
2359
  videoElement.srcObject = this.mediaStream;
2357
2360
  videoElement.muted = true;
2358
2361
  videoElement.playsInline = true;
2359
- videoElement.play().catch((e) => console.warn("Video autoplay warning:", e));
2362
+ await new Promise((resolve) => {
2363
+ const checkReady = () => {
2364
+ if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
2365
+ console.log(`\u{1F4F9} Video ready: ${videoElement.videoWidth}x${videoElement.videoHeight}`);
2366
+ resolve();
2367
+ } else {
2368
+ requestAnimationFrame(checkReady);
2369
+ }
2370
+ };
2371
+ videoElement.addEventListener("loadeddata", () => {
2372
+ checkReady();
2373
+ }, { once: true });
2374
+ videoElement.play().catch((e) => {
2375
+ console.warn("Video autoplay warning:", e);
2376
+ resolve();
2377
+ });
2378
+ setTimeout(() => {
2379
+ console.warn("\u26A0\uFE0F Video ready timeout - continuing anyway");
2380
+ resolve();
2381
+ }, 2e3);
2382
+ });
2360
2383
  const frameRate = settings.frameRate || 30;
2361
2384
  const stream = canvas.captureStream(frameRate);
2362
2385
  const audioTracks = this.mediaStream.getAudioTracks();
@@ -2379,13 +2402,28 @@ var WebSocketStreamer = class {
2379
2402
  lastVideoWidth: 0,
2380
2403
  lastVideoHeight: 0
2381
2404
  };
2405
+ if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
2406
+ const vw = videoElement.videoWidth;
2407
+ const vh = videoElement.videoHeight;
2408
+ const cw = canvas.width;
2409
+ const ch = canvas.height;
2410
+ const scale = Math.min(cw / vw, ch / vh);
2411
+ const sw = vw * scale;
2412
+ const sh = vh * scale;
2413
+ const sx = (cw - sw) / 2;
2414
+ const sy = (ch - sh) / 2;
2415
+ ctx.fillStyle = "#000";
2416
+ ctx.fillRect(0, 0, cw, ch);
2417
+ ctx.drawImage(videoElement, sx, sy, sw, sh);
2418
+ console.log("\u{1F5BC}\uFE0F Drew first frame synchronously to prevent black thumbnail");
2419
+ }
2382
2420
  const state = this.canvasState;
2383
2421
  const renderFrame = () => {
2384
2422
  if (!this.canvasState || state !== this.canvasState) return;
2385
2423
  const { ctx: ctx2, canvas: canvas2, videoElement: videoElement2 } = state;
2386
2424
  if (videoElement2.paused) {
2387
- state.renderLoopId = requestAnimationFrame(renderFrame);
2388
- return;
2425
+ videoElement2.play().catch(() => {
2426
+ });
2389
2427
  }
2390
2428
  const canvasWidth = canvas2.width;
2391
2429
  const canvasHeight = canvas2.height;
@@ -2520,7 +2558,7 @@ Please check encoder server logs and DATABASE_URL configuration.`
2520
2558
  });
2521
2559
  console.log("\u2705 WebSocket connected");
2522
2560
  this.setupWebSocketHandlers();
2523
- const streamToRecord = this.isVideo ? this.setupCanvasRendering() : this.mediaStream;
2561
+ const streamToRecord = this.isVideo ? await this.setupCanvasRendering() : this.mediaStream;
2524
2562
  const recorderOptions = getMediaRecorderOptions(this.isVideo);
2525
2563
  this.mimeType = recorderOptions.mimeType;
2526
2564
  this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
@@ -2598,20 +2636,70 @@ Please check encoder server logs and DATABASE_URL configuration.`
2598
2636
  /**
2599
2637
  * Replace the video track for camera flips.
2600
2638
  *
2601
- * When using canvas-based rendering (video streams), this updates the video
2602
- * element source. The canvas continues drawing, and MediaRecorder is unaffected.
2639
+ * When using canvas-based rendering (video streams), this preloads the new
2640
+ * camera in a temporary video element, waits for it to be ready, then swaps
2641
+ * it in. This ensures continuous frame output with no gaps.
2603
2642
  *
2604
2643
  * @param newVideoTrack - The new video track from the flipped camera
2644
+ * @returns Promise that resolves when the swap is complete
2605
2645
  */
2606
- replaceVideoTrack(newVideoTrack) {
2646
+ async replaceVideoTrack(newVideoTrack) {
2607
2647
  console.log("\u{1F504} Replacing video track");
2608
2648
  if (this.canvasState) {
2609
- console.log("\u{1F3A8} Using canvas-based swap (MediaRecorder unaffected)");
2649
+ console.log("\u{1F3A8} Using canvas-based swap with preloading (no frame gaps)");
2610
2650
  const audioTracks = this.mediaStream.getAudioTracks();
2611
2651
  const newStream = new MediaStream([newVideoTrack, ...audioTracks]);
2652
+ const preloadVideo = document.createElement("video");
2653
+ preloadVideo.srcObject = newStream;
2654
+ preloadVideo.muted = true;
2655
+ preloadVideo.playsInline = true;
2656
+ await new Promise((resolve, reject) => {
2657
+ const timeout = setTimeout(() => {
2658
+ console.warn("\u26A0\uFE0F Video preload timeout - switching anyway");
2659
+ if (preloadVideo.paused) {
2660
+ preloadVideo.play().catch(() => {
2661
+ });
2662
+ }
2663
+ resolve();
2664
+ }, 3e3);
2665
+ const checkFullyReady = () => {
2666
+ if (preloadVideo.videoWidth > 0 && preloadVideo.videoHeight > 0 && !preloadVideo.paused) {
2667
+ clearTimeout(timeout);
2668
+ console.log(`\u{1F4F9} New camera ready and playing: ${preloadVideo.videoWidth}x${preloadVideo.videoHeight}`);
2669
+ resolve();
2670
+ return true;
2671
+ }
2672
+ return false;
2673
+ };
2674
+ preloadVideo.addEventListener("loadeddata", () => {
2675
+ preloadVideo.play().then(() => {
2676
+ requestAnimationFrame(() => {
2677
+ if (!checkFullyReady()) {
2678
+ const pollPlaying = setInterval(() => {
2679
+ if (checkFullyReady()) {
2680
+ clearInterval(pollPlaying);
2681
+ }
2682
+ }, 50);
2683
+ setTimeout(() => clearInterval(pollPlaying), 2e3);
2684
+ }
2685
+ });
2686
+ }).catch((e) => {
2687
+ console.warn("Video preload play warning:", e);
2688
+ checkFullyReady();
2689
+ });
2690
+ }, { once: true });
2691
+ preloadVideo.addEventListener("error", (e) => {
2692
+ clearTimeout(timeout);
2693
+ reject(new Error(`Video preload failed: ${e}`));
2694
+ }, { once: true });
2695
+ preloadVideo.play().catch(() => {
2696
+ });
2697
+ });
2698
+ const oldVideoElement = this.canvasState.videoElement;
2699
+ this.canvasState.videoElement = preloadVideo;
2612
2700
  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));
2701
+ oldVideoElement.pause();
2702
+ oldVideoElement.srcObject = null;
2615
2703
  this.mediaStream = newStream;
2616
2704
  this.invalidateScalingCache();
2617
2705
  const settings = newVideoTrack.getSettings();
@@ -2675,7 +2763,7 @@ Please check encoder server logs and DATABASE_URL configuration.`
2675
2763
  let streamToRecord = this.mediaStream;
2676
2764
  if (this.isVideo) {
2677
2765
  this.cleanupCanvasRendering();
2678
- streamToRecord = this.setupCanvasRendering();
2766
+ streamToRecord = await this.setupCanvasRendering();
2679
2767
  console.log("\u{1F3A8} Canvas rendering recreated for new stream");
2680
2768
  }
2681
2769
  const recorderOptions = getMediaRecorderOptions(this.isVideo);
@@ -3651,7 +3739,7 @@ function DialtribeStreamer({
3651
3739
  console.log("\u{1F4F7} Got new camera stream:", newFacingMode);
3652
3740
  const newVideoTrack = newStream.getVideoTracks()[0];
3653
3741
  if (newVideoTrack) {
3654
- streamer.replaceVideoTrack(newVideoTrack);
3742
+ await streamer.replaceVideoTrack(newVideoTrack);
3655
3743
  }
3656
3744
  const updatedStream = streamer.getMediaStream();
3657
3745
  setMediaStream(updatedStream);