@unboundcx/video-sdk-client 2.0.2 → 2.0.3
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/managers/LocalMediaManager.js +171 -41
- package/package.json +1 -1
|
@@ -57,6 +57,15 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
57
57
|
// Video processor for virtual backgrounds
|
|
58
58
|
this.videoProcessor = new VideoProcessor({ debug: options.debug });
|
|
59
59
|
this.currentBackgroundOptions = null;
|
|
60
|
+
|
|
61
|
+
// When a virtual background is active, `streams.camera` is the
|
|
62
|
+
// canvas-captured (processed) stream — its track has no deviceId
|
|
63
|
+
// or facingMode. We keep the *raw* camera stream and the original
|
|
64
|
+
// source descriptor around so subsequent operations (background
|
|
65
|
+
// swaps, camera changes) can re-acquire or reuse the real camera
|
|
66
|
+
// rather than reading constraints off the canvas track.
|
|
67
|
+
this.rawCameraStream = null;
|
|
68
|
+
this.cameraSource = null; // { deviceId } | { facingMode } | null
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/**
|
|
@@ -186,6 +195,19 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
|
|
198
|
+
// Remember the raw camera stream + source descriptor BEFORE any
|
|
199
|
+
// background processing wraps it. Used by updateCameraBackground
|
|
200
|
+
// and changeCamera so they don't read constraints off a canvas
|
|
201
|
+
// track (which has no deviceId / facingMode).
|
|
202
|
+
this.rawCameraStream = stream;
|
|
203
|
+
if (options.facingMode) {
|
|
204
|
+
this.cameraSource = { facingMode: options.facingMode };
|
|
205
|
+
} else {
|
|
206
|
+
const trackDeviceId =
|
|
207
|
+
originalTrack.getSettings().deviceId || options.deviceId || null;
|
|
208
|
+
this.cameraSource = trackDeviceId ? { deviceId: trackDeviceId } : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
189
211
|
// Apply background effect if specified
|
|
190
212
|
if (options.background && options.background.type !== 'none') {
|
|
191
213
|
this.logger.info('Applying background effect:', options.background);
|
|
@@ -442,6 +464,22 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
442
464
|
// Stop all tracks
|
|
443
465
|
this.streams.camera.getTracks().forEach((track) => track.stop());
|
|
444
466
|
|
|
467
|
+
// Also stop the raw camera stream if it's distinct (background was
|
|
468
|
+
// active — processed/canvas stream wraps the raw camera stream).
|
|
469
|
+
if (this.rawCameraStream && this.rawCameraStream !== this.streams.camera) {
|
|
470
|
+
this.rawCameraStream.getTracks().forEach((t) => t.stop());
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Tear down processor if a background was active.
|
|
474
|
+
if (this.currentBackgroundOptions) {
|
|
475
|
+
try {
|
|
476
|
+
await this.videoProcessor.cleanup();
|
|
477
|
+
} catch (cleanupErr) {
|
|
478
|
+
this.logger.warn('Processor cleanup during stopCamera failed:', cleanupErr);
|
|
479
|
+
}
|
|
480
|
+
this.currentBackgroundOptions = null;
|
|
481
|
+
}
|
|
482
|
+
|
|
445
483
|
// Close producer
|
|
446
484
|
if (this.producers.video) {
|
|
447
485
|
await this.mediasoup.closeProducer('video');
|
|
@@ -449,6 +487,8 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
449
487
|
}
|
|
450
488
|
|
|
451
489
|
this.streams.camera = null;
|
|
490
|
+
this.rawCameraStream = null;
|
|
491
|
+
this.cameraSource = null;
|
|
452
492
|
|
|
453
493
|
this.emit('stream:removed', { type: 'camera' });
|
|
454
494
|
}
|
|
@@ -687,18 +727,28 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
687
727
|
throw new Error('No active camera to change');
|
|
688
728
|
}
|
|
689
729
|
|
|
730
|
+
const oldProcessedStream = this.streams.camera;
|
|
731
|
+
const oldRawStream = this.rawCameraStream;
|
|
732
|
+
|
|
690
733
|
try {
|
|
691
|
-
// Get new stream
|
|
734
|
+
// Get new stream (always fresh — camera device is changing).
|
|
692
735
|
const constraints = this._getVideoConstraints(options);
|
|
693
|
-
|
|
736
|
+
const rawStream = await navigator.mediaDevices.getUserMedia({
|
|
694
737
|
video: constraints,
|
|
695
738
|
});
|
|
696
739
|
|
|
740
|
+
let stream = rawStream;
|
|
741
|
+
|
|
697
742
|
// Reapply background effect if it was active
|
|
698
743
|
if (this.currentBackgroundOptions) {
|
|
699
744
|
this.logger.info('Reapplying background effect after camera change');
|
|
745
|
+
try {
|
|
746
|
+
await this.videoProcessor.cleanup();
|
|
747
|
+
} catch (cleanupErr) {
|
|
748
|
+
this.logger.warn('Processor cleanup during camera change failed:', cleanupErr);
|
|
749
|
+
}
|
|
700
750
|
stream = await this.videoProcessor.applyBackground(
|
|
701
|
-
|
|
751
|
+
rawStream,
|
|
702
752
|
this.currentBackgroundOptions,
|
|
703
753
|
);
|
|
704
754
|
}
|
|
@@ -716,13 +766,32 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
716
766
|
this.muteState.camera = false;
|
|
717
767
|
}
|
|
718
768
|
|
|
719
|
-
// Stop
|
|
720
|
-
|
|
721
|
-
|
|
769
|
+
// Stop the OLD raw camera stream (the actual device). The old
|
|
770
|
+
// processed/canvas stream's track is no longer attached to the
|
|
771
|
+
// producer; stop it too so the canvas doesn't keep pumping.
|
|
772
|
+
if (oldRawStream && oldRawStream !== rawStream) {
|
|
773
|
+
oldRawStream.getTracks().forEach((t) => t.stop());
|
|
774
|
+
}
|
|
775
|
+
if (
|
|
776
|
+
oldProcessedStream &&
|
|
777
|
+
oldProcessedStream !== oldRawStream &&
|
|
778
|
+
oldProcessedStream !== stream
|
|
779
|
+
) {
|
|
780
|
+
oldProcessedStream.getTracks().forEach((t) => t.stop());
|
|
722
781
|
}
|
|
723
782
|
|
|
724
|
-
// Store new
|
|
783
|
+
// Store new streams + source descriptor.
|
|
725
784
|
this.streams.camera = stream;
|
|
785
|
+
this.rawCameraStream = rawStream;
|
|
786
|
+
if (options.facingMode) {
|
|
787
|
+
this.cameraSource = { facingMode: options.facingMode };
|
|
788
|
+
} else {
|
|
789
|
+
const trackDeviceId =
|
|
790
|
+
rawStream.getVideoTracks()[0]?.getSettings().deviceId ||
|
|
791
|
+
options.deviceId ||
|
|
792
|
+
null;
|
|
793
|
+
this.cameraSource = trackDeviceId ? { deviceId: trackDeviceId } : null;
|
|
794
|
+
}
|
|
726
795
|
|
|
727
796
|
this.logger.info('Camera changed successfully');
|
|
728
797
|
|
|
@@ -766,66 +835,127 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
766
835
|
throw new Error('No active video producer');
|
|
767
836
|
}
|
|
768
837
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const deviceId = currentTrack.getSettings().deviceId;
|
|
838
|
+
// Remember everything we'd need to restore on failure. We do NOT
|
|
839
|
+
// stop the existing canvas / camera or tear down the processor
|
|
840
|
+
// until the NEW track has been swapped in successfully — otherwise
|
|
841
|
+
// a failure mid-swap leaves the producer holding a stopped track
|
|
842
|
+
// and the user's video freezes locally and on remotes.
|
|
843
|
+
const hadBackground = !!this.currentBackgroundOptions;
|
|
844
|
+
const oldProcessedStream = this.streams.camera;
|
|
845
|
+
const oldRawStream = this.rawCameraStream;
|
|
778
846
|
|
|
779
|
-
|
|
847
|
+
try {
|
|
848
|
+
// REMOVE BACKGROUND
|
|
780
849
|
if (options.type === 'none') {
|
|
781
850
|
this.logger.info('Removing background effect');
|
|
782
|
-
this.currentBackgroundOptions = null;
|
|
783
851
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
852
|
+
// Prefer the raw camera stream we already have — its track is
|
|
853
|
+
// the real device track. If it's somehow gone, fall back to
|
|
854
|
+
// getUserMedia using the recorded source.
|
|
855
|
+
let newStream = null;
|
|
856
|
+
const rawTrack = oldRawStream?.getVideoTracks?.()[0];
|
|
857
|
+
if (rawTrack && rawTrack.readyState === 'live') {
|
|
858
|
+
newStream = oldRawStream;
|
|
859
|
+
} else {
|
|
860
|
+
const constraints = this._getVideoConstraints(
|
|
861
|
+
this.cameraSource || {},
|
|
862
|
+
);
|
|
863
|
+
newStream = await navigator.mediaDevices.getUserMedia({
|
|
864
|
+
video: constraints,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
788
867
|
|
|
789
868
|
const newTrack = newStream.getVideoTracks()[0];
|
|
790
|
-
|
|
791
869
|
await this.producers.video.replaceTrack({ track: newTrack });
|
|
792
870
|
|
|
793
|
-
|
|
871
|
+
// Commit new state, then tear down old.
|
|
794
872
|
this.streams.camera = newStream;
|
|
873
|
+
this.rawCameraStream = newStream;
|
|
874
|
+
this.currentBackgroundOptions = null;
|
|
875
|
+
|
|
876
|
+
if (hadBackground) {
|
|
877
|
+
try {
|
|
878
|
+
await this.videoProcessor.cleanup();
|
|
879
|
+
} catch (cleanupErr) {
|
|
880
|
+
this.logger.warn('Processor cleanup after remove failed:', cleanupErr);
|
|
881
|
+
}
|
|
882
|
+
// Stop the processed (canvas) stream's track if it's
|
|
883
|
+
// distinct from the raw stream we just promoted.
|
|
884
|
+
if (oldProcessedStream && oldProcessedStream !== newStream) {
|
|
885
|
+
oldProcessedStream.getTracks().forEach((t) => t.stop());
|
|
886
|
+
}
|
|
887
|
+
}
|
|
795
888
|
|
|
796
889
|
this.logger.info('Background effect removed');
|
|
797
890
|
this.emit('background:changed', { type: 'none' });
|
|
798
|
-
|
|
799
891
|
return newStream;
|
|
800
892
|
}
|
|
801
893
|
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
894
|
+
// APPLY / SWAP BACKGROUND
|
|
895
|
+
//
|
|
896
|
+
// Reuse the raw camera stream if it's still live — that's the
|
|
897
|
+
// common case for switching between backgrounds without doing
|
|
898
|
+
// a second getUserMedia (which is slow and can hit
|
|
899
|
+
// OverconstrainedError because the canvas track has no
|
|
900
|
+
// deviceId).
|
|
901
|
+
let rawStream = oldRawStream;
|
|
902
|
+
const rawTrack = rawStream?.getVideoTracks?.()[0];
|
|
903
|
+
const rawAlive = !!rawTrack && rawTrack.readyState === 'live';
|
|
904
|
+
|
|
905
|
+
if (!rawAlive) {
|
|
906
|
+
const constraints = this._getVideoConstraints(
|
|
907
|
+
this.cameraSource || {},
|
|
908
|
+
);
|
|
909
|
+
rawStream = await navigator.mediaDevices.getUserMedia({
|
|
910
|
+
video: constraints,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
810
913
|
|
|
811
|
-
//
|
|
812
|
-
|
|
914
|
+
// If swapping (background already active) we must tear down the
|
|
915
|
+
// processor before starting a new one — its canvas / offscreen
|
|
916
|
+
// video are single-instance state.
|
|
917
|
+
if (hadBackground) {
|
|
918
|
+
try {
|
|
919
|
+
await this.videoProcessor.cleanup();
|
|
920
|
+
} catch (cleanupErr) {
|
|
921
|
+
this.logger.warn('Processor cleanup before swap failed:', cleanupErr);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
813
924
|
|
|
814
|
-
const
|
|
925
|
+
const processedStream = await this.videoProcessor.applyBackground(
|
|
926
|
+
rawStream,
|
|
927
|
+
options,
|
|
928
|
+
);
|
|
929
|
+
const newTrack = processedStream.getVideoTracks()[0];
|
|
815
930
|
|
|
816
|
-
// Replace track in producer
|
|
817
931
|
await this.producers.video.replaceTrack({ track: newTrack });
|
|
818
932
|
|
|
819
|
-
//
|
|
820
|
-
this.
|
|
821
|
-
this.streams.camera =
|
|
933
|
+
// Commit new state.
|
|
934
|
+
this.currentBackgroundOptions = options;
|
|
935
|
+
this.streams.camera = processedStream;
|
|
936
|
+
this.rawCameraStream = rawStream;
|
|
937
|
+
|
|
938
|
+
// Stop the OLD processed canvas stream's track (it was wrapping
|
|
939
|
+
// the processor we just cleaned up). The raw stream is
|
|
940
|
+
// intentionally preserved — it's still feeding the new processor.
|
|
941
|
+
if (
|
|
942
|
+
hadBackground &&
|
|
943
|
+
oldProcessedStream &&
|
|
944
|
+
oldProcessedStream !== processedStream &&
|
|
945
|
+
oldProcessedStream !== rawStream
|
|
946
|
+
) {
|
|
947
|
+
oldProcessedStream.getTracks().forEach((t) => t.stop());
|
|
948
|
+
}
|
|
822
949
|
|
|
823
950
|
this.logger.info('Background effect updated successfully');
|
|
824
951
|
this.emit('background:changed', options);
|
|
825
952
|
|
|
826
|
-
return
|
|
953
|
+
return processedStream;
|
|
827
954
|
} catch (error) {
|
|
828
955
|
this.logger.error('Failed to update camera background:', error);
|
|
956
|
+
// We did not mutate streams.camera or stop the old track until
|
|
957
|
+
// after the swap succeeded, so the producer is still holding a
|
|
958
|
+
// live track and the user's video keeps flowing.
|
|
829
959
|
throw wrapError(error, 'updateCameraBackground');
|
|
830
960
|
}
|
|
831
961
|
}
|