@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.
@@ -224,6 +224,9 @@ declare class WebSocketStreamer {
224
224
  * Set up canvas-based rendering pipeline for video streams.
225
225
  * This allows seamless camera flips by changing the video source
226
226
  * without affecting MediaRecorder (which records from the canvas).
227
+ *
228
+ * This is async to ensure the video is producing frames before returning,
229
+ * which prevents black initial thumbnails.
227
230
  */
228
231
  private setupCanvasRendering;
229
232
  /**
@@ -258,12 +261,14 @@ declare class WebSocketStreamer {
258
261
  /**
259
262
  * Replace the video track for camera flips.
260
263
  *
261
- * When using canvas-based rendering (video streams), this updates the video
262
- * element source. The canvas continues drawing, and MediaRecorder is unaffected.
264
+ * When using canvas-based rendering (video streams), this preloads the new
265
+ * camera in a temporary video element, waits for it to be ready, then swaps
266
+ * it in. This ensures continuous frame output with no gaps.
263
267
  *
264
268
  * @param newVideoTrack - The new video track from the flipped camera
269
+ * @returns Promise that resolves when the swap is complete
265
270
  */
266
- replaceVideoTrack(newVideoTrack: MediaStreamTrack): void;
271
+ replaceVideoTrack(newVideoTrack: MediaStreamTrack): Promise<void>;
267
272
  /**
268
273
  * Replace the audio track in the current MediaStream without stopping MediaRecorder.
269
274
  *
@@ -224,6 +224,9 @@ declare class WebSocketStreamer {
224
224
  * Set up canvas-based rendering pipeline for video streams.
225
225
  * This allows seamless camera flips by changing the video source
226
226
  * without affecting MediaRecorder (which records from the canvas).
227
+ *
228
+ * This is async to ensure the video is producing frames before returning,
229
+ * which prevents black initial thumbnails.
227
230
  */
228
231
  private setupCanvasRendering;
229
232
  /**
@@ -258,12 +261,14 @@ declare class WebSocketStreamer {
258
261
  /**
259
262
  * Replace the video track for camera flips.
260
263
  *
261
- * When using canvas-based rendering (video streams), this updates the video
262
- * element source. The canvas continues drawing, and MediaRecorder is unaffected.
264
+ * When using canvas-based rendering (video streams), this preloads the new
265
+ * camera in a temporary video element, waits for it to be ready, then swaps
266
+ * it in. This ensures continuous frame output with no gaps.
263
267
  *
264
268
  * @param newVideoTrack - The new video track from the flipped camera
269
+ * @returns Promise that resolves when the swap is complete
265
270
  */
266
- replaceVideoTrack(newVideoTrack: MediaStreamTrack): void;
271
+ replaceVideoTrack(newVideoTrack: MediaStreamTrack): Promise<void>;
267
272
  /**
268
273
  * Replace the audio track in the current MediaStream without stopping MediaRecorder.
269
274
  *
@@ -1,4 +1,4 @@
1
1
  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, r as useDialtribeOptional } from './dialtribe-player-CNriUtNi.mjs';
2
- 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, 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';
2
+ 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, 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-Do-8Oavc.mjs';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
@@ -1,4 +1,4 @@
1
1
  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, r as useDialtribeOptional } from './dialtribe-player-CNriUtNi.js';
2
- 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, 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';
2
+ 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, 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-DOhD-r_F.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
@@ -394,8 +394,11 @@ var WebSocketStreamer = class {
394
394
  * Set up canvas-based rendering pipeline for video streams.
395
395
  * This allows seamless camera flips by changing the video source
396
396
  * without affecting MediaRecorder (which records from the canvas).
397
+ *
398
+ * This is async to ensure the video is producing frames before returning,
399
+ * which prevents black initial thumbnails.
397
400
  */
398
- setupCanvasRendering() {
401
+ async setupCanvasRendering() {
399
402
  console.log("\u{1F3A8} Setting up canvas-based rendering for seamless camera flips");
400
403
  const videoTrack = this.mediaStream.getVideoTracks()[0];
401
404
  const settings = videoTrack?.getSettings() || {};
@@ -413,7 +416,27 @@ var WebSocketStreamer = class {
413
416
  videoElement.srcObject = this.mediaStream;
414
417
  videoElement.muted = true;
415
418
  videoElement.playsInline = true;
416
- videoElement.play().catch((e) => console.warn("Video autoplay warning:", e));
419
+ await new Promise((resolve) => {
420
+ const checkReady = () => {
421
+ if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
422
+ console.log(`\u{1F4F9} Video ready: ${videoElement.videoWidth}x${videoElement.videoHeight}`);
423
+ resolve();
424
+ } else {
425
+ requestAnimationFrame(checkReady);
426
+ }
427
+ };
428
+ videoElement.addEventListener("loadeddata", () => {
429
+ checkReady();
430
+ }, { once: true });
431
+ videoElement.play().catch((e) => {
432
+ console.warn("Video autoplay warning:", e);
433
+ resolve();
434
+ });
435
+ setTimeout(() => {
436
+ console.warn("\u26A0\uFE0F Video ready timeout - continuing anyway");
437
+ resolve();
438
+ }, 2e3);
439
+ });
417
440
  const frameRate = settings.frameRate || 30;
418
441
  const stream = canvas.captureStream(frameRate);
419
442
  const audioTracks = this.mediaStream.getAudioTracks();
@@ -436,13 +459,28 @@ var WebSocketStreamer = class {
436
459
  lastVideoWidth: 0,
437
460
  lastVideoHeight: 0
438
461
  };
462
+ if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
463
+ const vw = videoElement.videoWidth;
464
+ const vh = videoElement.videoHeight;
465
+ const cw = canvas.width;
466
+ const ch = canvas.height;
467
+ const scale = Math.min(cw / vw, ch / vh);
468
+ const sw = vw * scale;
469
+ const sh = vh * scale;
470
+ const sx = (cw - sw) / 2;
471
+ const sy = (ch - sh) / 2;
472
+ ctx.fillStyle = "#000";
473
+ ctx.fillRect(0, 0, cw, ch);
474
+ ctx.drawImage(videoElement, sx, sy, sw, sh);
475
+ console.log("\u{1F5BC}\uFE0F Drew first frame synchronously to prevent black thumbnail");
476
+ }
439
477
  const state = this.canvasState;
440
478
  const renderFrame = () => {
441
479
  if (!this.canvasState || state !== this.canvasState) return;
442
480
  const { ctx: ctx2, canvas: canvas2, videoElement: videoElement2 } = state;
443
481
  if (videoElement2.paused) {
444
- state.renderLoopId = requestAnimationFrame(renderFrame);
445
- return;
482
+ videoElement2.play().catch(() => {
483
+ });
446
484
  }
447
485
  const canvasWidth = canvas2.width;
448
486
  const canvasHeight = canvas2.height;
@@ -577,7 +615,7 @@ Please check encoder server logs and DATABASE_URL configuration.`
577
615
  });
578
616
  console.log("\u2705 WebSocket connected");
579
617
  this.setupWebSocketHandlers();
580
- const streamToRecord = this.isVideo ? this.setupCanvasRendering() : this.mediaStream;
618
+ const streamToRecord = this.isVideo ? await this.setupCanvasRendering() : this.mediaStream;
581
619
  const recorderOptions = getMediaRecorderOptions(this.isVideo);
582
620
  this.mimeType = recorderOptions.mimeType;
583
621
  this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
@@ -655,20 +693,70 @@ Please check encoder server logs and DATABASE_URL configuration.`
655
693
  /**
656
694
  * Replace the video track for camera flips.
657
695
  *
658
- * When using canvas-based rendering (video streams), this updates the video
659
- * element source. The canvas continues drawing, and MediaRecorder is unaffected.
696
+ * When using canvas-based rendering (video streams), this preloads the new
697
+ * camera in a temporary video element, waits for it to be ready, then swaps
698
+ * it in. This ensures continuous frame output with no gaps.
660
699
  *
661
700
  * @param newVideoTrack - The new video track from the flipped camera
701
+ * @returns Promise that resolves when the swap is complete
662
702
  */
663
- replaceVideoTrack(newVideoTrack) {
703
+ async replaceVideoTrack(newVideoTrack) {
664
704
  console.log("\u{1F504} Replacing video track");
665
705
  if (this.canvasState) {
666
- console.log("\u{1F3A8} Using canvas-based swap (MediaRecorder unaffected)");
706
+ console.log("\u{1F3A8} Using canvas-based swap with preloading (no frame gaps)");
667
707
  const audioTracks = this.mediaStream.getAudioTracks();
668
708
  const newStream = new MediaStream([newVideoTrack, ...audioTracks]);
709
+ const preloadVideo = document.createElement("video");
710
+ preloadVideo.srcObject = newStream;
711
+ preloadVideo.muted = true;
712
+ preloadVideo.playsInline = true;
713
+ await new Promise((resolve, reject) => {
714
+ const timeout = setTimeout(() => {
715
+ console.warn("\u26A0\uFE0F Video preload timeout - switching anyway");
716
+ if (preloadVideo.paused) {
717
+ preloadVideo.play().catch(() => {
718
+ });
719
+ }
720
+ resolve();
721
+ }, 3e3);
722
+ const checkFullyReady = () => {
723
+ if (preloadVideo.videoWidth > 0 && preloadVideo.videoHeight > 0 && !preloadVideo.paused) {
724
+ clearTimeout(timeout);
725
+ console.log(`\u{1F4F9} New camera ready and playing: ${preloadVideo.videoWidth}x${preloadVideo.videoHeight}`);
726
+ resolve();
727
+ return true;
728
+ }
729
+ return false;
730
+ };
731
+ preloadVideo.addEventListener("loadeddata", () => {
732
+ preloadVideo.play().then(() => {
733
+ requestAnimationFrame(() => {
734
+ if (!checkFullyReady()) {
735
+ const pollPlaying = setInterval(() => {
736
+ if (checkFullyReady()) {
737
+ clearInterval(pollPlaying);
738
+ }
739
+ }, 50);
740
+ setTimeout(() => clearInterval(pollPlaying), 2e3);
741
+ }
742
+ });
743
+ }).catch((e) => {
744
+ console.warn("Video preload play warning:", e);
745
+ checkFullyReady();
746
+ });
747
+ }, { once: true });
748
+ preloadVideo.addEventListener("error", (e) => {
749
+ clearTimeout(timeout);
750
+ reject(new Error(`Video preload failed: ${e}`));
751
+ }, { once: true });
752
+ preloadVideo.play().catch(() => {
753
+ });
754
+ });
755
+ const oldVideoElement = this.canvasState.videoElement;
756
+ this.canvasState.videoElement = preloadVideo;
669
757
  this.mediaStream.getVideoTracks().forEach((track) => track.stop());
670
- this.canvasState.videoElement.srcObject = newStream;
671
- this.canvasState.videoElement.play().catch((e) => console.warn("Video play warning:", e));
758
+ oldVideoElement.pause();
759
+ oldVideoElement.srcObject = null;
672
760
  this.mediaStream = newStream;
673
761
  this.invalidateScalingCache();
674
762
  const settings = newVideoTrack.getSettings();
@@ -732,7 +820,7 @@ Please check encoder server logs and DATABASE_URL configuration.`
732
820
  let streamToRecord = this.mediaStream;
733
821
  if (this.isVideo) {
734
822
  this.cleanupCanvasRendering();
735
- streamToRecord = this.setupCanvasRendering();
823
+ streamToRecord = await this.setupCanvasRendering();
736
824
  console.log("\u{1F3A8} Canvas rendering recreated for new stream");
737
825
  }
738
826
  const recorderOptions = getMediaRecorderOptions(this.isVideo);
@@ -2072,7 +2160,7 @@ function DialtribeStreamer({
2072
2160
  console.log("\u{1F4F7} Got new camera stream:", newFacingMode);
2073
2161
  const newVideoTrack = newStream.getVideoTracks()[0];
2074
2162
  if (newVideoTrack) {
2075
- streamer.replaceVideoTrack(newVideoTrack);
2163
+ await streamer.replaceVideoTrack(newVideoTrack);
2076
2164
  }
2077
2165
  const updatedStream = streamer.getMediaStream();
2078
2166
  setMediaStream(updatedStream);