@unboundcx/video-sdk-client 2.0.2 → 2.0.4
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/VideoMeetingClient.js +23 -0
- package/managers/LocalMediaManager.js +338 -47
- package/package.json +1 -1
package/VideoMeetingClient.js
CHANGED
|
@@ -206,6 +206,14 @@ export class VideoMeetingClient extends EventEmitter {
|
|
|
206
206
|
this.emit("device:changed", data);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
this.localMedia.on("camera:degraded", (data) => {
|
|
210
|
+
this.emit("camera:degraded", data);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
this.localMedia.on("camera:restarted", (data) => {
|
|
214
|
+
this.emit("camera:restarted", data);
|
|
215
|
+
});
|
|
216
|
+
|
|
209
217
|
// Remote media events
|
|
210
218
|
this.remoteMedia.on("participant:added", (data) => {
|
|
211
219
|
this.emit("participant:joined", data);
|
|
@@ -882,6 +890,21 @@ export class VideoMeetingClient extends EventEmitter {
|
|
|
882
890
|
return await this.localMedia.updateCameraBackground(options);
|
|
883
891
|
}
|
|
884
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Restart the local camera in place. Use after a 'camera:degraded'
|
|
895
|
+
* event, or as a user-triggered recovery from a frozen video state.
|
|
896
|
+
* Re-acquires the camera, reapplies any active background, and
|
|
897
|
+
* replaces the track on the existing producer (no renegotiation).
|
|
898
|
+
*
|
|
899
|
+
* @param {Object} [options] - Optional deviceId/facingMode override.
|
|
900
|
+
* If omitted, reuses the last-known camera source.
|
|
901
|
+
* @returns {Promise<MediaStream>}
|
|
902
|
+
*/
|
|
903
|
+
async restartCamera(options) {
|
|
904
|
+
this._ensureInRoom();
|
|
905
|
+
return await this.localMedia.restartCamera(options);
|
|
906
|
+
}
|
|
907
|
+
|
|
885
908
|
/**
|
|
886
909
|
* Mute camera
|
|
887
910
|
* @returns {Promise<void>}
|
|
@@ -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);
|
|
@@ -207,12 +229,11 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
207
229
|
// Store stream
|
|
208
230
|
this.streams.camera = stream;
|
|
209
231
|
|
|
210
|
-
//
|
|
211
|
-
track.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
});
|
|
232
|
+
// Attach health watchers (ended / muted) on the producer's
|
|
233
|
+
// current track. _watchCameraTrack handles both ended and
|
|
234
|
+
// browser-side mute (which is distinct from user mute and
|
|
235
|
+
// indicates the OS took the camera, USB unplug, etc.).
|
|
236
|
+
this._watchCameraTrack(track);
|
|
216
237
|
|
|
217
238
|
// Publish to mediasoup
|
|
218
239
|
const producer = await this.mediasoup.produce(track, {
|
|
@@ -442,6 +463,22 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
442
463
|
// Stop all tracks
|
|
443
464
|
this.streams.camera.getTracks().forEach((track) => track.stop());
|
|
444
465
|
|
|
466
|
+
// Also stop the raw camera stream if it's distinct (background was
|
|
467
|
+
// active — processed/canvas stream wraps the raw camera stream).
|
|
468
|
+
if (this.rawCameraStream && this.rawCameraStream !== this.streams.camera) {
|
|
469
|
+
this.rawCameraStream.getTracks().forEach((t) => t.stop());
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Tear down processor if a background was active.
|
|
473
|
+
if (this.currentBackgroundOptions) {
|
|
474
|
+
try {
|
|
475
|
+
await this.videoProcessor.cleanup();
|
|
476
|
+
} catch (cleanupErr) {
|
|
477
|
+
this.logger.warn('Processor cleanup during stopCamera failed:', cleanupErr);
|
|
478
|
+
}
|
|
479
|
+
this.currentBackgroundOptions = null;
|
|
480
|
+
}
|
|
481
|
+
|
|
445
482
|
// Close producer
|
|
446
483
|
if (this.producers.video) {
|
|
447
484
|
await this.mediasoup.closeProducer('video');
|
|
@@ -449,6 +486,8 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
449
486
|
}
|
|
450
487
|
|
|
451
488
|
this.streams.camera = null;
|
|
489
|
+
this.rawCameraStream = null;
|
|
490
|
+
this.cameraSource = null;
|
|
452
491
|
|
|
453
492
|
this.emit('stream:removed', { type: 'camera' });
|
|
454
493
|
}
|
|
@@ -687,18 +726,28 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
687
726
|
throw new Error('No active camera to change');
|
|
688
727
|
}
|
|
689
728
|
|
|
729
|
+
const oldProcessedStream = this.streams.camera;
|
|
730
|
+
const oldRawStream = this.rawCameraStream;
|
|
731
|
+
|
|
690
732
|
try {
|
|
691
|
-
// Get new stream
|
|
733
|
+
// Get new stream (always fresh — camera device is changing).
|
|
692
734
|
const constraints = this._getVideoConstraints(options);
|
|
693
|
-
|
|
735
|
+
const rawStream = await navigator.mediaDevices.getUserMedia({
|
|
694
736
|
video: constraints,
|
|
695
737
|
});
|
|
696
738
|
|
|
739
|
+
let stream = rawStream;
|
|
740
|
+
|
|
697
741
|
// Reapply background effect if it was active
|
|
698
742
|
if (this.currentBackgroundOptions) {
|
|
699
743
|
this.logger.info('Reapplying background effect after camera change');
|
|
744
|
+
try {
|
|
745
|
+
await this.videoProcessor.cleanup();
|
|
746
|
+
} catch (cleanupErr) {
|
|
747
|
+
this.logger.warn('Processor cleanup during camera change failed:', cleanupErr);
|
|
748
|
+
}
|
|
700
749
|
stream = await this.videoProcessor.applyBackground(
|
|
701
|
-
|
|
750
|
+
rawStream,
|
|
702
751
|
this.currentBackgroundOptions,
|
|
703
752
|
);
|
|
704
753
|
}
|
|
@@ -707,6 +756,7 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
707
756
|
|
|
708
757
|
// Replace track in producer
|
|
709
758
|
await this.producers.video.replaceTrack({ track: newTrack });
|
|
759
|
+
this._watchCameraTrack(newTrack);
|
|
710
760
|
|
|
711
761
|
// If producer was paused (camera muted), resume it
|
|
712
762
|
// When user manually changes camera, they likely want to use it
|
|
@@ -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,129 @@ 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 });
|
|
870
|
+
this._watchCameraTrack(newTrack);
|
|
792
871
|
|
|
793
|
-
|
|
872
|
+
// Commit new state, then tear down old.
|
|
794
873
|
this.streams.camera = newStream;
|
|
874
|
+
this.rawCameraStream = newStream;
|
|
875
|
+
this.currentBackgroundOptions = null;
|
|
876
|
+
|
|
877
|
+
if (hadBackground) {
|
|
878
|
+
try {
|
|
879
|
+
await this.videoProcessor.cleanup();
|
|
880
|
+
} catch (cleanupErr) {
|
|
881
|
+
this.logger.warn('Processor cleanup after remove failed:', cleanupErr);
|
|
882
|
+
}
|
|
883
|
+
// Stop the processed (canvas) stream's track if it's
|
|
884
|
+
// distinct from the raw stream we just promoted.
|
|
885
|
+
if (oldProcessedStream && oldProcessedStream !== newStream) {
|
|
886
|
+
oldProcessedStream.getTracks().forEach((t) => t.stop());
|
|
887
|
+
}
|
|
888
|
+
}
|
|
795
889
|
|
|
796
890
|
this.logger.info('Background effect removed');
|
|
797
891
|
this.emit('background:changed', { type: 'none' });
|
|
798
|
-
|
|
799
892
|
return newStream;
|
|
800
893
|
}
|
|
801
894
|
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
895
|
+
// APPLY / SWAP BACKGROUND
|
|
896
|
+
//
|
|
897
|
+
// Reuse the raw camera stream if it's still live — that's the
|
|
898
|
+
// common case for switching between backgrounds without doing
|
|
899
|
+
// a second getUserMedia (which is slow and can hit
|
|
900
|
+
// OverconstrainedError because the canvas track has no
|
|
901
|
+
// deviceId).
|
|
902
|
+
let rawStream = oldRawStream;
|
|
903
|
+
const rawTrack = rawStream?.getVideoTracks?.()[0];
|
|
904
|
+
const rawAlive = !!rawTrack && rawTrack.readyState === 'live';
|
|
905
|
+
|
|
906
|
+
if (!rawAlive) {
|
|
907
|
+
const constraints = this._getVideoConstraints(
|
|
908
|
+
this.cameraSource || {},
|
|
909
|
+
);
|
|
910
|
+
rawStream = await navigator.mediaDevices.getUserMedia({
|
|
911
|
+
video: constraints,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
810
914
|
|
|
811
|
-
//
|
|
812
|
-
|
|
915
|
+
// If swapping (background already active) we must tear down the
|
|
916
|
+
// processor before starting a new one — its canvas / offscreen
|
|
917
|
+
// video are single-instance state.
|
|
918
|
+
if (hadBackground) {
|
|
919
|
+
try {
|
|
920
|
+
await this.videoProcessor.cleanup();
|
|
921
|
+
} catch (cleanupErr) {
|
|
922
|
+
this.logger.warn('Processor cleanup before swap failed:', cleanupErr);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
813
925
|
|
|
814
|
-
const
|
|
926
|
+
const processedStream = await this.videoProcessor.applyBackground(
|
|
927
|
+
rawStream,
|
|
928
|
+
options,
|
|
929
|
+
);
|
|
930
|
+
const newTrack = processedStream.getVideoTracks()[0];
|
|
815
931
|
|
|
816
|
-
// Replace track in producer
|
|
817
932
|
await this.producers.video.replaceTrack({ track: newTrack });
|
|
933
|
+
this._watchCameraTrack(newTrack);
|
|
818
934
|
|
|
819
|
-
//
|
|
820
|
-
this.
|
|
821
|
-
this.streams.camera =
|
|
935
|
+
// Commit new state.
|
|
936
|
+
this.currentBackgroundOptions = options;
|
|
937
|
+
this.streams.camera = processedStream;
|
|
938
|
+
this.rawCameraStream = rawStream;
|
|
939
|
+
|
|
940
|
+
// Stop the OLD processed canvas stream's track (it was wrapping
|
|
941
|
+
// the processor we just cleaned up). The raw stream is
|
|
942
|
+
// intentionally preserved — it's still feeding the new processor.
|
|
943
|
+
if (
|
|
944
|
+
hadBackground &&
|
|
945
|
+
oldProcessedStream &&
|
|
946
|
+
oldProcessedStream !== processedStream &&
|
|
947
|
+
oldProcessedStream !== rawStream
|
|
948
|
+
) {
|
|
949
|
+
oldProcessedStream.getTracks().forEach((t) => t.stop());
|
|
950
|
+
}
|
|
822
951
|
|
|
823
952
|
this.logger.info('Background effect updated successfully');
|
|
824
953
|
this.emit('background:changed', options);
|
|
825
954
|
|
|
826
|
-
return
|
|
955
|
+
return processedStream;
|
|
827
956
|
} catch (error) {
|
|
828
957
|
this.logger.error('Failed to update camera background:', error);
|
|
958
|
+
// We did not mutate streams.camera or stop the old track until
|
|
959
|
+
// after the swap succeeded, so the producer is still holding a
|
|
960
|
+
// live track and the user's video keeps flowing.
|
|
829
961
|
throw wrapError(error, 'updateCameraBackground');
|
|
830
962
|
}
|
|
831
963
|
}
|
|
@@ -1036,6 +1168,165 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
1036
1168
|
return this.muteState.microphone;
|
|
1037
1169
|
}
|
|
1038
1170
|
|
|
1171
|
+
/**
|
|
1172
|
+
* Attach health watchers to the producer's current video track.
|
|
1173
|
+
*
|
|
1174
|
+
* Emits:
|
|
1175
|
+
* - 'track:ended' { type: 'camera' } — kept for back-compat
|
|
1176
|
+
* - 'camera:degraded' { reason, track } — track is no longer
|
|
1177
|
+
* delivering frames (ended, OS-level muted, etc.). Remotes
|
|
1178
|
+
* will see a frozen frame until the producer is paused or the
|
|
1179
|
+
* track is replaced via restartCamera().
|
|
1180
|
+
*
|
|
1181
|
+
* The 'mute' event here is the browser's own muting (camera taken
|
|
1182
|
+
* over by another app, USB unplug, OS privacy switch) — distinct
|
|
1183
|
+
* from the user pressing our mute button.
|
|
1184
|
+
*/
|
|
1185
|
+
_watchCameraTrack(track) {
|
|
1186
|
+
if (!track) return;
|
|
1187
|
+
// Stash so subsequent watchers can replace listeners cleanly.
|
|
1188
|
+
if (this._watchedCameraTrack === track) return;
|
|
1189
|
+
this._watchedCameraTrack = track;
|
|
1190
|
+
|
|
1191
|
+
const onEnded = () => {
|
|
1192
|
+
this.logger.warn('Camera track ended');
|
|
1193
|
+
this.emit('track:ended', { type: 'camera' });
|
|
1194
|
+
if (this.streams.camera) this.streams.camera = null;
|
|
1195
|
+
this.emit('camera:degraded', { reason: 'ended', track });
|
|
1196
|
+
};
|
|
1197
|
+
const onMute = () => {
|
|
1198
|
+
this.logger.warn('Camera track muted (browser-side)');
|
|
1199
|
+
this.emit('camera:degraded', { reason: 'muted', track });
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
track.addEventListener('ended', onEnded);
|
|
1203
|
+
track.addEventListener('mute', onMute);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Restart the local camera in place.
|
|
1208
|
+
*
|
|
1209
|
+
* Re-acquires the camera via `cameraSource` (or any caller-supplied
|
|
1210
|
+
* deviceId/facingMode), reapplies the current background if one was
|
|
1211
|
+
* active, and `replaceTrack`s the live producer. Use this when
|
|
1212
|
+
* 'camera:degraded' has fired or the user manually clicks "Restart
|
|
1213
|
+
* camera" after a freeze.
|
|
1214
|
+
*
|
|
1215
|
+
* Does NOT change isCameraMuted. If the producer was paused on
|
|
1216
|
+
* degradation, the caller is responsible for resuming it once the
|
|
1217
|
+
* new track is in place (we resume here automatically for ergonomics).
|
|
1218
|
+
*
|
|
1219
|
+
* @param {Object} [options] - Optional deviceId/facingMode override.
|
|
1220
|
+
* @returns {Promise<MediaStream>} New camera stream.
|
|
1221
|
+
*/
|
|
1222
|
+
async restartCamera(options = {}) {
|
|
1223
|
+
this.logger.info('Restarting camera', options);
|
|
1224
|
+
|
|
1225
|
+
if (!this.producers.video) {
|
|
1226
|
+
throw new Error('No active camera producer to restart');
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const source =
|
|
1230
|
+
Object.keys(options).length > 0 ? options : this.cameraSource || {};
|
|
1231
|
+
|
|
1232
|
+
const oldProcessedStream = this.streams.camera;
|
|
1233
|
+
const oldRawStream = this.rawCameraStream;
|
|
1234
|
+
|
|
1235
|
+
try {
|
|
1236
|
+
const constraints = this._getVideoConstraints(source);
|
|
1237
|
+
let rawStream;
|
|
1238
|
+
try {
|
|
1239
|
+
rawStream = await navigator.mediaDevices.getUserMedia({
|
|
1240
|
+
video: constraints,
|
|
1241
|
+
});
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
if (
|
|
1244
|
+
err.name === 'OverconstrainedError' ||
|
|
1245
|
+
err.name === 'ConstraintNotSatisfiedError'
|
|
1246
|
+
) {
|
|
1247
|
+
this.logger.warn(
|
|
1248
|
+
'Restart hit constraints, retrying with ideal-only:',
|
|
1249
|
+
err.constraint,
|
|
1250
|
+
);
|
|
1251
|
+
const fallback = {
|
|
1252
|
+
...(source.deviceId ? { deviceId: { exact: source.deviceId } } : {}),
|
|
1253
|
+
...(source.facingMode
|
|
1254
|
+
? { facingMode: { exact: source.facingMode } }
|
|
1255
|
+
: {}),
|
|
1256
|
+
width: { ideal: constraints.width?.ideal },
|
|
1257
|
+
height: { ideal: constraints.height?.ideal },
|
|
1258
|
+
frameRate: constraints.frameRate,
|
|
1259
|
+
};
|
|
1260
|
+
rawStream = await navigator.mediaDevices.getUserMedia({
|
|
1261
|
+
video: fallback,
|
|
1262
|
+
});
|
|
1263
|
+
} else {
|
|
1264
|
+
throw err;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
let stream = rawStream;
|
|
1269
|
+
if (this.currentBackgroundOptions) {
|
|
1270
|
+
try {
|
|
1271
|
+
await this.videoProcessor.cleanup();
|
|
1272
|
+
} catch (cleanupErr) {
|
|
1273
|
+
this.logger.warn('Processor cleanup during restart failed:', cleanupErr);
|
|
1274
|
+
}
|
|
1275
|
+
stream = await this.videoProcessor.applyBackground(
|
|
1276
|
+
rawStream,
|
|
1277
|
+
this.currentBackgroundOptions,
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const newTrack = stream.getVideoTracks()[0];
|
|
1282
|
+
await this.producers.video.replaceTrack({ track: newTrack });
|
|
1283
|
+
|
|
1284
|
+
// If we paused the producer when degradation was detected,
|
|
1285
|
+
// resume now that a healthy track is attached. Don't touch
|
|
1286
|
+
// the user's mute state.
|
|
1287
|
+
if (this.producers.video.paused && !this.muteState.camera) {
|
|
1288
|
+
try {
|
|
1289
|
+
this.producers.video.resume();
|
|
1290
|
+
} catch (resumeErr) {
|
|
1291
|
+
this.logger.warn('Producer resume after restart failed:', resumeErr);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Commit + stop old streams.
|
|
1296
|
+
this.streams.camera = stream;
|
|
1297
|
+
this.rawCameraStream = rawStream;
|
|
1298
|
+
if (source.facingMode) {
|
|
1299
|
+
this.cameraSource = { facingMode: source.facingMode };
|
|
1300
|
+
} else {
|
|
1301
|
+
const trackDeviceId =
|
|
1302
|
+
rawStream.getVideoTracks()[0]?.getSettings().deviceId ||
|
|
1303
|
+
source.deviceId ||
|
|
1304
|
+
null;
|
|
1305
|
+
this.cameraSource = trackDeviceId ? { deviceId: trackDeviceId } : null;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (oldRawStream && oldRawStream !== rawStream) {
|
|
1309
|
+
oldRawStream.getTracks().forEach((t) => t.stop());
|
|
1310
|
+
}
|
|
1311
|
+
if (
|
|
1312
|
+
oldProcessedStream &&
|
|
1313
|
+
oldProcessedStream !== oldRawStream &&
|
|
1314
|
+
oldProcessedStream !== stream
|
|
1315
|
+
) {
|
|
1316
|
+
oldProcessedStream.getTracks().forEach((t) => t.stop());
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
this._watchCameraTrack(newTrack);
|
|
1320
|
+
this.emit('camera:restarted', { stream });
|
|
1321
|
+
this.logger.info('Camera restarted successfully');
|
|
1322
|
+
|
|
1323
|
+
return stream;
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
this.logger.error('Failed to restart camera:', error);
|
|
1326
|
+
throw wrapError(error, 'restartCamera');
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1039
1330
|
/**
|
|
1040
1331
|
* Clean up all local streams
|
|
1041
1332
|
*/
|