@stream-io/video-client 1.8.3 → 1.9.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +434 -449
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +434 -449
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +434 -449
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +10 -12
  9. package/dist/src/compatibility.d.ts +7 -0
  10. package/dist/src/devices/CameraManager.d.ts +2 -22
  11. package/dist/src/events/internal.d.ts +0 -4
  12. package/dist/src/gen/video/sfu/event/events.d.ts +2 -71
  13. package/dist/src/helpers/sdp-munging.d.ts +8 -0
  14. package/dist/src/rtc/Publisher.d.ts +18 -23
  15. package/dist/src/rtc/bitrateLookup.d.ts +2 -0
  16. package/dist/src/rtc/codecs.d.ts +9 -2
  17. package/dist/src/rtc/videoLayers.d.ts +31 -4
  18. package/dist/src/types.d.ts +30 -2
  19. package/package.json +1 -1
  20. package/src/Call.ts +21 -38
  21. package/src/compatibility.ts +7 -0
  22. package/src/devices/CameraManager.ts +18 -47
  23. package/src/devices/ScreenShareManager.ts +1 -3
  24. package/src/devices/__tests__/CameraManager.test.ts +7 -15
  25. package/src/devices/__tests__/ScreenShareManager.test.ts +0 -14
  26. package/src/events/callEventHandlers.ts +0 -2
  27. package/src/events/internal.ts +0 -16
  28. package/src/gen/video/sfu/event/events.ts +8 -120
  29. package/src/helpers/sdp-munging.ts +38 -15
  30. package/src/rtc/Publisher.ts +211 -317
  31. package/src/rtc/__tests__/Publisher.test.ts +196 -7
  32. package/src/rtc/__tests__/bitrateLookup.test.ts +12 -0
  33. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +2 -0
  34. package/src/rtc/__tests__/videoLayers.test.ts +51 -36
  35. package/src/rtc/bitrateLookup.ts +61 -0
  36. package/src/rtc/codecs.ts +56 -9
  37. package/src/rtc/videoLayers.ts +74 -23
  38. package/src/types.ts +30 -2
@@ -4,7 +4,8 @@ import { CameraDirection, CameraManagerState } from './CameraManagerState';
4
4
  import { InputMediaDeviceManager } from './InputMediaDeviceManager';
5
5
  import { getVideoDevices, getVideoStream } from './devices';
6
6
  import { TrackType } from '../gen/video/sfu/models/models';
7
- import { PreferredCodec, PublishOptions } from '../types';
7
+ import { PreferredCodec } from '../types';
8
+ import { isMobile } from '../compatibility';
8
9
 
9
10
  export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
10
11
  private targetResolution = {
@@ -12,13 +13,6 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
12
13
  height: 720,
13
14
  };
14
15
 
15
- /**
16
- * The options to use when publishing the video stream.
17
- *
18
- * @internal
19
- */
20
- publishOptions: PublishOptions | undefined;
21
-
22
16
  /**
23
17
  * Constructs a new CameraManager.
24
18
  *
@@ -34,10 +28,14 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
34
28
  * @param direction the direction of the camera to select.
35
29
  */
36
30
  async selectDirection(direction: Exclude<CameraDirection, undefined>) {
37
- this.state.setDirection(direction);
38
- // Providing both device id and direction doesn't work, so we deselect the device
39
- this.state.setDevice(undefined);
40
- await this.applySettingsToStream();
31
+ if (isMobile()) {
32
+ this.state.setDirection(direction);
33
+ // Providing both device id and direction doesn't work, so we deselect the device
34
+ this.state.setDevice(undefined);
35
+ await this.applySettingsToStream();
36
+ } else {
37
+ this.logger('warn', 'Camera direction ignored for desktop devices');
38
+ }
41
39
  }
42
40
 
43
41
  /**
@@ -65,10 +63,10 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
65
63
  this.logger('warn', 'could not apply target resolution', error);
66
64
  }
67
65
  }
68
- if (this.enabled) {
69
- const { width, height } = this.state
70
- .mediaStream!.getVideoTracks()[0]
71
- ?.getSettings();
66
+ if (this.enabled && this.state.mediaStream) {
67
+ const [videoTrack] = this.state.mediaStream.getVideoTracks();
68
+ if (!videoTrack) return;
69
+ const { width, height } = videoTrack.getSettings();
72
70
  if (
73
71
  width !== this.targetResolution.width ||
74
72
  height !== this.targetResolution.height
@@ -86,38 +84,11 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
86
84
  * Sets the preferred codec for encoding the video.
87
85
  *
88
86
  * @internal internal use only, not part of the public API.
87
+ * @deprecated use {@link call.updatePublishOptions} instead.
89
88
  * @param codec the codec to use for encoding the video.
90
89
  */
91
90
  setPreferredCodec(codec: PreferredCodec | undefined) {
92
- this.updatePublishOptions({ preferredCodec: codec });
93
- }
94
-
95
- /**
96
- * Updates the preferred publish options for the video stream.
97
- *
98
- * @internal
99
- * @param options the options to use.
100
- */
101
- updatePublishOptions(options: PublishOptions) {
102
- this.publishOptions = { ...this.publishOptions, ...options };
103
- }
104
-
105
- /**
106
- * Returns the capture resolution of the camera.
107
- */
108
- getCaptureResolution() {
109
- const { mediaStream } = this.state;
110
- if (!mediaStream) return;
111
-
112
- const [videoTrack] = mediaStream.getVideoTracks();
113
- if (!videoTrack) return;
114
-
115
- const settings = videoTrack.getSettings();
116
- return {
117
- width: settings.width,
118
- height: settings.height,
119
- frameRate: settings.frameRate,
120
- };
91
+ this.call.updatePublishOptions({ preferredCodec: codec });
121
92
  }
122
93
 
123
94
  protected getDevices(): Observable<MediaDeviceInfo[]> {
@@ -131,7 +102,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
131
102
  constraints.height = this.targetResolution.height;
132
103
  // We can't set both device id and facing mode
133
104
  // Device id has higher priority
134
- if (!constraints.deviceId && this.state.direction) {
105
+ if (!constraints.deviceId && this.state.direction && isMobile()) {
135
106
  constraints.facingMode =
136
107
  this.state.direction === 'front' ? 'user' : 'environment';
137
108
  }
@@ -139,7 +110,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
139
110
  }
140
111
 
141
112
  protected publishStream(stream: MediaStream): Promise<void> {
142
- return this.call.publishVideoStream(stream, this.publishOptions);
113
+ return this.call.publishVideoStream(stream);
143
114
  }
144
115
 
145
116
  protected stopPublishStream(stopTracks: boolean): Promise<void> {
@@ -80,9 +80,7 @@ export class ScreenShareManager extends InputMediaDeviceManager<
80
80
  }
81
81
 
82
82
  protected publishStream(stream: MediaStream): Promise<void> {
83
- return this.call.publishScreenShareStream(stream, {
84
- screenShareSettings: this.state.settings,
85
- });
83
+ return this.call.publishScreenShareStream(stream);
86
84
  }
87
85
 
88
86
  protected async stopPublishStream(stopTracks: boolean): Promise<void> {
@@ -36,6 +36,13 @@ vi.mock('../../Call.ts', () => {
36
36
  };
37
37
  });
38
38
 
39
+ vi.mock('../../compatibility.ts', () => {
40
+ console.log('MOCKING mobile device');
41
+ return {
42
+ isMobile: () => true,
43
+ };
44
+ });
45
+
39
46
  describe('CameraManager', () => {
40
47
  let manager: CameraManager;
41
48
 
@@ -82,21 +89,6 @@ describe('CameraManager', () => {
82
89
 
83
90
  expect(manager['call'].publishVideoStream).toHaveBeenCalledWith(
84
91
  manager.state.mediaStream,
85
- undefined,
86
- );
87
- });
88
-
89
- it('publish stream with preferred codec', async () => {
90
- manager['call'].state.setCallingState(CallingState.JOINED);
91
- manager.setPreferredCodec('h264');
92
-
93
- await manager.enable();
94
-
95
- expect(manager['call'].publishVideoStream).toHaveBeenCalledWith(
96
- manager.state.mediaStream,
97
- {
98
- preferredCodec: 'h264',
99
- },
100
92
  );
101
93
  });
102
94
 
@@ -115,20 +115,6 @@ describe('ScreenShareManager', () => {
115
115
  await manager.enable();
116
116
  expect(call.publishScreenShareStream).toHaveBeenCalledWith(
117
117
  manager.state.mediaStream,
118
- { screenShareSettings: undefined },
119
- );
120
- });
121
-
122
- it('publishes screen share stream with settings', async () => {
123
- const call = manager['call'];
124
- call.state.setCallingState(CallingState.JOINED);
125
-
126
- manager.setSettings({ maxFramerate: 15, maxBitrate: 1000 });
127
-
128
- await manager.enable();
129
- expect(call.publishScreenShareStream).toHaveBeenCalledWith(
130
- manager.state.mediaStream,
131
- { screenShareSettings: { maxFramerate: 15, maxBitrate: 1000 } },
132
118
  );
133
119
  });
134
120
 
@@ -7,7 +7,6 @@ import {
7
7
  watchCallEnded,
8
8
  watchCallGrantsUpdated,
9
9
  watchCallRejected,
10
- watchChangePublishQuality,
11
10
  watchConnectionQualityChanged,
12
11
  watchDominantSpeakerChanged,
13
12
  watchLiveEnded,
@@ -46,7 +45,6 @@ export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
46
45
 
47
46
  watchLiveEnded(dispatcher, call),
48
47
  watchSfuErrorReports(dispatcher),
49
- watchChangePublishQuality(dispatcher, call),
50
48
  watchConnectionQualityChanged(dispatcher, state),
51
49
  watchParticipantCountChanged(dispatcher, state),
52
50
 
@@ -10,22 +10,6 @@ import {
10
10
  } from '../gen/video/sfu/models/models';
11
11
  import { OwnCapability } from '../gen/coordinator';
12
12
 
13
- /**
14
- * An event responder which handles the `changePublishQuality` event.
15
- */
16
- export const watchChangePublishQuality = (
17
- dispatcher: Dispatcher,
18
- call: Call,
19
- ) => {
20
- return dispatcher.on('changePublishQuality', (e) => {
21
- const { videoSenders } = e;
22
- videoSenders.forEach((videoSender) => {
23
- const { layers } = videoSender;
24
- call.updatePublishQuality(layers.filter((l) => l.active));
25
- });
26
- });
27
- };
28
-
29
13
  export const watchConnectionQualityChanged = (
30
14
  dispatcher: Dispatcher,
31
15
  state: CallState,
@@ -682,45 +682,15 @@ export interface AudioLevelChanged {
682
682
  */
683
683
  audioLevels: AudioLevel[];
684
684
  }
685
- /**
686
- * @generated from protobuf message stream.video.sfu.event.AudioMediaRequest
687
- */
688
- export interface AudioMediaRequest {
689
- /**
690
- * @generated from protobuf field: int32 channel_count = 1;
691
- */
692
- channelCount: number;
693
- }
694
685
  /**
695
686
  * @generated from protobuf message stream.video.sfu.event.AudioSender
696
687
  */
697
688
  export interface AudioSender {
698
- /**
699
- * @generated from protobuf field: stream.video.sfu.event.AudioMediaRequest media_request = 1;
700
- */
701
- mediaRequest?: AudioMediaRequest;
702
689
  /**
703
690
  * @generated from protobuf field: stream.video.sfu.models.Codec codec = 2;
704
691
  */
705
692
  codec?: Codec;
706
693
  }
707
- /**
708
- * @generated from protobuf message stream.video.sfu.event.VideoMediaRequest
709
- */
710
- export interface VideoMediaRequest {
711
- /**
712
- * @generated from protobuf field: int32 ideal_height = 1;
713
- */
714
- idealHeight: number;
715
- /**
716
- * @generated from protobuf field: int32 ideal_width = 2;
717
- */
718
- idealWidth: number;
719
- /**
720
- * @generated from protobuf field: int32 ideal_frame_rate = 3;
721
- */
722
- idealFrameRate: number;
723
- }
724
694
  /**
725
695
  * VideoLayerSetting is used to specify various parameters of a particular encoding in simulcast.
726
696
  * The parameters are specified here - https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpEncodingParameters
@@ -745,10 +715,6 @@ export interface VideoLayerSetting {
745
715
  * @generated from protobuf field: float scale_resolution_down_by = 4;
746
716
  */
747
717
  scaleResolutionDownBy: number;
748
- /**
749
- * @generated from protobuf field: stream.video.sfu.event.VideoLayerSetting.Priority priority = 5;
750
- */
751
- priority: VideoLayerSetting_Priority;
752
718
  /**
753
719
  * @generated from protobuf field: stream.video.sfu.models.Codec codec = 6;
754
720
  */
@@ -757,36 +723,15 @@ export interface VideoLayerSetting {
757
723
  * @generated from protobuf field: uint32 max_framerate = 7;
758
724
  */
759
725
  maxFramerate: number;
760
- }
761
- /**
762
- * @generated from protobuf enum stream.video.sfu.event.VideoLayerSetting.Priority
763
- */
764
- export enum VideoLayerSetting_Priority {
765
- /**
766
- * @generated from protobuf enum value: PRIORITY_HIGH_UNSPECIFIED = 0;
767
- */
768
- HIGH_UNSPECIFIED = 0,
769
- /**
770
- * @generated from protobuf enum value: PRIORITY_LOW = 1;
771
- */
772
- LOW = 1,
773
- /**
774
- * @generated from protobuf enum value: PRIORITY_MEDIUM = 2;
775
- */
776
- MEDIUM = 2,
777
726
  /**
778
- * @generated from protobuf enum value: PRIORITY_VERY_LOW = 3;
727
+ * @generated from protobuf field: string scalability_mode = 8;
779
728
  */
780
- VERY_LOW = 3,
729
+ scalabilityMode: string;
781
730
  }
782
731
  /**
783
732
  * @generated from protobuf message stream.video.sfu.event.VideoSender
784
733
  */
785
734
  export interface VideoSender {
786
- /**
787
- * @generated from protobuf field: stream.video.sfu.event.VideoMediaRequest media_request = 1;
788
- */
789
- mediaRequest?: VideoMediaRequest;
790
735
  /**
791
736
  * @generated from protobuf field: stream.video.sfu.models.Codec codec = 2;
792
737
  */
@@ -1537,32 +1482,9 @@ class AudioLevelChanged$Type extends MessageType<AudioLevelChanged> {
1537
1482
  */
1538
1483
  export const AudioLevelChanged = new AudioLevelChanged$Type();
1539
1484
  // @generated message type with reflection information, may provide speed optimized methods
1540
- class AudioMediaRequest$Type extends MessageType<AudioMediaRequest> {
1541
- constructor() {
1542
- super('stream.video.sfu.event.AudioMediaRequest', [
1543
- {
1544
- no: 1,
1545
- name: 'channel_count',
1546
- kind: 'scalar',
1547
- T: 5 /*ScalarType.INT32*/,
1548
- },
1549
- ]);
1550
- }
1551
- }
1552
- /**
1553
- * @generated MessageType for protobuf message stream.video.sfu.event.AudioMediaRequest
1554
- */
1555
- export const AudioMediaRequest = new AudioMediaRequest$Type();
1556
- // @generated message type with reflection information, may provide speed optimized methods
1557
1485
  class AudioSender$Type extends MessageType<AudioSender> {
1558
1486
  constructor() {
1559
1487
  super('stream.video.sfu.event.AudioSender', [
1560
- {
1561
- no: 1,
1562
- name: 'media_request',
1563
- kind: 'message',
1564
- T: () => AudioMediaRequest,
1565
- },
1566
1488
  { no: 2, name: 'codec', kind: 'message', T: () => Codec },
1567
1489
  ]);
1568
1490
  }
@@ -1572,30 +1494,6 @@ class AudioSender$Type extends MessageType<AudioSender> {
1572
1494
  */
1573
1495
  export const AudioSender = new AudioSender$Type();
1574
1496
  // @generated message type with reflection information, may provide speed optimized methods
1575
- class VideoMediaRequest$Type extends MessageType<VideoMediaRequest> {
1576
- constructor() {
1577
- super('stream.video.sfu.event.VideoMediaRequest', [
1578
- {
1579
- no: 1,
1580
- name: 'ideal_height',
1581
- kind: 'scalar',
1582
- T: 5 /*ScalarType.INT32*/,
1583
- },
1584
- { no: 2, name: 'ideal_width', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
1585
- {
1586
- no: 3,
1587
- name: 'ideal_frame_rate',
1588
- kind: 'scalar',
1589
- T: 5 /*ScalarType.INT32*/,
1590
- },
1591
- ]);
1592
- }
1593
- }
1594
- /**
1595
- * @generated MessageType for protobuf message stream.video.sfu.event.VideoMediaRequest
1596
- */
1597
- export const VideoMediaRequest = new VideoMediaRequest$Type();
1598
- // @generated message type with reflection information, may provide speed optimized methods
1599
1497
  class VideoLayerSetting$Type extends MessageType<VideoLayerSetting> {
1600
1498
  constructor() {
1601
1499
  super('stream.video.sfu.event.VideoLayerSetting', [
@@ -1608,16 +1506,6 @@ class VideoLayerSetting$Type extends MessageType<VideoLayerSetting> {
1608
1506
  kind: 'scalar',
1609
1507
  T: 2 /*ScalarType.FLOAT*/,
1610
1508
  },
1611
- {
1612
- no: 5,
1613
- name: 'priority',
1614
- kind: 'enum',
1615
- T: () => [
1616
- 'stream.video.sfu.event.VideoLayerSetting.Priority',
1617
- VideoLayerSetting_Priority,
1618
- 'PRIORITY_',
1619
- ],
1620
- },
1621
1509
  { no: 6, name: 'codec', kind: 'message', T: () => Codec },
1622
1510
  {
1623
1511
  no: 7,
@@ -1625,6 +1513,12 @@ class VideoLayerSetting$Type extends MessageType<VideoLayerSetting> {
1625
1513
  kind: 'scalar',
1626
1514
  T: 13 /*ScalarType.UINT32*/,
1627
1515
  },
1516
+ {
1517
+ no: 8,
1518
+ name: 'scalability_mode',
1519
+ kind: 'scalar',
1520
+ T: 9 /*ScalarType.STRING*/,
1521
+ },
1628
1522
  ]);
1629
1523
  }
1630
1524
  }
@@ -1636,12 +1530,6 @@ export const VideoLayerSetting = new VideoLayerSetting$Type();
1636
1530
  class VideoSender$Type extends MessageType<VideoSender> {
1637
1531
  constructor() {
1638
1532
  super('stream.video.sfu.event.VideoSender', [
1639
- {
1640
- no: 1,
1641
- name: 'media_request',
1642
- kind: 'message',
1643
- T: () => VideoMediaRequest,
1644
- },
1645
1533
  { no: 2, name: 'codec', kind: 'message', T: () => Codec },
1646
1534
  {
1647
1535
  no: 3,
@@ -118,21 +118,15 @@ const getOpusFmtp = (sdp: string): Fmtp | undefined => {
118
118
  */
119
119
  export const toggleDtx = (sdp: string, enable: boolean): string => {
120
120
  const opusFmtp = getOpusFmtp(sdp);
121
- if (opusFmtp) {
122
- const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
123
- const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
124
- if (matchDtx) {
125
- const newFmtp = opusFmtp.original.replace(
126
- /usedtx=(\d)/,
127
- requiredDtxConfig,
128
- );
129
- return sdp.replace(opusFmtp.original, newFmtp);
130
- } else {
131
- const newFmtp = `${opusFmtp.original};${requiredDtxConfig}`;
132
- return sdp.replace(opusFmtp.original, newFmtp);
133
- }
134
- }
135
- return sdp;
121
+ if (!opusFmtp) return sdp;
122
+
123
+ const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
124
+ const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
125
+ const newFmtp = matchDtx
126
+ ? opusFmtp.original.replace(/usedtx=(\d)/, requiredDtxConfig)
127
+ : `${opusFmtp.original};${requiredDtxConfig}`;
128
+
129
+ return sdp.replace(opusFmtp.original, newFmtp);
136
130
  };
137
131
 
138
132
  /**
@@ -181,3 +175,32 @@ export const enableHighQualityAudio = (
181
175
 
182
176
  return SDP.write(parsedSdp);
183
177
  };
178
+
179
+ /**
180
+ * Extracts the mid from the transceiver or the SDP.
181
+ *
182
+ * @param transceiver the transceiver.
183
+ * @param transceiverInitIndex the index of the transceiver in the transceiver's init array.
184
+ * @param sdp the SDP.
185
+ */
186
+ export const extractMid = (
187
+ transceiver: RTCRtpTransceiver,
188
+ transceiverInitIndex: number,
189
+ sdp: string | undefined,
190
+ ): string => {
191
+ if (transceiver.mid) return transceiver.mid;
192
+ if (!sdp) return '';
193
+
194
+ const track = transceiver.sender.track!;
195
+ const parsedSdp = SDP.parse(sdp);
196
+ const media = parsedSdp.media.find((m) => {
197
+ return (
198
+ m.type === track.kind &&
199
+ // if `msid` is not present, we assume that the track is the first one
200
+ (m.msid?.includes(track.id) ?? true)
201
+ );
202
+ });
203
+ if (typeof media?.mid !== 'undefined') return String(media.mid);
204
+ if (transceiverInitIndex === -1) return '';
205
+ return String(transceiverInitIndex);
206
+ };