@stream-io/video-client 1.4.8 → 1.5.0-0
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/CHANGELOG.md +231 -0
- package/dist/index.browser.es.js +1976 -1476
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1974 -1473
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1976 -1476
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +93 -9
- package/dist/src/StreamSfuClient.d.ts +72 -56
- package/dist/src/StreamVideoClient.d.ts +2 -2
- package/dist/src/coordinator/connection/client.d.ts +3 -4
- package/dist/src/coordinator/connection/types.d.ts +5 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +1 -3
- package/dist/src/events/internal.d.ts +4 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
- package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
- package/dist/src/helpers/ensureExhausted.d.ts +1 -0
- package/dist/src/helpers/withResolvers.d.ts +14 -0
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/rpc/createClient.d.ts +2 -0
- package/dist/src/rpc/index.d.ts +1 -0
- package/dist/src/rpc/retryable.d.ts +23 -0
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
- package/dist/src/rtc/Publisher.d.ts +24 -25
- package/dist/src/rtc/Subscriber.d.ts +12 -11
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +46 -2
- package/package.json +5 -5
- package/src/Call.ts +618 -563
- package/src/StreamSfuClient.ts +277 -246
- package/src/StreamVideoClient.ts +15 -16
- package/src/__tests__/Call.test.ts +145 -2
- package/src/__tests__/StreamVideoClient.api.test.ts +168 -0
- package/src/coordinator/connection/client.ts +25 -8
- package/src/coordinator/connection/connection.ts +2 -1
- package/src/coordinator/connection/types.ts +6 -0
- package/src/devices/BrowserPermission.ts +1 -1
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +12 -3
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +10 -13
- package/src/events/__tests__/participant.test.ts +75 -0
- package/src/events/callEventHandlers.ts +4 -7
- package/src/events/internal.ts +20 -3
- package/src/events/mutes.ts +5 -3
- package/src/events/participant.ts +48 -15
- package/src/gen/video/sfu/event/events.ts +451 -8
- package/src/gen/video/sfu/models/models.ts +211 -204
- package/src/helpers/ensureExhausted.ts +5 -0
- package/src/helpers/withResolvers.ts +43 -0
- package/src/logger.ts +3 -1
- package/src/rpc/__tests__/retryable.test.ts +72 -0
- package/src/rpc/createClient.ts +21 -0
- package/src/rpc/index.ts +1 -0
- package/src/rpc/retryable.ts +57 -0
- package/src/rtc/Dispatcher.ts +6 -2
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +127 -163
- package/src/rtc/Subscriber.ts +94 -155
- package/src/rtc/__tests__/Publisher.test.ts +18 -95
- package/src/rtc/__tests__/Subscriber.test.ts +63 -99
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/helpers/rtcConfiguration.ts +11 -0
- package/src/rtc/helpers/tracks.ts +27 -7
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +1 -10
- package/src/stats/SfuStatsReporter.ts +1 -0
- package/src/store/CallState.ts +109 -2
- package/src/store/__tests__/CallState.test.ts +48 -37
- package/dist/src/rtc/flows/join.d.ts +0 -20
- package/src/rtc/flows/join.ts +0 -65
|
@@ -28,9 +28,9 @@ describe('mutes', () => {
|
|
|
28
28
|
// @ts-expect-error partial data
|
|
29
29
|
call.publisher.isPublishing = vi.fn().mockReturnValue(true);
|
|
30
30
|
|
|
31
|
-
vi.spyOn(call, 'stopPublish').mockResolvedValue(undefined);
|
|
32
31
|
vi.spyOn(call.camera, 'disable').mockResolvedValue(undefined);
|
|
33
32
|
vi.spyOn(call.microphone, 'disable').mockResolvedValue(undefined);
|
|
33
|
+
vi.spyOn(call.screenShare, 'disable').mockResolvedValue(undefined);
|
|
34
34
|
|
|
35
35
|
// @ts-ignore
|
|
36
36
|
call.on = (event: string, h) => {
|
|
@@ -56,41 +56,38 @@ describe('mutes', () => {
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
it('should automatically mute only when cause is moderation',
|
|
60
|
-
|
|
59
|
+
it('should automatically mute only when cause is moderation', () => {
|
|
60
|
+
handler!({
|
|
61
61
|
cause: TrackUnpublishReason.PERMISSION_REVOKED,
|
|
62
62
|
type: TrackType.VIDEO,
|
|
63
63
|
sessionId: 'session-id',
|
|
64
64
|
userId: 'user-id',
|
|
65
65
|
});
|
|
66
66
|
expect(call.camera.disable).not.toHaveBeenCalled();
|
|
67
|
-
expect(call.stopPublish).not.toHaveBeenCalledWith(TrackType.VIDEO);
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
it('should handle remote soft video mute',
|
|
71
|
-
|
|
69
|
+
it('should handle remote soft video mute', () => {
|
|
70
|
+
handler!({
|
|
72
71
|
cause: TrackUnpublishReason.MODERATION,
|
|
73
72
|
type: TrackType.VIDEO,
|
|
74
73
|
sessionId: 'session-id',
|
|
75
74
|
userId: 'user-id',
|
|
76
75
|
});
|
|
77
76
|
expect(call.camera.disable).toHaveBeenCalled();
|
|
78
|
-
expect(call.stopPublish).toHaveBeenCalledWith(TrackType.VIDEO);
|
|
79
77
|
});
|
|
80
78
|
|
|
81
|
-
it('should handle remote soft audio mute',
|
|
82
|
-
|
|
79
|
+
it('should handle remote soft audio mute', () => {
|
|
80
|
+
handler!({
|
|
83
81
|
cause: TrackUnpublishReason.MODERATION,
|
|
84
82
|
type: TrackType.AUDIO,
|
|
85
83
|
sessionId: 'session-id',
|
|
86
84
|
userId: 'user-id',
|
|
87
85
|
});
|
|
88
86
|
expect(call.microphone.disable).toHaveBeenCalled();
|
|
89
|
-
expect(call.stopPublish).toHaveBeenCalledWith(TrackType.AUDIO);
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
it('should handle remote soft screenshare mute',
|
|
93
|
-
|
|
89
|
+
it('should handle remote soft screenshare mute', () => {
|
|
90
|
+
handler!({
|
|
94
91
|
cause: TrackUnpublishReason.MODERATION,
|
|
95
92
|
type: TrackType.SCREEN_SHARE,
|
|
96
93
|
sessionId: 'session-id',
|
|
@@ -98,7 +95,7 @@ describe('mutes', () => {
|
|
|
98
95
|
});
|
|
99
96
|
expect(call.camera.disable).not.toHaveBeenCalled();
|
|
100
97
|
expect(call.microphone.disable).not.toHaveBeenCalled();
|
|
101
|
-
expect(call.
|
|
98
|
+
expect(call.screenShare.disable).toHaveBeenCalled();
|
|
102
99
|
});
|
|
103
100
|
});
|
|
104
101
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import '../../rtc/__tests__/mocks/webrtc.mocks';
|
|
1
2
|
import { describe, expect, it } from 'vitest';
|
|
2
3
|
import { CallState } from '../../store';
|
|
3
4
|
import { VisibilityState } from '../../types';
|
|
@@ -75,6 +76,80 @@ describe('Participant events', () => {
|
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
|
|
79
|
+
describe('orphaned tracks reconciliation', () => {
|
|
80
|
+
it('participantJoined should reconcile orphaned tracks if any', () => {
|
|
81
|
+
const state = new CallState();
|
|
82
|
+
const mediaStream = new MediaStream();
|
|
83
|
+
state.registerOrphanedTrack({
|
|
84
|
+
trackLookupPrefix: 'track-lookup-prefix',
|
|
85
|
+
trackType: TrackType.VIDEO,
|
|
86
|
+
track: mediaStream,
|
|
87
|
+
});
|
|
88
|
+
const onParticipantJoined = watchParticipantJoined(state);
|
|
89
|
+
onParticipantJoined({
|
|
90
|
+
// @ts-expect-error incomplete data
|
|
91
|
+
participant: {
|
|
92
|
+
userId: 'user-id',
|
|
93
|
+
sessionId: 'session-id',
|
|
94
|
+
trackLookupPrefix: 'track-lookup-prefix',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const p = state.findParticipantBySessionId('session-id');
|
|
99
|
+
expect(p).toBeDefined();
|
|
100
|
+
expect(p?.videoStream).toBe(mediaStream);
|
|
101
|
+
expect(state.takeOrphanedTracks('track-lookup-prefix')).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('trackPublished should reconcile orphaned tracks if any', () => {
|
|
105
|
+
const state = new CallState();
|
|
106
|
+
const mediaStream = new MediaStream();
|
|
107
|
+
state.registerOrphanedTrack({
|
|
108
|
+
trackLookupPrefix: 'track-lookup-prefix',
|
|
109
|
+
trackType: TrackType.AUDIO,
|
|
110
|
+
track: mediaStream,
|
|
111
|
+
});
|
|
112
|
+
const onTrackPublished = watchTrackPublished(state);
|
|
113
|
+
onTrackPublished({
|
|
114
|
+
// @ts-expect-error incomplete data
|
|
115
|
+
participant: {
|
|
116
|
+
userId: 'user-id',
|
|
117
|
+
sessionId: 'session-id',
|
|
118
|
+
trackLookupPrefix: 'track-lookup-prefix',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const p = state.findParticipantBySessionId('session-id');
|
|
123
|
+
expect(p).toBeDefined();
|
|
124
|
+
expect(p?.audioStream).toBe(mediaStream);
|
|
125
|
+
expect(state.takeOrphanedTracks('track-lookup-prefix')).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('trackUnpublished should reconcile orphaned tracks if any', () => {
|
|
129
|
+
const state = new CallState();
|
|
130
|
+
const mediaStream = new MediaStream();
|
|
131
|
+
state.registerOrphanedTrack({
|
|
132
|
+
trackLookupPrefix: 'track-lookup-prefix',
|
|
133
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
134
|
+
track: mediaStream,
|
|
135
|
+
});
|
|
136
|
+
const onTrackUnPublished = watchTrackUnpublished(state);
|
|
137
|
+
onTrackUnPublished({
|
|
138
|
+
// @ts-expect-error incomplete data
|
|
139
|
+
participant: {
|
|
140
|
+
userId: 'user-id',
|
|
141
|
+
sessionId: 'session-id',
|
|
142
|
+
trackLookupPrefix: 'track-lookup-prefix',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const p = state.findParticipantBySessionId('session-id');
|
|
147
|
+
expect(p).toBeDefined();
|
|
148
|
+
expect(p?.screenShareStream).toBe(mediaStream);
|
|
149
|
+
expect(state.takeOrphanedTracks('track-lookup-prefix')).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
78
153
|
describe('trackPublished', () => {
|
|
79
154
|
it('updates the participant track list', () => {
|
|
80
155
|
const state = new CallState();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Call } from '../Call';
|
|
2
2
|
import { Dispatcher } from '../rtc';
|
|
3
|
-
import { CallState } from '../store';
|
|
4
3
|
import {
|
|
5
4
|
handleRemoteSoftMute,
|
|
6
5
|
watchAudioLevelChanged,
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
watchParticipantLeft,
|
|
18
17
|
watchParticipantUpdated,
|
|
19
18
|
watchPinsUpdated,
|
|
19
|
+
watchSfuCallEnded,
|
|
20
20
|
watchSfuErrorReports,
|
|
21
21
|
watchTrackPublished,
|
|
22
22
|
watchTrackUnpublished,
|
|
@@ -36,16 +36,13 @@ type RingCallEvents = Extract<
|
|
|
36
36
|
* Registers the default event handlers for a call during its lifecycle.
|
|
37
37
|
*
|
|
38
38
|
* @param call the call to register event handlers for.
|
|
39
|
-
* @param state the call state.
|
|
40
39
|
* @param dispatcher the dispatcher.
|
|
41
40
|
*/
|
|
42
|
-
export const registerEventHandlers = (
|
|
43
|
-
call
|
|
44
|
-
state: CallState,
|
|
45
|
-
dispatcher: Dispatcher,
|
|
46
|
-
) => {
|
|
41
|
+
export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
|
|
42
|
+
const state = call.state;
|
|
47
43
|
const eventHandlers = [
|
|
48
44
|
call.on('call.ended', watchCallEnded(call)),
|
|
45
|
+
watchSfuCallEnded(call),
|
|
49
46
|
|
|
50
47
|
watchLiveEnded(dispatcher, call),
|
|
51
48
|
watchSfuErrorReports(dispatcher),
|
package/src/events/internal.ts
CHANGED
|
@@ -3,8 +3,12 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { CallState } from '../store';
|
|
4
4
|
import { StreamVideoParticipantPatches } from '../types';
|
|
5
5
|
import { getLogger } from '../logger';
|
|
6
|
-
import type { PinsChanged } from '../gen/video/sfu/event/events';
|
|
7
|
-
import {
|
|
6
|
+
import type { CallEnded, PinsChanged } from '../gen/video/sfu/event/events';
|
|
7
|
+
import {
|
|
8
|
+
CallEndedReason,
|
|
9
|
+
ErrorCode,
|
|
10
|
+
WebsocketReconnectStrategy,
|
|
11
|
+
} from '../gen/video/sfu/models/models';
|
|
8
12
|
import { OwnCapability } from '../gen/coordinator';
|
|
9
13
|
|
|
10
14
|
const logger = getLogger(['events']);
|
|
@@ -82,9 +86,10 @@ export const watchLiveEnded = (dispatcher: Dispatcher, call: Call) => {
|
|
|
82
86
|
export const watchSfuErrorReports = (dispatcher: Dispatcher) => {
|
|
83
87
|
return dispatcher.on('error', (e) => {
|
|
84
88
|
if (!e.error) return;
|
|
85
|
-
const { error } = e;
|
|
89
|
+
const { error, reconnectStrategy } = e;
|
|
86
90
|
logger('error', 'SFU reported error', {
|
|
87
91
|
code: ErrorCode[error.code],
|
|
92
|
+
reconnectStrategy: WebsocketReconnectStrategy[reconnectStrategy],
|
|
88
93
|
message: error.message,
|
|
89
94
|
shouldRetry: error.shouldRetry,
|
|
90
95
|
});
|
|
@@ -101,3 +106,15 @@ export const watchPinsUpdated = (state: CallState) => {
|
|
|
101
106
|
state.setServerSidePins(pins);
|
|
102
107
|
};
|
|
103
108
|
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Watches for `callEnded` events.
|
|
112
|
+
*/
|
|
113
|
+
export const watchSfuCallEnded = (call: Call) => {
|
|
114
|
+
return call.on('callEnded', (e: CallEnded) => {
|
|
115
|
+
const reason = CallEndedReason[e.reason];
|
|
116
|
+
call.leave({ reason }).catch((err) => {
|
|
117
|
+
logger('error', 'Failed to leave call after call ended by the SFU', err);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
};
|
package/src/events/mutes.ts
CHANGED
|
@@ -27,6 +27,11 @@ export const handleRemoteSoftMute = (call: Call) => {
|
|
|
27
27
|
await call.camera.disable();
|
|
28
28
|
} else if (type === TrackType.AUDIO) {
|
|
29
29
|
await call.microphone.disable();
|
|
30
|
+
} else if (
|
|
31
|
+
type === TrackType.SCREEN_SHARE ||
|
|
32
|
+
type === TrackType.SCREEN_SHARE_AUDIO
|
|
33
|
+
) {
|
|
34
|
+
await call.screenShare.disable();
|
|
30
35
|
} else {
|
|
31
36
|
logger(
|
|
32
37
|
'warn',
|
|
@@ -34,9 +39,6 @@ export const handleRemoteSoftMute = (call: Call) => {
|
|
|
34
39
|
TrackType[type],
|
|
35
40
|
);
|
|
36
41
|
}
|
|
37
|
-
if (call.publisher?.isPublishing(type)) {
|
|
38
|
-
await call.stopPublish(type);
|
|
39
|
-
}
|
|
40
42
|
} catch (error) {
|
|
41
43
|
logger('error', 'Failed to stop publishing', error);
|
|
42
44
|
}
|
|
@@ -5,8 +5,14 @@ import type {
|
|
|
5
5
|
TrackPublished,
|
|
6
6
|
TrackUnpublished,
|
|
7
7
|
} from '../gen/video/sfu/event/events';
|
|
8
|
-
import {
|
|
8
|
+
import type { Participant } from '../gen/video/sfu/models/models';
|
|
9
|
+
import {
|
|
10
|
+
StreamVideoParticipant,
|
|
11
|
+
StreamVideoParticipantPatch,
|
|
12
|
+
VisibilityState,
|
|
13
|
+
} from '../types';
|
|
9
14
|
import { CallState } from '../store';
|
|
15
|
+
import { trackTypeToParticipantStreamKey } from '../rtc/helpers/tracks';
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* An event responder which handles the `participantJoined` event.
|
|
@@ -19,21 +25,23 @@ export const watchParticipantJoined = (state: CallState) => {
|
|
|
19
25
|
// potential duplicate events from the SFU.
|
|
20
26
|
//
|
|
21
27
|
// Although the SFU should not send duplicate events, we have seen
|
|
22
|
-
// some race conditions in the past during the `join-flow
|
|
23
|
-
//
|
|
28
|
+
// some race conditions in the past during the `join-flow`.
|
|
29
|
+
// The SFU would send participant info as part of the `join`
|
|
24
30
|
// response and then follow up with a `participantJoined` event for
|
|
25
31
|
// already announced participants.
|
|
32
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
26
33
|
state.updateOrAddParticipant(
|
|
27
34
|
participant.sessionId,
|
|
28
|
-
Object.assign<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
Object.assign<
|
|
36
|
+
StreamVideoParticipant,
|
|
37
|
+
StreamVideoParticipantPatch | undefined,
|
|
38
|
+
Partial<StreamVideoParticipant>
|
|
39
|
+
>(participant, orphanedTracks, {
|
|
40
|
+
viewportVisibilityState: {
|
|
41
|
+
videoTrack: VisibilityState.UNKNOWN,
|
|
42
|
+
screenShareTrack: VisibilityState.UNKNOWN,
|
|
35
43
|
},
|
|
36
|
-
),
|
|
44
|
+
}),
|
|
37
45
|
);
|
|
38
46
|
};
|
|
39
47
|
};
|
|
@@ -69,12 +77,14 @@ export const watchParticipantUpdated = (state: CallState) => {
|
|
|
69
77
|
*/
|
|
70
78
|
export const watchTrackPublished = (state: CallState) => {
|
|
71
79
|
return function onTrackPublished(e: TrackPublished) {
|
|
72
|
-
const { type, sessionId
|
|
80
|
+
const { type, sessionId } = e;
|
|
73
81
|
// An optimization for large calls.
|
|
74
82
|
// After a certain threshold, the SFU would stop emitting `participantJoined`
|
|
75
83
|
// events, and instead, it would only provide the participant's information
|
|
76
84
|
// once they start publishing a track.
|
|
77
|
-
if (participant) {
|
|
85
|
+
if (e.participant) {
|
|
86
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
87
|
+
const participant = Object.assign(e.participant, orphanedTracks);
|
|
78
88
|
state.updateOrAddParticipant(sessionId, participant);
|
|
79
89
|
} else {
|
|
80
90
|
state.updateParticipant(sessionId, (p) => ({
|
|
@@ -90,9 +100,11 @@ export const watchTrackPublished = (state: CallState) => {
|
|
|
90
100
|
*/
|
|
91
101
|
export const watchTrackUnpublished = (state: CallState) => {
|
|
92
102
|
return function onTrackUnpublished(e: TrackUnpublished) {
|
|
93
|
-
const { type, sessionId
|
|
103
|
+
const { type, sessionId } = e;
|
|
94
104
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
95
|
-
if (participant) {
|
|
105
|
+
if (e.participant) {
|
|
106
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
107
|
+
const participant = Object.assign(e.participant, orphanedTracks);
|
|
96
108
|
state.updateOrAddParticipant(sessionId, participant);
|
|
97
109
|
} else {
|
|
98
110
|
state.updateParticipant(sessionId, (p) => ({
|
|
@@ -103,3 +115,24 @@ export const watchTrackUnpublished = (state: CallState) => {
|
|
|
103
115
|
};
|
|
104
116
|
|
|
105
117
|
const unique = <T>(v: T, i: number, arr: T[]) => arr.indexOf(v) === i;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reconciles orphaned tracks (if any) for the given participant.
|
|
121
|
+
*
|
|
122
|
+
* @param state the call state.
|
|
123
|
+
* @param participant the participant.
|
|
124
|
+
*/
|
|
125
|
+
const reconcileOrphanedTracks = (
|
|
126
|
+
state: CallState,
|
|
127
|
+
participant: Participant,
|
|
128
|
+
): StreamVideoParticipantPatch | undefined => {
|
|
129
|
+
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
130
|
+
if (!orphanTracks.length) return;
|
|
131
|
+
const reconciledTracks: StreamVideoParticipantPatch = {};
|
|
132
|
+
for (const orphan of orphanTracks) {
|
|
133
|
+
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
134
|
+
if (!key) continue;
|
|
135
|
+
reconciledTracks[key] = orphan.track;
|
|
136
|
+
}
|
|
137
|
+
return reconciledTracks;
|
|
138
|
+
};
|