@stream-io/video-client 0.3.2 → 0.3.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +77 -56
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +77 -56
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +77 -56
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +2 -1
  9. package/dist/src/coordinator/connection/client.d.ts +9 -4
  10. package/dist/src/devices/CameraManager.d.ts +3 -9
  11. package/dist/src/devices/InputMediaDeviceManager.d.ts +11 -5
  12. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +6 -1
  13. package/dist/src/devices/MicrophoneManager.d.ts +3 -9
  14. package/dist/src/devices/MicrophoneManagerState.d.ts +1 -0
  15. package/dist/src/rtc/Publisher.d.ts +2 -1
  16. package/dist/version.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/src/Call.ts +3 -2
  19. package/src/StreamVideoClient.ts +1 -10
  20. package/src/__tests__/StreamVideoClient.test.ts +13 -0
  21. package/src/coordinator/connection/client.ts +28 -0
  22. package/src/devices/CameraManager.ts +8 -17
  23. package/src/devices/CameraManagerState.ts +1 -1
  24. package/src/devices/InputMediaDeviceManager.ts +39 -17
  25. package/src/devices/InputMediaDeviceManagerState.ts +12 -2
  26. package/src/devices/MicrophoneManager.ts +8 -17
  27. package/src/devices/MicrophoneManagerState.ts +4 -0
  28. package/src/devices/__tests__/CameraManager.test.ts +4 -14
  29. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +48 -5
  30. package/src/devices/__tests__/MicrophoneManager.test.ts +8 -7
  31. package/src/rtc/Publisher.ts +8 -2
  32. package/src/rtc/__tests__/Publisher.test.ts +67 -1
@@ -7,6 +7,14 @@ import { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
7
7
  import { InputMediaDeviceManager } from '../InputMediaDeviceManager';
8
8
  import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
9
9
  import { of } from 'rxjs';
10
+ import { disposeOfMediaStream } from '../devices';
11
+
12
+ vi.mock('../devices.ts', () => {
13
+ console.log('MOCKING devices');
14
+ return {
15
+ disposeOfMediaStream: vi.fn(),
16
+ };
17
+ });
10
18
 
11
19
  vi.mock('../../Call.ts', () => {
12
20
  console.log('MOCKING Call');
@@ -24,8 +32,8 @@ class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMedia
24
32
  public getStream = vi.fn(() => Promise.resolve(mockVideoStream()));
25
33
  public publishStream = vi.fn();
26
34
  public stopPublishStream = vi.fn();
27
- public pause = vi.fn();
28
- public resume = vi.fn();
35
+ public muteTracks = vi.fn();
36
+ public unmuteTracks = vi.fn();
29
37
 
30
38
  constructor(call: Call) {
31
39
  super(call, new TestInputMediaDeviceManagerState());
@@ -92,14 +100,14 @@ describe('InputMediaDeviceManager.test', () => {
92
100
  expect(manager.state.status).toBe('disabled');
93
101
  });
94
102
 
95
- it('disable camera - after joined to call', async () => {
103
+ it('disable device - after joined to call', async () => {
96
104
  // @ts-expect-error
97
105
  manager['call'].state.callingState = CallingState.JOINED;
98
106
  await manager.enable();
99
107
 
100
108
  await manager.disable();
101
109
 
102
- expect(manager.stopPublishStream).toHaveBeenCalledWith();
110
+ expect(manager.stopPublishStream).toHaveBeenCalledWith(true);
103
111
  });
104
112
 
105
113
  it('toggle device', async () => {
@@ -124,6 +132,16 @@ describe('InputMediaDeviceManager.test', () => {
124
132
  expect(manager.publishStream).not.toHaveBeenCalled();
125
133
  });
126
134
 
135
+ it('select device when status is enabled', async () => {
136
+ await manager.enable();
137
+ const prevStream = manager.state.mediaStream;
138
+
139
+ const deviceId = mockVideoDevices[1].deviceId;
140
+ await manager.select(deviceId);
141
+
142
+ expect(disposeOfMediaStream).toHaveBeenCalledWith(prevStream);
143
+ });
144
+
127
145
  it('select device when status is enabled and in call', async () => {
128
146
  // @ts-expect-error
129
147
  manager['call'].state.callingState = CallingState.JOINED;
@@ -132,7 +150,7 @@ describe('InputMediaDeviceManager.test', () => {
132
150
  const deviceId = mockVideoDevices[1].deviceId;
133
151
  await manager.select(deviceId);
134
152
 
135
- expect(manager.stopPublishStream).toHaveBeenCalledWith();
153
+ expect(manager.stopPublishStream).toHaveBeenCalledWith(true);
136
154
  expect(manager.getStream).toHaveBeenCalledWith({
137
155
  deviceId,
138
156
  });
@@ -152,6 +170,31 @@ describe('InputMediaDeviceManager.test', () => {
152
170
  expect(spy.mock.calls.length).toBe(1);
153
171
  });
154
172
 
173
+ it('should resume previously enabled state', async () => {
174
+ vi.spyOn(manager, 'enable');
175
+
176
+ await manager.enable();
177
+
178
+ expect(manager.enable).toHaveBeenCalledTimes(1);
179
+
180
+ await manager.disable();
181
+ await manager.resume();
182
+
183
+ expect(manager.enable).toHaveBeenCalledTimes(2);
184
+ });
185
+
186
+ it(`shouldn't resume if previous state is disabled`, async () => {
187
+ vi.spyOn(manager, 'enable');
188
+
189
+ await manager.disable();
190
+
191
+ expect(manager.enable).not.toHaveBeenCalled();
192
+
193
+ await manager.resume();
194
+
195
+ expect(manager.enable).not.toHaveBeenCalled();
196
+ });
197
+
155
198
  afterEach(() => {
156
199
  vi.clearAllMocks();
157
200
  vi.resetModules();
@@ -81,19 +81,20 @@ describe('MicrophoneManager', () => {
81
81
 
82
82
  await manager.disable();
83
83
 
84
- expect(manager['call'].stopPublish).toHaveBeenCalledWith(TrackType.AUDIO);
84
+ expect(manager['call'].stopPublish).toHaveBeenCalledWith(
85
+ TrackType.AUDIO,
86
+ false,
87
+ );
85
88
  });
86
89
 
87
- it('should pause and resume tracks', async () => {
90
+ it('disable-enable mic should set track.enabled', async () => {
88
91
  await manager.enable();
89
92
 
90
- manager.pause();
91
-
92
- expect(manager.state.mediaStream?.getAudioTracks()[0].enabled).toBe(false);
93
+ expect(manager.state.mediaStream!.getAudioTracks()[0].enabled).toBe(true);
93
94
 
94
- manager.resume();
95
+ await manager.disable();
95
96
 
96
- expect(manager.state.mediaStream?.getAudioTracks()[0].enabled).toBe(true);
97
+ expect(manager.state.mediaStream!.getAudioTracks()[0].enabled).toBe(false);
97
98
  });
98
99
 
99
100
  afterEach(() => {
@@ -283,6 +283,9 @@ export class Publisher {
283
283
  previousTrack.removeEventListener('ended', handleTrackEnded);
284
284
  track.addEventListener('ended', handleTrackEnded);
285
285
  }
286
+ if (!track.enabled) {
287
+ track.enabled = true;
288
+ }
286
289
  await transceiver.sender.replaceTrack(track);
287
290
  }
288
291
 
@@ -298,8 +301,9 @@ export class Publisher {
298
301
  * Stops publishing the given track type to the SFU, if it is currently being published.
299
302
  * Underlying track will be stopped and removed from the publisher.
300
303
  * @param trackType the track type to unpublish.
304
+ * @param stopTrack specifies whether track should be stopped or just disabled
301
305
  */
302
- unpublishStream = async (trackType: TrackType) => {
306
+ unpublishStream = async (trackType: TrackType, stopTrack: boolean) => {
303
307
  const transceiver = this.pc
304
308
  .getTransceivers()
305
309
  .find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
@@ -308,7 +312,9 @@ export class Publisher {
308
312
  transceiver.sender.track &&
309
313
  transceiver.sender.track.readyState === 'live'
310
314
  ) {
311
- transceiver.sender.track.stop();
315
+ stopTrack
316
+ ? transceiver.sender.track.stop()
317
+ : (transceiver.sender.track.enabled = false);
312
318
  return this.notifyTrackMuteStateChanged(
313
319
  undefined,
314
320
  transceiver.sender.track,
@@ -139,7 +139,7 @@ describe('Publisher', () => {
139
139
  expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id-2');
140
140
 
141
141
  // stop publishing
142
- await publisher.unpublishStream(TrackType.VIDEO);
142
+ await publisher.unpublishStream(TrackType.VIDEO, true);
143
143
  expect(newTrack.stop).toHaveBeenCalled();
144
144
  expect(state.localParticipant?.publishedTracks).not.toContain(
145
145
  TrackType.VIDEO,
@@ -147,6 +147,72 @@ describe('Publisher', () => {
147
147
  expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id-2');
148
148
  });
149
149
 
150
+ it('can publish and un-pubish with just enabling and disabling tracks', async () => {
151
+ const mediaStream = new MediaStream();
152
+ const track = new MediaStreamTrack();
153
+ mediaStream.addTrack(track);
154
+
155
+ state.setParticipants([
156
+ // @ts-ignore
157
+ {
158
+ isLocalParticipant: true,
159
+ userId: 'test-user-id',
160
+ sessionId: sessionId,
161
+ publishedTracks: [],
162
+ },
163
+ ]);
164
+
165
+ vi.spyOn(track, 'getSettings').mockReturnValue({
166
+ width: 640,
167
+ height: 480,
168
+ deviceId: 'test-device-id',
169
+ });
170
+
171
+ const transceiver = new RTCRtpTransceiver();
172
+ vi.spyOn(transceiver.sender, 'track', 'get').mockReturnValue(track);
173
+ vi.spyOn(publisher['pc'], 'addTransceiver').mockReturnValue(transceiver);
174
+ vi.spyOn(publisher['pc'], 'getTransceivers').mockReturnValue([transceiver]);
175
+
176
+ sfuClient.updateMuteState = vi.fn();
177
+
178
+ // initial publish
179
+ await publisher.publishStream(mediaStream, track, TrackType.VIDEO);
180
+
181
+ expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id');
182
+ expect(state.localParticipant?.publishedTracks).toContain(TrackType.VIDEO);
183
+ expect(state.localParticipant?.videoStream).toEqual(mediaStream);
184
+ expect(transceiver.setCodecPreferences).toHaveBeenCalled();
185
+ expect(sfuClient.updateMuteState).toHaveBeenCalledWith(
186
+ TrackType.VIDEO,
187
+ false,
188
+ );
189
+
190
+ expect(track.addEventListener).toHaveBeenCalledWith(
191
+ 'ended',
192
+ expect.any(Function),
193
+ );
194
+
195
+ // stop publishing
196
+ await publisher.unpublishStream(TrackType.VIDEO, false);
197
+ expect(track.stop).not.toHaveBeenCalled();
198
+ expect(track.enabled).toBe(false);
199
+ expect(state.localParticipant?.publishedTracks).not.toContain(
200
+ TrackType.VIDEO,
201
+ );
202
+ expect(state.localParticipant?.videoStream).toBeUndefined();
203
+
204
+ const addEventListenerSpy = vi.spyOn(track, 'addEventListener');
205
+ const removeEventListenerSpy = vi.spyOn(track, 'removeEventListener');
206
+
207
+ // start publish again
208
+ await publisher.publishStream(mediaStream, track, TrackType.VIDEO);
209
+
210
+ expect(track.enabled).toBe(true);
211
+ // republishing the same stream should use the previously registered event handlers
212
+ expect(removeEventListenerSpy).not.toHaveBeenCalled();
213
+ expect(addEventListenerSpy).not.toHaveBeenCalled();
214
+ });
215
+
150
216
  describe('Publisher migration', () => {
151
217
  it('should update the sfuClient and peer connection configuration', async () => {
152
218
  const newSfuClient = new StreamSfuClient({