You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Fix screenshare failing after several attempts (#2771)
* Fix screenshare failing after several attempts Re-use any existing transceivers when screen sharing. This prevents transceivers accumulating and making the SDP too big: see linked bug. This also switches from `addTrack()` to `addTransceiver ()` which is not that large of a change, other than having to explicitly find the transceivers after an offer has arrived rather than just adding tracks and letting WebRTC take care of it. Fixes https://github.com/vector-im/element-call/issues/625 * Fix tests * Unused import * Use a map instead of an array * Add comment * more comment * Remove commented code * Remove unintentional debugging * Add test for screenshare transceiver re-use * Type alias for transceiver map
This commit is contained in:
@@ -104,12 +104,12 @@ export class MockRTCPeerConnection {
|
|||||||
private negotiationNeededListener: () => void;
|
private negotiationNeededListener: () => void;
|
||||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||||
private needsNegotiation = false;
|
public needsNegotiation = false;
|
||||||
public readyToNegotiate: Promise<void>;
|
public readyToNegotiate: Promise<void>;
|
||||||
private onReadyToNegotiate: () => void;
|
private onReadyToNegotiate: () => void;
|
||||||
localDescription: RTCSessionDescription;
|
localDescription: RTCSessionDescription;
|
||||||
signalingState: RTCSignalingState = "stable";
|
signalingState: RTCSignalingState = "stable";
|
||||||
public senders: MockRTCRtpSender[] = [];
|
public transceivers: MockRTCRtpTransceiver[] = [];
|
||||||
|
|
||||||
public static triggerAllNegotiations(): void {
|
public static triggerAllNegotiations(): void {
|
||||||
for (const inst of this.instances) {
|
for (const inst of this.instances) {
|
||||||
@@ -169,12 +169,23 @@ export class MockRTCPeerConnection {
|
|||||||
}
|
}
|
||||||
close() { }
|
close() { }
|
||||||
getStats() { return []; }
|
getStats() { return []; }
|
||||||
addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
|
addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
|
||||||
this.needsNegotiation = true;
|
this.needsNegotiation = true;
|
||||||
this.onReadyToNegotiate();
|
this.onReadyToNegotiate();
|
||||||
|
|
||||||
const newSender = new MockRTCRtpSender(track);
|
const newSender = new MockRTCRtpSender(track);
|
||||||
this.senders.push(newSender);
|
const newReceiver = new MockRTCRtpReceiver(track);
|
||||||
return newSender;
|
|
||||||
|
const newTransceiver = new MockRTCRtpTransceiver(this);
|
||||||
|
newTransceiver.sender = newSender as unknown as RTCRtpSender;
|
||||||
|
newTransceiver.receiver = newReceiver as unknown as RTCRtpReceiver;
|
||||||
|
|
||||||
|
this.transceivers.push(newTransceiver);
|
||||||
|
|
||||||
|
return newTransceiver;
|
||||||
|
}
|
||||||
|
addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
|
||||||
|
return this.addTransceiver(track).sender as unknown as MockRTCRtpSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTrack() {
|
removeTrack() {
|
||||||
@@ -182,9 +193,8 @@ export class MockRTCPeerConnection {
|
|||||||
this.onReadyToNegotiate();
|
this.onReadyToNegotiate();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSenders(): MockRTCRtpSender[] { return this.senders; }
|
getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
|
||||||
|
getSenders(): MockRTCRtpSender[] { return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender); }
|
||||||
getTransceivers = jest.fn().mockReturnValue([]);
|
|
||||||
|
|
||||||
doNegotiation() {
|
doNegotiation() {
|
||||||
if (this.needsNegotiation && this.negotiationNeededListener) {
|
if (this.needsNegotiation && this.negotiationNeededListener) {
|
||||||
@@ -198,7 +208,23 @@ export class MockRTCRtpSender {
|
|||||||
constructor(public track: MockMediaStreamTrack) { }
|
constructor(public track: MockMediaStreamTrack) { }
|
||||||
|
|
||||||
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||||
setCodecPreferences(prefs: RTCRtpCodecCapability[]): void {}
|
}
|
||||||
|
|
||||||
|
export class MockRTCRtpReceiver {
|
||||||
|
constructor(public track: MockMediaStreamTrack) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockRTCRtpTransceiver {
|
||||||
|
constructor(private peerConn: MockRTCPeerConnection) {}
|
||||||
|
|
||||||
|
public sender: RTCRtpSender;
|
||||||
|
public receiver: RTCRtpReceiver;
|
||||||
|
|
||||||
|
public set direction(_: string) {
|
||||||
|
this.peerConn.needsNegotiation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockMediaStreamTrack {
|
export class MockMediaStreamTrack {
|
||||||
|
@@ -41,7 +41,6 @@ import {
|
|||||||
installWebRTCMocks,
|
installWebRTCMocks,
|
||||||
MockRTCPeerConnection,
|
MockRTCPeerConnection,
|
||||||
SCREENSHARE_STREAM_ID,
|
SCREENSHARE_STREAM_ID,
|
||||||
MockRTCRtpSender,
|
|
||||||
} from "../../test-utils/webrtc";
|
} from "../../test-utils/webrtc";
|
||||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||||
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||||
@@ -370,17 +369,15 @@ describe('Call', function() {
|
|||||||
).typed(),
|
).typed(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const usermediaSenders: Array<RTCRtpSender> = (call as any).usermediaSenders;
|
// XXX: Lots of inspecting the prvate state of the call object here
|
||||||
|
const transceivers: Map<string, RTCRtpTransceiver> = (call as any).transceivers;
|
||||||
|
|
||||||
expect(call.localUsermediaStream.id).toBe("stream");
|
expect(call.localUsermediaStream.id).toBe("stream");
|
||||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
|
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
|
||||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||||
expect(usermediaSenders.find((sender) => {
|
// call has a function for generating these but we hardcode here to avoid exporting it
|
||||||
return sender?.track?.kind === "audio";
|
expect(transceivers.get("m.usermedia:audio").sender.track.id).toBe("new_audio_track");
|
||||||
}).track.id).toBe("new_audio_track");
|
expect(transceivers.get("m.usermedia:video").sender.track.id).toBe("video_track");
|
||||||
expect(usermediaSenders.find((sender) => {
|
|
||||||
return sender?.track?.kind === "video";
|
|
||||||
}).track.id).toBe("video_track");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle upgrade to video call", async () => {
|
it("should handle upgrade to video call", async () => {
|
||||||
@@ -400,16 +397,13 @@ describe('Call', function() {
|
|||||||
// setLocalVideoMuted probably?
|
// setLocalVideoMuted probably?
|
||||||
await (call as any).upgradeCall(false, true);
|
await (call as any).upgradeCall(false, true);
|
||||||
|
|
||||||
const usermediaSenders: Array<RTCRtpSender> = (call as any).usermediaSenders;
|
// XXX: More inspecting private state of the call object
|
||||||
|
const transceivers: Map<string, RTCRtpTransceiver> = (call as any).transceivers;
|
||||||
|
|
||||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("usermedia_audio_track");
|
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("usermedia_audio_track");
|
||||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("usermedia_video_track");
|
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("usermedia_video_track");
|
||||||
expect(usermediaSenders.find((sender) => {
|
expect(transceivers.get("m.usermedia:audio").sender.track.id).toBe("usermedia_audio_track");
|
||||||
return sender?.track?.kind === "audio";
|
expect(transceivers.get("m.usermedia:video").sender.track.id).toBe("usermedia_video_track");
|
||||||
}).track.id).toBe("usermedia_audio_track");
|
|
||||||
expect(usermediaSenders.find((sender) => {
|
|
||||||
return sender?.track?.kind === "video";
|
|
||||||
}).track.id).toBe("usermedia_video_track");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle SDPStreamMetadata changes", async () => {
|
it("should handle SDPStreamMetadata changes", async () => {
|
||||||
@@ -479,6 +473,23 @@ describe('Call', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("should deduce the call type correctly", () => {
|
describe("should deduce the call type correctly", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// start an incoming call, but add no feeds
|
||||||
|
await call.initWithInvite({
|
||||||
|
getContent: jest.fn().mockReturnValue({
|
||||||
|
version: "1",
|
||||||
|
call_id: "call_id",
|
||||||
|
party_id: "remote_party_id",
|
||||||
|
lifetime: CALL_LIFETIME,
|
||||||
|
offer: {
|
||||||
|
sdp: DUMMY_SDP,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getSender: () => "@test:foo",
|
||||||
|
getLocalAge: () => 1,
|
||||||
|
} as unknown as MatrixEvent);
|
||||||
|
});
|
||||||
|
|
||||||
it("if no video", async () => {
|
it("if no video", async () => {
|
||||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||||
|
|
||||||
@@ -1057,9 +1068,24 @@ describe('Call', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Screen sharing", () => {
|
describe("Screen sharing", () => {
|
||||||
|
const waitNegotiateFunc = resolve => {
|
||||||
|
mockSendEvent.mockImplementationOnce(() => {
|
||||||
|
// Note that the peer connection here is a dummy one and always returns
|
||||||
|
// dummy SDP, so there's not much point returning the content: the SDP will
|
||||||
|
// always be the same.
|
||||||
|
resolve();
|
||||||
|
return Promise.resolve({ event_id: "foo" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await startVoiceCall(client, call);
|
await startVoiceCall(client, call);
|
||||||
|
|
||||||
|
const sendNegotiatePromise = new Promise<void>(waitNegotiateFunc);
|
||||||
|
|
||||||
|
MockRTCPeerConnection.triggerAllNegotiations();
|
||||||
|
await sendNegotiatePromise;
|
||||||
|
|
||||||
await call.onAnswerReceived(makeMockEvent("@test:foo", {
|
await call.onAnswerReceived(makeMockEvent("@test:foo", {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"call_id": call.callId,
|
"call_id": call.callId,
|
||||||
@@ -1090,12 +1116,7 @@ describe('Call', function() {
|
|||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
|
|
||||||
mockSendEvent.mockReset();
|
mockSendEvent.mockReset();
|
||||||
const sendNegotiatePromise = new Promise<void>(resolve => {
|
const sendNegotiatePromise = new Promise<void>(waitNegotiateFunc);
|
||||||
mockSendEvent.mockImplementationOnce(() => {
|
|
||||||
resolve();
|
|
||||||
return Promise.resolve({ event_id: "foo" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
MockRTCPeerConnection.triggerAllNegotiations();
|
MockRTCPeerConnection.triggerAllNegotiations();
|
||||||
await sendNegotiatePromise;
|
await sendNegotiatePromise;
|
||||||
@@ -1130,29 +1151,52 @@ describe('Call', function() {
|
|||||||
headerExtensions: [],
|
headerExtensions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const prom = new Promise<void>(resolve => {
|
mockSendEvent.mockReset();
|
||||||
const mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
const sendNegotiatePromise = new Promise<void>(waitNegotiateFunc);
|
||||||
mockPeerConn.addTrack = jest.fn().mockImplementation((track: MockMediaStreamTrack) => {
|
|
||||||
const mockSender = new MockRTCRtpSender(track);
|
|
||||||
mockPeerConn.getTransceivers.mockReturnValue([{
|
|
||||||
sender: mockSender,
|
|
||||||
setCodecPreferences: (prefs: RTCRtpCodecCapability[]) => {
|
|
||||||
expect(prefs).toEqual([
|
|
||||||
expect.objectContaining({ mimeType: "video/somethingelse" }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
}]);
|
|
||||||
|
|
||||||
return mockSender;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await call.setScreensharingEnabled(true);
|
await call.setScreensharingEnabled(true);
|
||||||
MockRTCPeerConnection.triggerAllNegotiations();
|
MockRTCPeerConnection.triggerAllNegotiations();
|
||||||
|
|
||||||
await prom;
|
await sendNegotiatePromise;
|
||||||
|
|
||||||
|
const mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
||||||
|
expect(
|
||||||
|
mockPeerConn.transceivers[mockPeerConn.transceivers.length - 1].setCodecPreferences,
|
||||||
|
).toHaveBeenCalledWith([expect.objectContaining({ mimeType: "video/somethingelse" })]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-uses transceiver when screen sharing is re-enabled", async () => {
|
||||||
|
const mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
||||||
|
|
||||||
|
// sanity check: we should start with one transciever (user media audio)
|
||||||
|
expect(mockPeerConn.transceivers.length).toEqual(1);
|
||||||
|
|
||||||
|
const screenshareOnProm1 = new Promise<void>(waitNegotiateFunc);
|
||||||
|
|
||||||
|
await call.setScreensharingEnabled(true);
|
||||||
|
MockRTCPeerConnection.triggerAllNegotiations();
|
||||||
|
|
||||||
|
await screenshareOnProm1;
|
||||||
|
|
||||||
|
// we should now have another transciever for the screenshare
|
||||||
|
expect(mockPeerConn.transceivers.length).toEqual(2);
|
||||||
|
|
||||||
|
const screenshareOffProm = new Promise<void>(waitNegotiateFunc);
|
||||||
|
await call.setScreensharingEnabled(false);
|
||||||
|
MockRTCPeerConnection.triggerAllNegotiations();
|
||||||
|
await screenshareOffProm;
|
||||||
|
|
||||||
|
// both transceivers should still be there
|
||||||
|
expect(mockPeerConn.transceivers.length).toEqual(2);
|
||||||
|
|
||||||
|
const screenshareOnProm2 = new Promise<void>(waitNegotiateFunc);
|
||||||
|
await call.setScreensharingEnabled(true);
|
||||||
|
MockRTCPeerConnection.triggerAllNegotiations();
|
||||||
|
await screenshareOnProm2;
|
||||||
|
|
||||||
|
// should still be two, ie. another one should not have been created
|
||||||
|
// when re-enabling the screen share.
|
||||||
|
expect(mockPeerConn.transceivers.length).toEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -308,6 +308,16 @@ export type CallEventHandlerMap = {
|
|||||||
[CallEvent.SendVoipEvent]: (event: Record<string, any>) => void;
|
[CallEvent.SendVoipEvent]: (event: Record<string, any>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The key of the transceiver map (purpose + media type, separated by ':')
|
||||||
|
type TransceiverKey = string;
|
||||||
|
|
||||||
|
// generates keys for the map of transceivers
|
||||||
|
// kind is unfortunately a string rather than MediaType as this is the type of
|
||||||
|
// track.kind
|
||||||
|
function getTransceiverKey(purpose: SDPStreamMetadataPurpose, kind: TransceiverKey): string {
|
||||||
|
return purpose + ':' + kind;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new Matrix Call.
|
* Construct a new Matrix Call.
|
||||||
* @constructor
|
* @constructor
|
||||||
@@ -345,8 +355,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
private candidateSendTries = 0;
|
private candidateSendTries = 0;
|
||||||
private candidatesEnded = false;
|
private candidatesEnded = false;
|
||||||
private feeds: Array<CallFeed> = [];
|
private feeds: Array<CallFeed> = [];
|
||||||
private usermediaSenders: Array<RTCRtpSender> = [];
|
|
||||||
private screensharingSenders: Array<RTCRtpSender> = [];
|
// our transceivers for each purpose and type of media
|
||||||
|
private transceivers = new Map<TransceiverKey, RTCRtpTransceiver>();
|
||||||
|
|
||||||
private inviteOrAnswerSent = false;
|
private inviteOrAnswerSent = false;
|
||||||
private waitForLocalAVStream: boolean;
|
private waitForLocalAVStream: boolean;
|
||||||
private successor: MatrixCall;
|
private successor: MatrixCall;
|
||||||
@@ -634,6 +646,18 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
audioMuted,
|
audioMuted,
|
||||||
videoMuted,
|
videoMuted,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// gather transceivers from the new tracks so that we can use the same ones for tracks that
|
||||||
|
// we add later. We only do this for user media streams though: screenshare streams just always
|
||||||
|
// get their own unidirectional transceiver since a bidirectional screen share is pretty rare
|
||||||
|
// (we *could* re-use an existing recvonly transceiver for this, but it's simpler to just not).
|
||||||
|
if (purpose == SDPStreamMetadataPurpose.Usermedia) {
|
||||||
|
for (const track of stream.getTracks()) {
|
||||||
|
const transceiver = this.peerConn.getTransceivers().find(t => t.receiver.track == track);
|
||||||
|
this.transceivers.set(getTransceiverKey(purpose, track.kind), transceiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -675,6 +699,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
stream,
|
stream,
|
||||||
purpose,
|
purpose,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
for (const track of stream.getTracks()) {
|
||||||
|
const transceiver = this.peerConn.getTransceivers().find(t => t.receiver.track == track);
|
||||||
|
this.transceivers.set(getTransceiverKey(purpose, track.kind), transceiver);
|
||||||
|
}
|
||||||
|
|
||||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||||
|
|
||||||
logger.info(`Call ${this.callId} pushed remote stream (id="${stream.id}", active="${stream.active}")`);
|
logger.info(`Call ${this.callId} pushed remote stream (id="${stream.id}", active="${stream.active}")`);
|
||||||
@@ -722,11 +752,6 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
this.feeds.push(callFeed);
|
this.feeds.push(callFeed);
|
||||||
|
|
||||||
if (addToPeerConnection) {
|
if (addToPeerConnection) {
|
||||||
const senderArray = callFeed.purpose === SDPStreamMetadataPurpose.Usermedia ?
|
|
||||||
this.usermediaSenders : this.screensharingSenders;
|
|
||||||
// Empty the array
|
|
||||||
senderArray.splice(0, senderArray.length);
|
|
||||||
|
|
||||||
for (const track of callFeed.stream.getTracks()) {
|
for (const track of callFeed.stream.getTracks()) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Call ${this.callId} ` +
|
`Call ${this.callId} ` +
|
||||||
@@ -738,7 +763,27 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
`enabled=${track.enabled}` +
|
`enabled=${track.enabled}` +
|
||||||
`) to peer connection`,
|
`) to peer connection`,
|
||||||
);
|
);
|
||||||
senderArray.push(this.peerConn.addTrack(track, callFeed.stream));
|
|
||||||
|
const tKey = getTransceiverKey(callFeed.purpose, track.kind);
|
||||||
|
if (this.transceivers.has(tKey)) {
|
||||||
|
// we already have a sender, so we re-use it. We try to re-use transceivers as much
|
||||||
|
// as possible because they can't be removed once added, so otherwise they just
|
||||||
|
// accumulate which makes the SDP very large very quickly: in fact it only takes
|
||||||
|
// about 6 video tracks to exceed the maximum size of an Olm-encrypted
|
||||||
|
// Matrix event.
|
||||||
|
const transceiver = this.transceivers.get(tKey);
|
||||||
|
|
||||||
|
transceiver.sender.replaceTrack(track);
|
||||||
|
// set the direction to indicate we're going to start sending again
|
||||||
|
// (this will trigger the re-negotiation)
|
||||||
|
transceiver.direction = transceiver.direction === "inactive" ? "sendonly" : "sendrecv";
|
||||||
|
} else {
|
||||||
|
// create a new one: pass the track in and everything happens automatically
|
||||||
|
this.transceivers.set(tKey, this.peerConn.addTransceiver(track, {
|
||||||
|
streams: [callFeed.stream],
|
||||||
|
direction: callFeed.purpose === SDPStreamMetadataPurpose.Usermedia ? "sendrecv" : "sendonly",
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,20 +804,23 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
* @param callFeed to remove
|
* @param callFeed to remove
|
||||||
*/
|
*/
|
||||||
public removeLocalFeed(callFeed: CallFeed): void {
|
public removeLocalFeed(callFeed: CallFeed): void {
|
||||||
const senderArray = callFeed.purpose === SDPStreamMetadataPurpose.Usermedia
|
const audioTransceiverKey = getTransceiverKey(callFeed.purpose, "audio");
|
||||||
? this.usermediaSenders
|
const videoTransceiverKey = getTransceiverKey(callFeed.purpose, "video");
|
||||||
: this.screensharingSenders;
|
|
||||||
|
|
||||||
for (const sender of senderArray) {
|
for (const transceiverKey of [audioTransceiverKey, videoTransceiverKey]) {
|
||||||
this.peerConn.removeTrack(sender);
|
// this is slightly mixing the track and transceiver API but is basically just shorthand.
|
||||||
|
// There is no way to actually remove a transceiver, so this just sets it to inactive
|
||||||
|
// (or recvonly) and replaces the source with nothing.
|
||||||
|
if (this.transceivers.has(transceiverKey)) {
|
||||||
|
const transceiver = this.transceivers.get(transceiverKey);
|
||||||
|
if (transceiver.sender) this.peerConn.removeTrack(transceiver.sender);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callFeed.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
if (callFeed.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
||||||
this.client.getMediaHandler().stopScreensharingStream(callFeed.stream);
|
this.client.getMediaHandler().stopScreensharingStream(callFeed.stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty the array
|
|
||||||
senderArray.splice(0, senderArray.length);
|
|
||||||
this.deleteFeed(callFeed);
|
this.deleteFeed(callFeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,9 +1187,19 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const sender of this.screensharingSenders) {
|
const audioTransceiver = this.transceivers.get(getTransceiverKey(
|
||||||
this.peerConn.removeTrack(sender);
|
SDPStreamMetadataPurpose.Screenshare, "audio",
|
||||||
|
));
|
||||||
|
const videoTransceiver = this.transceivers.get(getTransceiverKey(
|
||||||
|
SDPStreamMetadataPurpose.Screenshare, "video",
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const transceiver of [audioTransceiver, videoTransceiver]) {
|
||||||
|
// this is slightly mixing the track and transceiver API but is basically just shorthand
|
||||||
|
// for removing the sender.
|
||||||
|
if (transceiver && transceiver.sender) this.peerConn.removeTrack(transceiver.sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream);
|
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream);
|
||||||
this.deleteFeedByStream(this.localScreensharingStream);
|
this.deleteFeedByStream(this.localScreensharingStream);
|
||||||
return false;
|
return false;
|
||||||
@@ -1167,9 +1225,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
const track = stream.getTracks().find((track) => {
|
const track = stream.getTracks().find((track) => {
|
||||||
return track.kind === "video";
|
return track.kind === "video";
|
||||||
});
|
});
|
||||||
const sender = this.usermediaSenders.find((sender) => {
|
|
||||||
return sender.track?.kind === "video";
|
const sender = this.transceivers.get(getTransceiverKey(
|
||||||
});
|
SDPStreamMetadataPurpose.Usermedia, "video",
|
||||||
|
)).sender;
|
||||||
|
|
||||||
sender.replaceTrack(track);
|
sender.replaceTrack(track);
|
||||||
|
|
||||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
|
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
|
||||||
@@ -1183,9 +1243,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
const track = this.localUsermediaStream.getTracks().find((track) => {
|
const track = this.localUsermediaStream.getTracks().find((track) => {
|
||||||
return track.kind === "video";
|
return track.kind === "video";
|
||||||
});
|
});
|
||||||
const sender = this.usermediaSenders.find((sender) => {
|
const sender = this.transceivers.get(getTransceiverKey(
|
||||||
return sender.track?.kind === "video";
|
SDPStreamMetadataPurpose.Usermedia, "video",
|
||||||
});
|
)).sender;
|
||||||
sender.replaceTrack(track);
|
sender.replaceTrack(track);
|
||||||
|
|
||||||
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream);
|
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream);
|
||||||
@@ -1219,15 +1279,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
this.localUsermediaStream.addTrack(track);
|
this.localUsermediaStream.addTrack(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSenders = [];
|
|
||||||
|
|
||||||
for (const track of stream.getTracks()) {
|
for (const track of stream.getTracks()) {
|
||||||
const oldSender = this.usermediaSenders.find((sender) => {
|
const tKey = getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, track.kind);
|
||||||
return sender.track?.kind === track.kind;
|
|
||||||
});
|
|
||||||
|
|
||||||
let newSender: RTCRtpSender;
|
|
||||||
|
|
||||||
|
const oldSender = this.transceivers.get(tKey)?.sender;
|
||||||
|
let added = false;
|
||||||
|
if (oldSender) {
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Call ${this.callId} `+
|
`Call ${this.callId} `+
|
||||||
@@ -1239,8 +1296,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
`) to peer connection`,
|
`) to peer connection`,
|
||||||
);
|
);
|
||||||
await oldSender.replaceTrack(track);
|
await oldSender.replaceTrack(track);
|
||||||
newSender = oldSender;
|
added = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.warn(`replaceTrack failed: adding new transceiver instead`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!added) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Call ${this.callId} `+
|
`Call ${this.callId} `+
|
||||||
`Adding track (` +
|
`Adding track (` +
|
||||||
@@ -1250,13 +1312,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
`streamPurpose="${callFeed.purpose}"` +
|
`streamPurpose="${callFeed.purpose}"` +
|
||||||
`) to peer connection`,
|
`) to peer connection`,
|
||||||
);
|
);
|
||||||
newSender = this.peerConn.addTrack(track, this.localUsermediaStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
newSenders.push(newSender);
|
this.transceivers.set(tKey, this.peerConn.addTransceiver(track, {
|
||||||
|
streams: [this.localUsermediaStream],
|
||||||
|
direction: "sendrecv",
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.usermediaSenders = newSenders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2109,17 +2171,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const trans of this.peerConn.getTransceivers()) {
|
const screenshareVideoTransceiver = this.transceivers.get(getTransceiverKey(
|
||||||
if (
|
SDPStreamMetadataPurpose.Screenshare, "video",
|
||||||
this.screensharingSenders.includes(trans.sender) &&
|
));
|
||||||
(
|
if (screenshareVideoTransceiver) screenshareVideoTransceiver.setCodecPreferences(codecs);
|
||||||
trans.sender.track?.kind === "video" ||
|
|
||||||
trans.receiver.track?.kind === "video"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
trans.setCodecPreferences(codecs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNegotiationNeeded = async (): Promise<void> => {
|
private onNegotiationNeeded = async (): Promise<void> => {
|
||||||
|
@@ -607,7 +607,9 @@ export class GroupCall extends TypedEventEmitter<
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await Promise.all(this.calls.map(call => call.removeLocalFeed(call.localScreensharingFeed)));
|
await Promise.all(this.calls.map(call => {
|
||||||
|
if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed);
|
||||||
|
}));
|
||||||
this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream);
|
this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream);
|
||||||
this.removeScreenshareFeed(this.localScreenshareFeed);
|
this.removeScreenshareFeed(this.localScreenshareFeed);
|
||||||
this.localScreenshareFeed = undefined;
|
this.localScreenshareFeed = undefined;
|
||||||
|
Reference in New Issue
Block a user