Skip to content

Commit 9e82210

Browse files
committed
Support mid-call device changes
Signed-off-by: Šimon Brandner <[email protected]>
1 parent f1fbf51 commit 9e82210

File tree

2 files changed

+84
-16
lines changed

2 files changed

+84
-16
lines changed

src/client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ export class MatrixClient extends EventEmitter {
822822
protected checkTurnServersIntervalID: number;
823823
protected exportedOlmDeviceToImport: IOlmDevice;
824824
protected txnCtr = 0;
825-
protected mediaHandler = new MediaHandler();
825+
protected mediaHandler = new MediaHandler(this);
826826
protected pendingEventEncryption = new Map<string, Promise<void>>();
827827

828828
constructor(opts: IMatrixClientCreateOpts) {

src/webrtc/mediaHandler.ts

+83-15
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,72 @@ limitations under the License.
1818
*/
1919

2020
import { logger } from "../logger";
21+
import { MatrixClient } from "../client";
22+
import { CallState } from "./call";
2123

2224
export class MediaHandler {
2325
private audioInput: string;
2426
private videoInput: string;
25-
private userMediaStreams: MediaStream[] = [];
26-
private screensharingStreams: MediaStream[] = [];
27+
private localUserMediaStream?: MediaStream;
28+
public userMediaStreams: MediaStream[] = [];
29+
public screensharingStreams: MediaStream[] = [];
30+
31+
constructor(private client: MatrixClient) { }
2732

2833
/**
2934
* Set an audio input device to use for MatrixCalls
3035
* @param {string} deviceId the identifier for the device
3136
* undefined treated as unset
3237
*/
33-
public setAudioInput(deviceId: string): void {
38+
public async setAudioInput(deviceId: string): Promise<void> {
39+
if (this.audioInput === deviceId) return;
40+
3441
this.audioInput = deviceId;
42+
await this.updateLocalUsermediaStreams();
3543
}
3644

3745
/**
3846
* Set a video input device to use for MatrixCalls
3947
* @param {string} deviceId the identifier for the device
4048
* undefined treated as unset
4149
*/
42-
public setVideoInput(deviceId: string): void {
50+
public async setVideoInput(deviceId: string): Promise<void> {
51+
if (this.videoInput === deviceId) return;
52+
4353
this.videoInput = deviceId;
54+
await this.updateLocalUsermediaStreams();
55+
}
56+
57+
/**
58+
* Requests new usermedia streams and replace the old ones
59+
*/
60+
public async updateLocalUsermediaStreams(): Promise<void> {
61+
if (this.userMediaStreams.length === 0) return;
62+
63+
const callMediaStreamParams: Map<string, { audio: boolean, video: boolean }> = new Map();
64+
for (const call of this.client.callEventHandler.calls.values()) {
65+
callMediaStreamParams.set(call.callId, {
66+
audio: call.hasLocalUserMediaAudioTrack,
67+
video: call.hasLocalUserMediaVideoTrack,
68+
});
69+
}
70+
71+
for (const stream of this.userMediaStreams) {
72+
for (const track of stream.getTracks()) {
73+
track.stop();
74+
}
75+
}
76+
77+
this.userMediaStreams = [];
78+
this.localUserMediaStream = undefined;
79+
80+
for (const call of this.client.callEventHandler.calls.values()) {
81+
if (call.state === CallState.Ended || !callMediaStreamParams.has(call.callId)) continue;
82+
83+
const { audio, video } = callMediaStreamParams.get(call.callId);
84+
const stream = await this.getUserMediaStream(audio, video);
85+
await call.updateLocalUsermediaStream(stream);
86+
}
4487
}
4588

4689
public async hasAudioDevice(): Promise<boolean> {
@@ -65,20 +108,40 @@ export class MediaHandler {
65108

66109
let stream: MediaStream;
67110

68-
// Find a stream with matching tracks
69-
const matchingStream = this.userMediaStreams.find((stream) => {
70-
if (shouldRequestAudio !== (stream.getAudioTracks().length > 0)) return false;
71-
if (shouldRequestVideo !== (stream.getVideoTracks().length > 0)) return false;
72-
return true;
73-
});
74-
75-
if (matchingStream) {
76-
logger.log("Cloning user media stream", matchingStream.id);
77-
stream = matchingStream.clone();
78-
} else {
111+
if (
112+
!this.localUserMediaStream ||
113+
(this.localUserMediaStream.getAudioTracks().length === 0 && shouldRequestAudio) ||
114+
(this.localUserMediaStream.getVideoTracks().length === 0 && shouldRequestVideo)
115+
) {
79116
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
80117
logger.log("Getting user media with constraints", constraints);
81118
stream = await navigator.mediaDevices.getUserMedia(constraints);
119+
120+
for (const track of stream.getTracks()) {
121+
const settings = track.getSettings();
122+
123+
if (track.kind === "audio") {
124+
this.audioInput = settings.deviceId;
125+
} else if (track.kind === "video") {
126+
this.videoInput = settings.deviceId;
127+
}
128+
}
129+
130+
this.localUserMediaStream = stream;
131+
} else {
132+
stream = this.localUserMediaStream.clone();
133+
134+
if (!shouldRequestAudio) {
135+
for (const track of stream.getAudioTracks()) {
136+
stream.removeTrack(track);
137+
}
138+
}
139+
140+
if (!shouldRequestVideo) {
141+
for (const track of stream.getVideoTracks()) {
142+
stream.removeTrack(track);
143+
}
144+
}
82145
}
83146

84147
if (reusable) {
@@ -103,6 +166,10 @@ export class MediaHandler {
103166
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
104167
this.userMediaStreams.splice(index, 1);
105168
}
169+
170+
if (this.localUserMediaStream === mediaStream) {
171+
this.localUserMediaStream = undefined;
172+
}
106173
}
107174

108175
/**
@@ -174,6 +241,7 @@ export class MediaHandler {
174241

175242
this.userMediaStreams = [];
176243
this.screensharingStreams = [];
244+
this.localUserMediaStream = undefined;
177245
}
178246

179247
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {

0 commit comments

Comments
 (0)