@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
|
@@ -323,6 +323,9 @@ var WebSocketStreamer = class {
|
|
|
323
323
|
this.isHotSwapping = false;
|
|
324
324
|
// Track if we're swapping media streams
|
|
325
325
|
this.startTime = 0;
|
|
326
|
+
// Canvas-based rendering for seamless camera flips
|
|
327
|
+
// MediaRecorder records from canvas stream, so track changes don't affect it
|
|
328
|
+
this.canvasState = null;
|
|
326
329
|
this.streamKey = options.streamKey;
|
|
327
330
|
this.mediaStream = options.mediaStream;
|
|
328
331
|
this.isVideo = options.isVideo;
|
|
@@ -331,6 +334,36 @@ var WebSocketStreamer = class {
|
|
|
331
334
|
this.onStateChange = options.onStateChange;
|
|
332
335
|
this.onError = options.onError;
|
|
333
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Calculate scaled dimensions for fitting video into canvas.
|
|
339
|
+
* @param mode - "contain" fits video inside canvas, "cover" fills canvas (cropping)
|
|
340
|
+
*/
|
|
341
|
+
calculateScaledDimensions(videoWidth, videoHeight, canvasWidth, canvasHeight, mode) {
|
|
342
|
+
const videoAspect = videoWidth / videoHeight;
|
|
343
|
+
const canvasAspect = canvasWidth / canvasHeight;
|
|
344
|
+
const useWidthBased = mode === "contain" ? videoAspect > canvasAspect : videoAspect <= canvasAspect;
|
|
345
|
+
if (useWidthBased) {
|
|
346
|
+
const width = canvasWidth;
|
|
347
|
+
const height = canvasWidth / videoAspect;
|
|
348
|
+
return { x: 0, y: (canvasHeight - height) / 2, width, height };
|
|
349
|
+
} else {
|
|
350
|
+
const height = canvasHeight;
|
|
351
|
+
const width = canvasHeight * videoAspect;
|
|
352
|
+
return { x: (canvasWidth - width) / 2, y: 0, width, height };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Invalidate cached scaling dimensions (call when video source changes)
|
|
357
|
+
*/
|
|
358
|
+
invalidateScalingCache() {
|
|
359
|
+
if (this.canvasState) {
|
|
360
|
+
this.canvasState.cachedContain = null;
|
|
361
|
+
this.canvasState.cachedCover = null;
|
|
362
|
+
this.canvasState.cachedNeedsBackground = false;
|
|
363
|
+
this.canvasState.lastVideoWidth = 0;
|
|
364
|
+
this.canvasState.lastVideoHeight = 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
334
367
|
/**
|
|
335
368
|
* Validate stream key format
|
|
336
369
|
* Stream keys must follow format: {tierCode}{foreignId}_{randomKey}
|
|
@@ -357,6 +390,130 @@ var WebSocketStreamer = class {
|
|
|
357
390
|
isVIP: tierCode === "b" || tierCode === "w"
|
|
358
391
|
});
|
|
359
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Set up canvas-based rendering pipeline for video streams.
|
|
395
|
+
* This allows seamless camera flips by changing the video source
|
|
396
|
+
* without affecting MediaRecorder (which records from the canvas).
|
|
397
|
+
*/
|
|
398
|
+
setupCanvasRendering() {
|
|
399
|
+
console.log("\u{1F3A8} Setting up canvas-based rendering for seamless camera flips");
|
|
400
|
+
const videoTrack = this.mediaStream.getVideoTracks()[0];
|
|
401
|
+
const settings = videoTrack?.getSettings() || {};
|
|
402
|
+
const width = settings.width || 1280;
|
|
403
|
+
const height = settings.height || 720;
|
|
404
|
+
console.log(`\u{1F4D0} Video dimensions: ${width}x${height}`);
|
|
405
|
+
const canvas = document.createElement("canvas");
|
|
406
|
+
canvas.width = width;
|
|
407
|
+
canvas.height = height;
|
|
408
|
+
const ctx = canvas.getContext("2d");
|
|
409
|
+
if (!ctx) {
|
|
410
|
+
throw new Error("Failed to get 2D canvas context - canvas rendering unavailable");
|
|
411
|
+
}
|
|
412
|
+
const videoElement = document.createElement("video");
|
|
413
|
+
videoElement.srcObject = this.mediaStream;
|
|
414
|
+
videoElement.muted = true;
|
|
415
|
+
videoElement.playsInline = true;
|
|
416
|
+
videoElement.play().catch((e) => console.warn("Video autoplay warning:", e));
|
|
417
|
+
const frameRate = settings.frameRate || 30;
|
|
418
|
+
const stream = canvas.captureStream(frameRate);
|
|
419
|
+
const audioTracks = this.mediaStream.getAudioTracks();
|
|
420
|
+
audioTracks.forEach((track) => {
|
|
421
|
+
stream.addTrack(track);
|
|
422
|
+
});
|
|
423
|
+
console.log(`\u{1F3AC} Canvas stream created with ${frameRate}fps video + ${audioTracks.length} audio track(s)`);
|
|
424
|
+
this.canvasState = {
|
|
425
|
+
canvas,
|
|
426
|
+
ctx,
|
|
427
|
+
videoElement,
|
|
428
|
+
stream,
|
|
429
|
+
renderLoopId: 0,
|
|
430
|
+
// Will be set below
|
|
431
|
+
useBlurBackground: true,
|
|
432
|
+
slowFrameCount: 0,
|
|
433
|
+
cachedContain: null,
|
|
434
|
+
cachedCover: null,
|
|
435
|
+
cachedNeedsBackground: false,
|
|
436
|
+
lastVideoWidth: 0,
|
|
437
|
+
lastVideoHeight: 0
|
|
438
|
+
};
|
|
439
|
+
const state = this.canvasState;
|
|
440
|
+
const renderFrame = () => {
|
|
441
|
+
if (!this.canvasState || state !== this.canvasState) return;
|
|
442
|
+
const { ctx: ctx2, canvas: canvas2, videoElement: videoElement2 } = state;
|
|
443
|
+
if (videoElement2.paused) {
|
|
444
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const canvasWidth = canvas2.width;
|
|
448
|
+
const canvasHeight = canvas2.height;
|
|
449
|
+
const videoWidth = videoElement2.videoWidth;
|
|
450
|
+
const videoHeight = videoElement2.videoHeight;
|
|
451
|
+
if (videoWidth === 0 || videoHeight === 0) {
|
|
452
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (videoWidth !== state.lastVideoWidth || videoHeight !== state.lastVideoHeight) {
|
|
456
|
+
state.lastVideoWidth = videoWidth;
|
|
457
|
+
state.lastVideoHeight = videoHeight;
|
|
458
|
+
state.cachedContain = this.calculateScaledDimensions(
|
|
459
|
+
videoWidth,
|
|
460
|
+
videoHeight,
|
|
461
|
+
canvasWidth,
|
|
462
|
+
canvasHeight,
|
|
463
|
+
"contain"
|
|
464
|
+
);
|
|
465
|
+
state.cachedCover = this.calculateScaledDimensions(
|
|
466
|
+
videoWidth,
|
|
467
|
+
videoHeight,
|
|
468
|
+
canvasWidth,
|
|
469
|
+
canvasHeight,
|
|
470
|
+
"cover"
|
|
471
|
+
);
|
|
472
|
+
state.cachedNeedsBackground = Math.abs(state.cachedContain.width - canvasWidth) > 1 || Math.abs(state.cachedContain.height - canvasHeight) > 1;
|
|
473
|
+
console.log(`\u{1F4D0} Video dimensions changed: ${videoWidth}x${videoHeight}, needsBackground: ${state.cachedNeedsBackground}`);
|
|
474
|
+
}
|
|
475
|
+
const contain = state.cachedContain;
|
|
476
|
+
const cover = state.cachedCover;
|
|
477
|
+
const frameStart = performance.now();
|
|
478
|
+
if (state.cachedNeedsBackground && state.useBlurBackground) {
|
|
479
|
+
ctx2.save();
|
|
480
|
+
ctx2.filter = "blur(20px)";
|
|
481
|
+
ctx2.drawImage(videoElement2, cover.x, cover.y, cover.width, cover.height);
|
|
482
|
+
ctx2.restore();
|
|
483
|
+
ctx2.fillStyle = "rgba(0, 0, 0, 0.5)";
|
|
484
|
+
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
485
|
+
} else if (state.cachedNeedsBackground) {
|
|
486
|
+
ctx2.fillStyle = "#000";
|
|
487
|
+
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
488
|
+
}
|
|
489
|
+
ctx2.drawImage(videoElement2, contain.x, contain.y, contain.width, contain.height);
|
|
490
|
+
const frameDuration = performance.now() - frameStart;
|
|
491
|
+
if (frameDuration > 16 && state.useBlurBackground) {
|
|
492
|
+
state.slowFrameCount++;
|
|
493
|
+
if (state.slowFrameCount > 5) {
|
|
494
|
+
console.log("\u26A1 Disabling blur background for performance");
|
|
495
|
+
state.useBlurBackground = false;
|
|
496
|
+
}
|
|
497
|
+
} else if (frameDuration <= 16) {
|
|
498
|
+
state.slowFrameCount = 0;
|
|
499
|
+
}
|
|
500
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
501
|
+
};
|
|
502
|
+
state.renderLoopId = requestAnimationFrame(renderFrame);
|
|
503
|
+
console.log("\u2705 Canvas rendering pipeline ready (with adaptive blur background)");
|
|
504
|
+
return stream;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Clean up canvas rendering resources
|
|
508
|
+
*/
|
|
509
|
+
cleanupCanvasRendering() {
|
|
510
|
+
if (!this.canvasState) return;
|
|
511
|
+
cancelAnimationFrame(this.canvasState.renderLoopId);
|
|
512
|
+
this.canvasState.videoElement.pause();
|
|
513
|
+
this.canvasState.videoElement.srcObject = null;
|
|
514
|
+
this.canvasState.stream.getTracks().forEach((track) => track.stop());
|
|
515
|
+
this.canvasState = null;
|
|
516
|
+
}
|
|
360
517
|
/**
|
|
361
518
|
* Build WebSocket URL from stream key
|
|
362
519
|
*/
|
|
@@ -376,6 +533,10 @@ var WebSocketStreamer = class {
|
|
|
376
533
|
*/
|
|
377
534
|
async start() {
|
|
378
535
|
try {
|
|
536
|
+
this.userStopped = false;
|
|
537
|
+
this.chunksSent = 0;
|
|
538
|
+
this.bytesSent = 0;
|
|
539
|
+
this.startTime = 0;
|
|
379
540
|
this.validateStreamKeyFormat();
|
|
380
541
|
this.onStateChange?.("connecting");
|
|
381
542
|
const wsUrl = this.buildWebSocketUrl();
|
|
@@ -386,8 +547,15 @@ var WebSocketStreamer = class {
|
|
|
386
547
|
reject(new Error("WebSocket not initialized"));
|
|
387
548
|
return;
|
|
388
549
|
}
|
|
389
|
-
|
|
550
|
+
const timeoutId = setTimeout(() => {
|
|
551
|
+
reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
|
|
552
|
+
}, 1e4);
|
|
553
|
+
this.websocket.addEventListener("open", () => {
|
|
554
|
+
clearTimeout(timeoutId);
|
|
555
|
+
resolve();
|
|
556
|
+
}, { once: true });
|
|
390
557
|
this.websocket.addEventListener("error", (event) => {
|
|
558
|
+
clearTimeout(timeoutId);
|
|
391
559
|
console.error("\u274C WebSocket error event:", event);
|
|
392
560
|
console.error("\u{1F50D} Connection diagnostics:", {
|
|
393
561
|
url: wsUrl.replace(this.streamKey, "***"),
|
|
@@ -406,16 +574,17 @@ Common causes:
|
|
|
406
574
|
Please check encoder server logs and DATABASE_URL configuration.`
|
|
407
575
|
));
|
|
408
576
|
}, { once: true });
|
|
409
|
-
setTimeout(() => {
|
|
410
|
-
reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
|
|
411
|
-
}, 1e4);
|
|
412
577
|
});
|
|
413
578
|
console.log("\u2705 WebSocket connected");
|
|
414
579
|
this.setupWebSocketHandlers();
|
|
580
|
+
const streamToRecord = this.isVideo ? this.setupCanvasRendering() : this.mediaStream;
|
|
415
581
|
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
416
582
|
this.mimeType = recorderOptions.mimeType;
|
|
417
|
-
this.mediaRecorder = new MediaRecorder(
|
|
583
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
418
584
|
console.log("\u{1F399}\uFE0F MediaRecorder created with options:", recorderOptions);
|
|
585
|
+
if (this.isVideo) {
|
|
586
|
+
console.log("\u{1F3A8} Recording from canvas stream (enables seamless camera flips)");
|
|
587
|
+
}
|
|
419
588
|
this.setupMediaRecorderHandlers();
|
|
420
589
|
this.mediaRecorder.start(300);
|
|
421
590
|
this.startTime = Date.now();
|
|
@@ -452,9 +621,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
452
621
|
} else {
|
|
453
622
|
console.log("\u26A0\uFE0F No WebSocket to close");
|
|
454
623
|
}
|
|
624
|
+
this.cleanupCanvasRendering();
|
|
455
625
|
this.mediaRecorder = null;
|
|
456
626
|
this.websocket = null;
|
|
457
|
-
this.bytesSent = 0;
|
|
458
627
|
this.onStateChange?.("stopped");
|
|
459
628
|
}
|
|
460
629
|
/**
|
|
@@ -463,6 +632,13 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
463
632
|
getBytesSent() {
|
|
464
633
|
return this.bytesSent;
|
|
465
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Get the current source media stream.
|
|
637
|
+
* This may change after replaceVideoTrack() is called.
|
|
638
|
+
*/
|
|
639
|
+
getMediaStream() {
|
|
640
|
+
return this.mediaStream;
|
|
641
|
+
}
|
|
466
642
|
/**
|
|
467
643
|
* Get current diagnostics
|
|
468
644
|
*/
|
|
@@ -477,25 +653,41 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
477
653
|
};
|
|
478
654
|
}
|
|
479
655
|
/**
|
|
480
|
-
* Replace the video track
|
|
481
|
-
* This avoids sending a new WebM header, which FFmpeg can't handle mid-stream.
|
|
656
|
+
* Replace the video track for camera flips.
|
|
482
657
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
658
|
+
* When using canvas-based rendering (video streams), this updates the video
|
|
659
|
+
* element source. The canvas continues drawing, and MediaRecorder is unaffected.
|
|
485
660
|
*
|
|
486
661
|
* @param newVideoTrack - The new video track from the flipped camera
|
|
487
662
|
*/
|
|
488
663
|
replaceVideoTrack(newVideoTrack) {
|
|
489
|
-
console.log("\u{1F504} Replacing video track
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
this.mediaStream.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
664
|
+
console.log("\u{1F504} Replacing video track");
|
|
665
|
+
if (this.canvasState) {
|
|
666
|
+
console.log("\u{1F3A8} Using canvas-based swap (MediaRecorder unaffected)");
|
|
667
|
+
const audioTracks = this.mediaStream.getAudioTracks();
|
|
668
|
+
const newStream = new MediaStream([newVideoTrack, ...audioTracks]);
|
|
669
|
+
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));
|
|
672
|
+
this.mediaStream = newStream;
|
|
673
|
+
this.invalidateScalingCache();
|
|
674
|
+
const settings = newVideoTrack.getSettings();
|
|
675
|
+
if (settings.width && settings.height) {
|
|
676
|
+
console.log(`\u{1F4D0} New camera resolution: ${settings.width}x${settings.height}`);
|
|
677
|
+
}
|
|
678
|
+
console.log("\u2705 Video source swapped - canvas continues seamlessly");
|
|
679
|
+
} else {
|
|
680
|
+
console.warn("\u26A0\uFE0F Canvas not available - attempting direct track replacement");
|
|
681
|
+
const oldVideoTracks = this.mediaStream.getVideoTracks();
|
|
682
|
+
this.mediaStream.addTrack(newVideoTrack);
|
|
683
|
+
console.log("\u2795 New video track added");
|
|
684
|
+
oldVideoTracks.forEach((track) => {
|
|
685
|
+
this.mediaStream.removeTrack(track);
|
|
686
|
+
track.stop();
|
|
687
|
+
});
|
|
688
|
+
console.log("\u2796 Old video track(s) removed");
|
|
689
|
+
console.log("\u2705 Video track replaced");
|
|
690
|
+
}
|
|
499
691
|
}
|
|
500
692
|
/**
|
|
501
693
|
* Replace the audio track in the current MediaStream without stopping MediaRecorder.
|
|
@@ -506,88 +698,28 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
506
698
|
console.log("\u{1F504} Replacing audio track (no MediaRecorder restart)");
|
|
507
699
|
const oldAudioTracks = this.mediaStream.getAudioTracks();
|
|
508
700
|
this.mediaStream.addTrack(newAudioTrack);
|
|
509
|
-
console.log("\u2795 New audio track added");
|
|
701
|
+
console.log("\u2795 New audio track added to source stream");
|
|
510
702
|
oldAudioTracks.forEach((track) => {
|
|
511
703
|
this.mediaStream.removeTrack(track);
|
|
512
704
|
track.stop();
|
|
513
705
|
});
|
|
514
|
-
console.log("\u2796 Old audio track(s) removed");
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
* Prepare for a hot-swap by stopping the MediaRecorder
|
|
519
|
-
* Call this BEFORE requesting a new camera stream on iOS
|
|
520
|
-
* This prevents iOS Safari from interfering with the active MediaRecorder
|
|
521
|
-
*
|
|
522
|
-
* IMPORTANT: This is async because we must wait for the MediaRecorder's 'stop'
|
|
523
|
-
* event to fire BEFORE proceeding. Otherwise there's a race condition where
|
|
524
|
-
* the stop event fires after isHotSwapping is set to false, causing the
|
|
525
|
-
* WebSocket to close unexpectedly.
|
|
526
|
-
*
|
|
527
|
-
* @deprecated Prefer replaceVideoTrack() which doesn't restart MediaRecorder
|
|
528
|
-
*/
|
|
529
|
-
async prepareForHotSwap() {
|
|
530
|
-
console.log("\u{1F504} Preparing for hot-swap (stopping MediaRecorder first)");
|
|
531
|
-
this.isHotSwapping = true;
|
|
532
|
-
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
533
|
-
await new Promise((resolve) => {
|
|
534
|
-
const currentRecorder = this.mediaRecorder;
|
|
535
|
-
const handleStop = () => {
|
|
536
|
-
currentRecorder.removeEventListener("stop", handleStop);
|
|
537
|
-
console.log("\u23F9\uFE0F MediaRecorder stopped - ready for camera switch");
|
|
538
|
-
resolve();
|
|
539
|
-
};
|
|
540
|
-
currentRecorder.addEventListener("stop", handleStop);
|
|
541
|
-
currentRecorder.stop();
|
|
706
|
+
console.log("\u2796 Old audio track(s) removed from source stream");
|
|
707
|
+
if (this.canvasState) {
|
|
708
|
+
this.canvasState.stream.getAudioTracks().forEach((track) => {
|
|
709
|
+
this.canvasState.stream.removeTrack(track);
|
|
542
710
|
});
|
|
711
|
+
this.canvasState.stream.addTrack(newAudioTrack);
|
|
712
|
+
console.log("\u{1F3A8} Audio track synced to canvas stream");
|
|
543
713
|
}
|
|
714
|
+
console.log("\u2705 Audio track replaced - streaming continues seamlessly");
|
|
544
715
|
}
|
|
545
716
|
/**
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* @deprecated Prefer replaceVideoTrack() which doesn't restart MediaRecorder
|
|
550
|
-
*/
|
|
551
|
-
cancelHotSwap() {
|
|
552
|
-
console.log("\u21A9\uFE0F Canceling hot-swap - restarting with original stream");
|
|
553
|
-
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
554
|
-
this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
|
|
555
|
-
this.setupMediaRecorderHandlers();
|
|
556
|
-
this.mediaRecorder.start(300);
|
|
557
|
-
this.isHotSwapping = false;
|
|
558
|
-
console.log("\u2705 Original stream restored");
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Complete the hot-swap with a new media stream
|
|
562
|
-
* Call this AFTER successfully obtaining a new camera stream
|
|
563
|
-
*
|
|
564
|
-
* Note: Errors are thrown to the caller, not sent to onError callback
|
|
565
|
-
* This allows the caller to handle camera flip failures gracefully
|
|
566
|
-
*
|
|
567
|
-
* @deprecated Prefer replaceVideoTrack() which doesn't restart MediaRecorder
|
|
568
|
-
*/
|
|
569
|
-
completeHotSwap(newMediaStream) {
|
|
570
|
-
console.log("\u{1F504} Completing hot-swap with new stream");
|
|
571
|
-
this.mediaStream = newMediaStream;
|
|
572
|
-
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
573
|
-
this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
|
|
574
|
-
console.log("\u{1F399}\uFE0F New MediaRecorder created");
|
|
575
|
-
this.setupMediaRecorderHandlers();
|
|
576
|
-
this.mediaRecorder.start(300);
|
|
577
|
-
this.isHotSwapping = false;
|
|
578
|
-
console.log("\u2705 Media stream updated - streaming continues");
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Update the media stream (e.g., when flipping camera)
|
|
582
|
-
* This keeps the WebSocket connection alive while swapping the media source
|
|
583
|
-
*
|
|
584
|
-
* Note: On iOS, prefer using prepareForHotSwap() + completeHotSwap() to avoid
|
|
585
|
-
* issues where getUserMedia interferes with the active MediaRecorder.
|
|
586
|
-
*
|
|
587
|
-
* Note: Errors are thrown to the caller, not sent to onError callback
|
|
588
|
-
* This allows the caller to handle camera flip failures gracefully
|
|
717
|
+
* Update the media stream (e.g., when switching devices from settings)
|
|
718
|
+
* This keeps the WebSocket connection alive while swapping the media source.
|
|
719
|
+
* Restarts the MediaRecorder with the new stream.
|
|
589
720
|
*
|
|
590
|
-
*
|
|
721
|
+
* Note: For camera flips, prefer replaceVideoTrack() which doesn't restart MediaRecorder.
|
|
722
|
+
* Note: Errors are thrown to the caller, not sent to onError callback.
|
|
591
723
|
*/
|
|
592
724
|
async updateMediaStream(newMediaStream) {
|
|
593
725
|
console.log("\u{1F504} Updating media stream (hot-swap)");
|
|
@@ -597,8 +729,14 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
597
729
|
console.log("\u23F9\uFE0F Old MediaRecorder stopped");
|
|
598
730
|
}
|
|
599
731
|
this.mediaStream = newMediaStream;
|
|
732
|
+
let streamToRecord = this.mediaStream;
|
|
733
|
+
if (this.isVideo) {
|
|
734
|
+
this.cleanupCanvasRendering();
|
|
735
|
+
streamToRecord = this.setupCanvasRendering();
|
|
736
|
+
console.log("\u{1F3A8} Canvas rendering recreated for new stream");
|
|
737
|
+
}
|
|
600
738
|
const recorderOptions = getMediaRecorderOptions(this.isVideo);
|
|
601
|
-
this.mediaRecorder = new MediaRecorder(
|
|
739
|
+
this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
|
|
602
740
|
console.log("\u{1F399}\uFE0F New MediaRecorder created");
|
|
603
741
|
this.setupMediaRecorderHandlers();
|
|
604
742
|
this.mediaRecorder.start(300);
|
|
@@ -651,7 +789,9 @@ Please check encoder server logs and DATABASE_URL configuration.`
|
|
|
651
789
|
this.bytesSent += event.data.size;
|
|
652
790
|
this.chunksSent += 1;
|
|
653
791
|
this.onBytesUpdate?.(this.bytesSent);
|
|
654
|
-
|
|
792
|
+
if (this.chunksSent % 10 === 0) {
|
|
793
|
+
console.log(`\u{1F4E4} Sent ${this.chunksSent} chunks (${(this.bytesSent / 1024 / 1024).toFixed(2)} MB total)`);
|
|
794
|
+
}
|
|
655
795
|
}
|
|
656
796
|
});
|
|
657
797
|
this.mediaRecorder.addEventListener("error", (event) => {
|
|
@@ -1922,7 +2062,7 @@ function DialtribeStreamer({
|
|
|
1922
2062
|
if (!isVideoKey || !hasMultipleCameras) return;
|
|
1923
2063
|
const newFacingMode = facingMode === "user" ? "environment" : "user";
|
|
1924
2064
|
if (state === "live" && streamer) {
|
|
1925
|
-
console.log("\u{1F504} Flipping camera during live broadcast (
|
|
2065
|
+
console.log("\u{1F504} Flipping camera during live broadcast (canvas-based swap)");
|
|
1926
2066
|
try {
|
|
1927
2067
|
const constraints = getMediaConstraints({
|
|
1928
2068
|
isVideo: true,
|
|
@@ -1934,9 +2074,11 @@ function DialtribeStreamer({
|
|
|
1934
2074
|
if (newVideoTrack) {
|
|
1935
2075
|
streamer.replaceVideoTrack(newVideoTrack);
|
|
1936
2076
|
}
|
|
2077
|
+
const updatedStream = streamer.getMediaStream();
|
|
2078
|
+
setMediaStream(updatedStream);
|
|
1937
2079
|
setFacingMode(newFacingMode);
|
|
1938
|
-
if (videoRef.current
|
|
1939
|
-
videoRef.current.srcObject =
|
|
2080
|
+
if (videoRef.current) {
|
|
2081
|
+
videoRef.current.srcObject = updatedStream;
|
|
1940
2082
|
}
|
|
1941
2083
|
console.log("\u2705 Camera flipped successfully - broadcast continues seamlessly");
|
|
1942
2084
|
} catch (err) {
|