@stream-io/video-client 0.3.13 → 0.3.14
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 +7 -0
- package/dist/index.browser.es.js +272 -173
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +272 -173
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +272 -173
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/CameraManager.d.ts +1 -2
- package/dist/src/devices/InputMediaDeviceManager.d.ts +10 -3
- package/dist/src/devices/MicrophoneManager.d.ts +1 -2
- package/dist/src/rtc/Publisher.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Call.ts +79 -5
- package/src/StreamSfuClient.ts +4 -0
- package/src/coordinator/connection/location.ts +2 -2
- package/src/devices/CameraManager.ts +7 -8
- package/src/devices/InputMediaDeviceManager.ts +58 -12
- package/src/devices/MicrophoneManager.ts +3 -8
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +5 -12
- package/src/devices/__tests__/mocks.ts +4 -0
- package/src/helpers/__tests__/DynascaleManager.test.ts +3 -0
- package/src/rtc/Publisher.ts +35 -7
- package/src/rtc/__tests__/Publisher.test.ts +1 -0
- package/src/rtc/codecs.ts +0 -2
|
@@ -28,6 +28,5 @@ export declare class CameraManager extends InputMediaDeviceManager<CameraManager
|
|
|
28
28
|
protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
29
29
|
protected publishStream(stream: MediaStream): Promise<void>;
|
|
30
30
|
protected stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
31
|
-
protected
|
|
32
|
-
protected unmuteTracks(): void;
|
|
31
|
+
protected getTrack(): MediaStreamTrack | undefined;
|
|
33
32
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
4
|
+
import { Logger } from '../coordinator/connection/types';
|
|
5
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
4
6
|
export declare abstract class InputMediaDeviceManager<T extends InputMediaDeviceManagerState> {
|
|
5
7
|
protected readonly call: Call;
|
|
6
8
|
readonly state: T;
|
|
9
|
+
protected readonly trackType: TrackType;
|
|
7
10
|
/**
|
|
8
11
|
* @internal
|
|
9
12
|
*/
|
|
@@ -12,7 +15,8 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
12
15
|
* @internal
|
|
13
16
|
*/
|
|
14
17
|
disablePromise?: Promise<void>;
|
|
15
|
-
|
|
18
|
+
logger: Logger;
|
|
19
|
+
constructor(call: Call, state: T, trackType: TrackType);
|
|
16
20
|
/**
|
|
17
21
|
* Lists the available audio/video devices
|
|
18
22
|
*
|
|
@@ -54,8 +58,11 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
54
58
|
protected abstract getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
55
59
|
protected abstract publishStream(stream: MediaStream): Promise<void>;
|
|
56
60
|
protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
57
|
-
protected abstract
|
|
58
|
-
protected abstract unmuteTracks(): void;
|
|
61
|
+
protected abstract getTrack(): undefined | MediaStreamTrack;
|
|
59
62
|
private muteStream;
|
|
63
|
+
private muteTrack;
|
|
64
|
+
private unmuteTrack;
|
|
65
|
+
private stopTrack;
|
|
66
|
+
private muteLocalStream;
|
|
60
67
|
private unmuteStream;
|
|
61
68
|
}
|
|
@@ -8,6 +8,5 @@ export declare class MicrophoneManager extends InputMediaDeviceManager<Microphon
|
|
|
8
8
|
protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
9
9
|
protected publishStream(stream: MediaStream): Promise<void>;
|
|
10
10
|
protected stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
11
|
-
protected
|
|
12
|
-
protected unmuteTracks(): void;
|
|
11
|
+
protected getTrack(): MediaStreamTrack | undefined;
|
|
13
12
|
}
|
|
@@ -85,6 +85,12 @@ export declare class Publisher {
|
|
|
85
85
|
* @param trackType the track type to check.
|
|
86
86
|
*/
|
|
87
87
|
isPublishing: (trackType: TrackType) => boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if the given track type is currently live
|
|
90
|
+
*
|
|
91
|
+
* @param trackType the track type to check.
|
|
92
|
+
*/
|
|
93
|
+
isLive: (trackType: TrackType) => boolean;
|
|
88
94
|
private notifyTrackMuteStateChanged;
|
|
89
95
|
/**
|
|
90
96
|
* Stops publishing all tracks and stop all tracks.
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.3.
|
|
1
|
+
export declare const version = "0.3.14";
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -277,6 +277,37 @@ export class Call {
|
|
|
277
277
|
|
|
278
278
|
this.camera = new CameraManager(this);
|
|
279
279
|
this.microphone = new MicrophoneManager(this);
|
|
280
|
+
|
|
281
|
+
this.state.localParticipant$.subscribe(async (p) => {
|
|
282
|
+
// Mute via device manager
|
|
283
|
+
// If integrator doesn't use device manager, we mute using stopPublish
|
|
284
|
+
if (
|
|
285
|
+
!p?.publishedTracks.includes(TrackType.VIDEO) &&
|
|
286
|
+
this.publisher?.isPublishing(TrackType.VIDEO)
|
|
287
|
+
) {
|
|
288
|
+
this.logger(
|
|
289
|
+
'info',
|
|
290
|
+
`Local participant's video track is muted remotely`,
|
|
291
|
+
);
|
|
292
|
+
await this.camera.disable();
|
|
293
|
+
if (this.publisher.isPublishing(TrackType.VIDEO)) {
|
|
294
|
+
this.stopPublish(TrackType.VIDEO);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
!p?.publishedTracks.includes(TrackType.AUDIO) &&
|
|
299
|
+
this.publisher?.isPublishing(TrackType.AUDIO)
|
|
300
|
+
) {
|
|
301
|
+
this.logger(
|
|
302
|
+
'info',
|
|
303
|
+
`Local participant's audio track is muted remotely`,
|
|
304
|
+
);
|
|
305
|
+
await this.microphone.disable();
|
|
306
|
+
if (this.publisher.isPublishing(TrackType.AUDIO)) {
|
|
307
|
+
this.stopPublish(TrackType.AUDIO);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
280
311
|
this.speaker = new SpeakerManager();
|
|
281
312
|
}
|
|
282
313
|
|
|
@@ -309,10 +340,50 @@ export class Call {
|
|
|
309
340
|
const hasPermission = this.permissionsContext.hasPermission(
|
|
310
341
|
permission as OwnCapability,
|
|
311
342
|
);
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
343
|
+
if (
|
|
344
|
+
!hasPermission &&
|
|
345
|
+
(this.publisher.isPublishing(trackType) ||
|
|
346
|
+
this.publisher.isLive(trackType))
|
|
347
|
+
) {
|
|
348
|
+
// Stop tracks, then notify device manager
|
|
349
|
+
this.stopPublish(trackType)
|
|
350
|
+
.catch((err) => {
|
|
351
|
+
this.logger(
|
|
352
|
+
'error',
|
|
353
|
+
`Error stopping publish ${trackType}`,
|
|
354
|
+
err,
|
|
355
|
+
);
|
|
356
|
+
})
|
|
357
|
+
.then(() => {
|
|
358
|
+
if (
|
|
359
|
+
trackType === TrackType.VIDEO &&
|
|
360
|
+
this.camera.state.status === 'enabled'
|
|
361
|
+
) {
|
|
362
|
+
this.camera
|
|
363
|
+
.disable()
|
|
364
|
+
.catch((err) =>
|
|
365
|
+
this.logger(
|
|
366
|
+
'error',
|
|
367
|
+
`Error disabling camera after pemission revoked`,
|
|
368
|
+
err,
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
trackType === TrackType.AUDIO &&
|
|
374
|
+
this.microphone.state.status === 'enabled'
|
|
375
|
+
) {
|
|
376
|
+
this.microphone
|
|
377
|
+
.disable()
|
|
378
|
+
.catch((err) =>
|
|
379
|
+
this.logger(
|
|
380
|
+
'error',
|
|
381
|
+
`Error disabling microphone after pemission revoked`,
|
|
382
|
+
err,
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
316
387
|
}
|
|
317
388
|
}
|
|
318
389
|
}),
|
|
@@ -1112,7 +1183,10 @@ export class Call {
|
|
|
1112
1183
|
* @param stopTrack if `true` the track will be stopped, else it will be just disabled
|
|
1113
1184
|
*/
|
|
1114
1185
|
stopPublish = async (trackType: TrackType, stopTrack: boolean = true) => {
|
|
1115
|
-
this.logger(
|
|
1186
|
+
this.logger(
|
|
1187
|
+
'info',
|
|
1188
|
+
`stopPublish ${TrackType[trackType]}, stop tracks: ${stopTrack}`,
|
|
1189
|
+
);
|
|
1116
1190
|
await this.publisher?.unpublishStream(trackType, stopTrack);
|
|
1117
1191
|
};
|
|
1118
1192
|
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -5,7 +5,7 @@ const HINT_URL = `https://hint.stream-io-video.com/`;
|
|
|
5
5
|
|
|
6
6
|
export const getLocationHint = async (
|
|
7
7
|
hintUrl: string = HINT_URL,
|
|
8
|
-
timeout: number =
|
|
8
|
+
timeout: number = 2000,
|
|
9
9
|
) => {
|
|
10
10
|
const abortController = new AbortController();
|
|
11
11
|
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
@@ -18,7 +18,7 @@ export const getLocationHint = async (
|
|
|
18
18
|
logger('debug', `Location header: ${awsPop}`);
|
|
19
19
|
return awsPop.substring(0, 3); // AMS1-P2 -> AMS
|
|
20
20
|
} catch (e) {
|
|
21
|
-
logger('
|
|
21
|
+
logger('warn', `Failed to get location hint from ${HINT_URL}`, e);
|
|
22
22
|
return 'ERR';
|
|
23
23
|
} finally {
|
|
24
24
|
clearTimeout(timeoutId);
|
|
@@ -12,7 +12,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
constructor(call: Call) {
|
|
15
|
-
super(call, new CameraManagerState());
|
|
15
|
+
super(call, new CameraManagerState(), TrackType.VIDEO);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -59,6 +59,10 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
59
59
|
height !== this.targetResolution.height
|
|
60
60
|
)
|
|
61
61
|
await this.applySettingsToStream();
|
|
62
|
+
this.logger(
|
|
63
|
+
'debug',
|
|
64
|
+
`${width}x${height} target resolution applied to media stream`,
|
|
65
|
+
);
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -85,12 +89,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
85
89
|
return this.call.stopPublish(TrackType.VIDEO, stopTracks);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
protected
|
|
89
|
-
this.state.mediaStream
|
|
90
|
-
?.getVideoTracks()
|
|
91
|
-
.forEach((t) => (t.enabled = false));
|
|
92
|
-
}
|
|
93
|
-
protected unmuteTracks(): void {
|
|
94
|
-
this.state.mediaStream?.getVideoTracks().forEach((t) => (t.enabled = true));
|
|
92
|
+
protected getTrack() {
|
|
93
|
+
return this.state.mediaStream?.getVideoTracks()[0];
|
|
95
94
|
}
|
|
96
95
|
}
|
|
@@ -2,8 +2,10 @@ import { Observable } from 'rxjs';
|
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { CallingState } from '../store';
|
|
4
4
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
5
|
-
import { disposeOfMediaStream } from './devices';
|
|
6
5
|
import { isReactNative } from '../helpers/platforms';
|
|
6
|
+
import { Logger } from '../coordinator/connection/types';
|
|
7
|
+
import { getLogger } from '../logger';
|
|
8
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
9
|
|
|
8
10
|
export abstract class InputMediaDeviceManager<
|
|
9
11
|
T extends InputMediaDeviceManagerState,
|
|
@@ -16,7 +18,15 @@ export abstract class InputMediaDeviceManager<
|
|
|
16
18
|
* @internal
|
|
17
19
|
*/
|
|
18
20
|
disablePromise?: Promise<void>;
|
|
19
|
-
|
|
21
|
+
logger: Logger;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
protected readonly call: Call,
|
|
25
|
+
public readonly state: T,
|
|
26
|
+
protected readonly trackType: TrackType,
|
|
27
|
+
) {
|
|
28
|
+
this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Lists the available audio/video devices
|
|
@@ -129,32 +139,68 @@ export abstract class InputMediaDeviceManager<
|
|
|
129
139
|
|
|
130
140
|
protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
131
141
|
|
|
132
|
-
protected abstract
|
|
133
|
-
|
|
134
|
-
protected abstract unmuteTracks(): void;
|
|
142
|
+
protected abstract getTrack(): undefined | MediaStreamTrack;
|
|
135
143
|
|
|
136
144
|
private async muteStream(stopTracks: boolean = true) {
|
|
137
145
|
if (!this.state.mediaStream) {
|
|
138
146
|
return;
|
|
139
147
|
}
|
|
148
|
+
this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
|
|
140
149
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
141
150
|
await this.stopPublishStream(stopTracks);
|
|
142
|
-
} else if (this.state.mediaStream) {
|
|
143
|
-
stopTracks
|
|
144
|
-
? disposeOfMediaStream(this.state.mediaStream)
|
|
145
|
-
: this.muteTracks();
|
|
146
151
|
}
|
|
147
|
-
|
|
152
|
+
this.muteLocalStream(stopTracks);
|
|
153
|
+
if (this.getTrack()?.readyState === 'ended') {
|
|
154
|
+
// @ts-expect-error release() is present in react-native-webrtc and must be called to dispose the stream
|
|
155
|
+
if (typeof this.state.mediaStream.release === 'function') {
|
|
156
|
+
// @ts-expect-error
|
|
157
|
+
this.state.mediaStream.release();
|
|
158
|
+
}
|
|
148
159
|
this.state.setMediaStream(undefined);
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
private muteTrack() {
|
|
164
|
+
const track = this.getTrack();
|
|
165
|
+
if (!track || !track.enabled) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
track.enabled = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private unmuteTrack() {
|
|
172
|
+
const track = this.getTrack();
|
|
173
|
+
if (!track || track.enabled) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
track.enabled = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private stopTrack() {
|
|
180
|
+
const track = this.getTrack();
|
|
181
|
+
if (!track || track.readyState === 'ended') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
track.stop();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private muteLocalStream(stopTracks: boolean) {
|
|
188
|
+
if (!this.state.mediaStream) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
stopTracks ? this.stopTrack() : this.muteTrack();
|
|
192
|
+
}
|
|
193
|
+
|
|
152
194
|
private async unmuteStream() {
|
|
195
|
+
this.logger('debug', 'Starting stream');
|
|
153
196
|
let stream: MediaStream;
|
|
154
|
-
if (this.state.mediaStream) {
|
|
197
|
+
if (this.state.mediaStream && this.getTrack()?.readyState === 'live') {
|
|
155
198
|
stream = this.state.mediaStream;
|
|
156
|
-
this.
|
|
199
|
+
this.unmuteTrack();
|
|
157
200
|
} else {
|
|
201
|
+
if (this.state.mediaStream) {
|
|
202
|
+
this.stopTrack();
|
|
203
|
+
}
|
|
158
204
|
const constraints = { deviceId: this.state.selectedDevice };
|
|
159
205
|
stream = await this.getStream(constraints);
|
|
160
206
|
}
|
|
@@ -7,7 +7,7 @@ import { TrackType } from '../gen/video/sfu/models/models';
|
|
|
7
7
|
|
|
8
8
|
export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
|
|
9
9
|
constructor(call: Call) {
|
|
10
|
-
super(call, new MicrophoneManagerState());
|
|
10
|
+
super(call, new MicrophoneManagerState(), TrackType.AUDIO);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
@@ -25,12 +25,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
25
25
|
return this.call.stopPublish(TrackType.AUDIO, stopTracks);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
protected
|
|
29
|
-
this.state.mediaStream
|
|
30
|
-
?.getAudioTracks()
|
|
31
|
-
.forEach((t) => (t.enabled = false));
|
|
32
|
-
}
|
|
33
|
-
protected unmuteTracks(): void {
|
|
34
|
-
this.state.mediaStream?.getAudioTracks().forEach((t) => (t.enabled = true));
|
|
28
|
+
protected getTrack() {
|
|
29
|
+
return this.state.mediaStream?.getAudioTracks()[0];
|
|
35
30
|
}
|
|
36
31
|
}
|
|
@@ -7,14 +7,7 @@ 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 {
|
|
11
|
-
|
|
12
|
-
vi.mock('../devices.ts', () => {
|
|
13
|
-
console.log('MOCKING devices');
|
|
14
|
-
return {
|
|
15
|
-
disposeOfMediaStream: vi.fn(),
|
|
16
|
-
};
|
|
17
|
-
});
|
|
10
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
18
11
|
|
|
19
12
|
vi.mock('../../Call.ts', () => {
|
|
20
13
|
console.log('MOCKING Call');
|
|
@@ -32,11 +25,10 @@ class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMedia
|
|
|
32
25
|
public getStream = vi.fn(() => Promise.resolve(mockVideoStream()));
|
|
33
26
|
public publishStream = vi.fn();
|
|
34
27
|
public stopPublishStream = vi.fn();
|
|
35
|
-
public
|
|
36
|
-
public unmuteTracks = vi.fn();
|
|
28
|
+
public getTrack = () => this.state.mediaStream!.getVideoTracks()[0];
|
|
37
29
|
|
|
38
30
|
constructor(call: Call) {
|
|
39
|
-
super(call, new TestInputMediaDeviceManagerState());
|
|
31
|
+
super(call, new TestInputMediaDeviceManagerState(), TrackType.VIDEO);
|
|
40
32
|
}
|
|
41
33
|
}
|
|
42
34
|
|
|
@@ -135,11 +127,12 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
135
127
|
it('select device when status is enabled', async () => {
|
|
136
128
|
await manager.enable();
|
|
137
129
|
const prevStream = manager.state.mediaStream;
|
|
130
|
+
vi.spyOn(prevStream!.getVideoTracks()[0], 'stop');
|
|
138
131
|
|
|
139
132
|
const deviceId = mockVideoDevices[1].deviceId;
|
|
140
133
|
await manager.select(deviceId);
|
|
141
134
|
|
|
142
|
-
expect(
|
|
135
|
+
expect(prevStream!.getVideoTracks()[0].stop).toHaveBeenCalledWith();
|
|
143
136
|
});
|
|
144
137
|
|
|
145
138
|
it('select device when status is enabled and in call', async () => {
|
|
@@ -53,6 +53,7 @@ describe('DynascaleManager', () => {
|
|
|
53
53
|
call.state.updateOrAddParticipant('session-id', {
|
|
54
54
|
userId: 'user-id',
|
|
55
55
|
sessionId: 'session-id',
|
|
56
|
+
publishedTracks: [],
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
const element = document.createElement('div');
|
|
@@ -113,6 +114,7 @@ describe('DynascaleManager', () => {
|
|
|
113
114
|
call.state.updateOrAddParticipant('session-id', {
|
|
114
115
|
userId: 'user-id',
|
|
115
116
|
sessionId: 'session-id',
|
|
117
|
+
publishedTracks: [],
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
// @ts-ignore
|
|
@@ -120,6 +122,7 @@ describe('DynascaleManager', () => {
|
|
|
120
122
|
userId: 'user-id-local',
|
|
121
123
|
sessionId: 'session-id-local',
|
|
122
124
|
isLocalParticipant: true,
|
|
125
|
+
publishedTracks: [],
|
|
123
126
|
});
|
|
124
127
|
|
|
125
128
|
const cleanup = dynascaleManager.bindAudioElement(
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -253,6 +253,9 @@ export class Publisher {
|
|
|
253
253
|
// by an external factor as permission revokes, device disconnected, etc.
|
|
254
254
|
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
255
255
|
track.addEventListener('ended', handleTrackEnded);
|
|
256
|
+
if (!track.enabled) {
|
|
257
|
+
track.enabled = true;
|
|
258
|
+
}
|
|
256
259
|
|
|
257
260
|
transceiver = this.pc.addTransceiver(track, {
|
|
258
261
|
direction: 'sendonly',
|
|
@@ -310,17 +313,24 @@ export class Publisher {
|
|
|
310
313
|
if (
|
|
311
314
|
transceiver &&
|
|
312
315
|
transceiver.sender.track &&
|
|
313
|
-
|
|
316
|
+
(stopTrack
|
|
317
|
+
? transceiver.sender.track.readyState === 'live'
|
|
318
|
+
: transceiver.sender.track.enabled)
|
|
314
319
|
) {
|
|
315
320
|
stopTrack
|
|
316
321
|
? transceiver.sender.track.stop()
|
|
317
322
|
: (transceiver.sender.track.enabled = false);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
// We don't need to notify SFU if unpublishing in response to remote soft mute
|
|
324
|
+
if (!this.state.localParticipant?.publishedTracks.includes(trackType)) {
|
|
325
|
+
return;
|
|
326
|
+
} else {
|
|
327
|
+
return this.notifyTrackMuteStateChanged(
|
|
328
|
+
undefined,
|
|
329
|
+
transceiver.sender.track,
|
|
330
|
+
trackType,
|
|
331
|
+
true,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
324
334
|
}
|
|
325
335
|
};
|
|
326
336
|
|
|
@@ -330,6 +340,24 @@ export class Publisher {
|
|
|
330
340
|
* @param trackType the track type to check.
|
|
331
341
|
*/
|
|
332
342
|
isPublishing = (trackType: TrackType): boolean => {
|
|
343
|
+
const transceiverForTrackType = this.transceiverRegistry[trackType];
|
|
344
|
+
if (transceiverForTrackType && transceiverForTrackType.sender) {
|
|
345
|
+
const sender = transceiverForTrackType.sender;
|
|
346
|
+
return (
|
|
347
|
+
!!sender.track &&
|
|
348
|
+
sender.track.readyState === 'live' &&
|
|
349
|
+
sender.track.enabled
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Returns true if the given track type is currently live
|
|
357
|
+
*
|
|
358
|
+
* @param trackType the track type to check.
|
|
359
|
+
*/
|
|
360
|
+
isLive = (trackType: TrackType): boolean => {
|
|
333
361
|
const transceiverForTrackType = this.transceiverRegistry[trackType];
|
|
334
362
|
if (transceiverForTrackType && transceiverForTrackType.sender) {
|
|
335
363
|
const sender = transceiverForTrackType.sender;
|
|
@@ -180,6 +180,7 @@ describe('Publisher', () => {
|
|
|
180
180
|
|
|
181
181
|
expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id');
|
|
182
182
|
expect(state.localParticipant?.publishedTracks).toContain(TrackType.VIDEO);
|
|
183
|
+
expect(track.enabled).toBe(true);
|
|
183
184
|
expect(state.localParticipant?.videoStream).toEqual(mediaStream);
|
|
184
185
|
expect(transceiver.setCodecPreferences).toHaveBeenCalled();
|
|
185
186
|
expect(sfuClient.updateMuteState).toHaveBeenCalledWith(
|