@stream-io/video-client 1.9.2 → 1.10.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.
@@ -1,15 +1,23 @@
1
+ import { combineLatest } from 'rxjs';
1
2
  import { StreamSfuClient } from '../StreamSfuClient';
2
- import { StatsOptions } from '../gen/coordinator';
3
+ import { OwnCapability, StatsOptions } from '../gen/coordinator';
3
4
  import { getLogger } from '../logger';
4
5
  import { Publisher, Subscriber } from '../rtc';
5
6
  import { flatten, getSdkName, getSdkVersion } from './utils';
6
7
  import { getWebRTCInfo, LocalClientDetailsType } from '../client-details';
8
+ import { InputDevices } from '../gen/video/sfu/models/models';
9
+ import { CameraManager, MicrophoneManager } from '../devices';
10
+ import { createSubscription } from '../store/rxUtils';
11
+ import { CallState } from '../store';
7
12
 
8
13
  export type SfuStatsReporterOptions = {
9
14
  options: StatsOptions;
10
15
  clientDetails: LocalClientDetailsType;
11
16
  subscriber: Subscriber;
12
17
  publisher?: Publisher;
18
+ microphone: MicrophoneManager;
19
+ camera: CameraManager;
20
+ state: CallState;
13
21
  };
14
22
 
15
23
  export class SfuStatsReporter {
@@ -20,34 +28,91 @@ export class SfuStatsReporter {
20
28
  private readonly sfuClient: StreamSfuClient;
21
29
  private readonly subscriber: Subscriber;
22
30
  private readonly publisher?: Publisher;
31
+ private readonly microphone: MicrophoneManager;
32
+ private readonly camera: CameraManager;
33
+ private readonly state: CallState;
23
34
 
24
35
  private intervalId: NodeJS.Timeout | undefined;
36
+ private unsubscribeDevicePermissionsSubscription?: () => void;
37
+ private unsubscribeListDevicesSubscription?: () => void;
25
38
  private readonly sdkName: string;
26
39
  private readonly sdkVersion: string;
27
40
  private readonly webRTCVersion: string;
41
+ private readonly inputDevices = new Map<'mic' | 'camera', InputDevices>();
28
42
 
29
43
  constructor(
30
44
  sfuClient: StreamSfuClient,
31
- { options, clientDetails, subscriber, publisher }: SfuStatsReporterOptions,
45
+ {
46
+ options,
47
+ clientDetails,
48
+ subscriber,
49
+ publisher,
50
+ microphone,
51
+ camera,
52
+ state,
53
+ }: SfuStatsReporterOptions,
32
54
  ) {
33
55
  this.sfuClient = sfuClient;
34
56
  this.options = options;
35
57
  this.subscriber = subscriber;
36
58
  this.publisher = publisher;
37
- const webRTCInfo = getWebRTCInfo();
59
+ this.microphone = microphone;
60
+ this.camera = camera;
61
+ this.state = state;
38
62
 
39
63
  const { sdk, browser } = clientDetails;
40
-
41
64
  this.sdkName = getSdkName(sdk);
42
65
  this.sdkVersion = getSdkVersion(sdk);
43
66
 
44
- // The WebRTC version if passed from the SDK, it is taken else the browser info is sent.
67
+ // use the WebRTC version if set by the SDK (React Native) otherwise,
68
+ // use the browser version as a fallback
69
+ const webRTCInfo = getWebRTCInfo();
45
70
  this.webRTCVersion =
46
71
  webRTCInfo?.version ||
47
72
  `${browser?.name || ''}-${browser?.version || ''}` ||
48
73
  'N/A';
49
74
  }
50
75
 
76
+ private observeDevice = (
77
+ device: CameraManager | MicrophoneManager,
78
+ kind: 'mic' | 'camera',
79
+ ) => {
80
+ const { hasBrowserPermission$ } = device.state;
81
+ this.unsubscribeDevicePermissionsSubscription?.();
82
+ this.unsubscribeDevicePermissionsSubscription = createSubscription(
83
+ combineLatest([hasBrowserPermission$, this.state.ownCapabilities$]),
84
+ ([hasPermission, ownCapabilities]) => {
85
+ // cleanup the previous listDevices() subscription in case
86
+ // permissions or capabilities have changed.
87
+ // we will subscribe again if everything is in order.
88
+ this.unsubscribeListDevicesSubscription?.();
89
+ const hasCapability =
90
+ kind === 'mic'
91
+ ? ownCapabilities.includes(OwnCapability.SEND_AUDIO)
92
+ : ownCapabilities.includes(OwnCapability.SEND_VIDEO);
93
+ if (!hasPermission || !hasCapability) {
94
+ this.inputDevices.set(kind, {
95
+ currentDevice: '',
96
+ availableDevices: [],
97
+ isPermitted: false,
98
+ });
99
+ return;
100
+ }
101
+ this.unsubscribeListDevicesSubscription = createSubscription(
102
+ combineLatest([device.listDevices(), device.state.selectedDevice$]),
103
+ ([devices, deviceId]) => {
104
+ const selected = devices.find((d) => d.deviceId === deviceId);
105
+ this.inputDevices.set(kind, {
106
+ currentDevice: selected?.label || deviceId || '',
107
+ availableDevices: devices.map((d) => d.label),
108
+ isPermitted: true,
109
+ });
110
+ },
111
+ );
112
+ },
113
+ );
114
+ };
115
+
51
116
  private run = async () => {
52
117
  const [subscriberStats, publisherStats] = await Promise.all([
53
118
  this.subscriber.getStats().then(flatten).then(JSON.stringify),
@@ -60,11 +125,18 @@ export class SfuStatsReporter {
60
125
  webrtcVersion: this.webRTCVersion,
61
126
  subscriberStats,
62
127
  publisherStats,
128
+ audioDevices: this.inputDevices.get('mic'),
129
+ videoDevices: this.inputDevices.get('camera'),
130
+ deviceState: { oneofKind: undefined },
63
131
  });
64
132
  };
65
133
 
66
134
  start = () => {
67
135
  if (this.options.reporting_interval_ms <= 0) return;
136
+
137
+ this.observeDevice(this.microphone, 'mic');
138
+ this.observeDevice(this.camera, 'camera');
139
+
68
140
  clearInterval(this.intervalId);
69
141
  this.intervalId = setInterval(() => {
70
142
  this.run().catch((err) => {
@@ -74,6 +146,12 @@ export class SfuStatsReporter {
74
146
  };
75
147
 
76
148
  stop = () => {
149
+ this.unsubscribeDevicePermissionsSubscription?.();
150
+ this.unsubscribeDevicePermissionsSubscription = undefined;
151
+ this.unsubscribeListDevicesSubscription?.();
152
+ this.unsubscribeListDevicesSubscription = undefined;
153
+
154
+ this.inputDevices.clear();
77
155
  clearInterval(this.intervalId);
78
156
  this.intervalId = undefined;
79
157
  };
@@ -1,5 +1,6 @@
1
1
  import { combineLatest, Observable, Subject } from 'rxjs';
2
2
  import { withoutConcurrency } from '../helpers/concurrency';
3
+ import { getLogger } from '../logger';
3
4
 
4
5
  type FunctionPatch<T> = (currentValue: T) => T;
5
6
 
@@ -63,12 +64,15 @@ export const setCurrentValue = <T>(subject: Subject<T>, update: Patch<T>) => {
63
64
  *
64
65
  * @param observable the observable to subscribe to.
65
66
  * @param handler the handler to call when the observable emits a value.
67
+ * @param onError an optional error handler.
66
68
  */
67
69
  export const createSubscription = <T>(
68
70
  observable: Observable<T>,
69
71
  handler: (value: T) => void,
72
+ onError: (error: any) => void = (error) =>
73
+ getLogger(['RxUtils'])('warn', 'An observable emitted an error', error),
70
74
  ) => {
71
- const subscription = observable.subscribe(handler);
75
+ const subscription = observable.subscribe({ next: handler, error: onError });
72
76
  return () => {
73
77
  subscription.unsubscribe();
74
78
  };
@@ -87,10 +91,7 @@ export const createSafeAsyncSubscription = <T>(
87
91
  handler: (value: T) => Promise<void>,
88
92
  ) => {
89
93
  const tag = Symbol();
90
- const subscription = observable.subscribe((value) => {
94
+ return createSubscription(observable, (value) => {
91
95
  withoutConcurrency(tag, () => handler(value));
92
96
  });
93
- return () => {
94
- subscription.unsubscribe();
95
- };
96
97
  };