Compare commits

...

16 Commits

Author SHA1 Message Date
RiotRobot 45e9b3ac68 v9.2.0-rc.1 2020-11-18 15:54:10 +00:00
RiotRobot a076e3f0fc Prepare changelog for v9.2.0-rc.1 2020-11-18 15:54:09 +00:00
David Baker 34882cc438 Merge pull request #1532 from matrix-org/dbkr/call_hold
Implement call holding functionality
2020-11-10 08:41:26 +00:00
RiotRobot 5ac00e3465 Merge branch 'master' into develop 2020-11-09 16:21:25 +00:00
David Baker da423ed508 Add comment that the timeout probably isn't
After a fair emount of investigation, it looks like there's not
really a particularly elegant way to have an environment where
setTimeout has a return type that makes sensible assertions in both
node and the browser. Ideally we want it to be something that asserts
that we don't try to use it as a NodeJS.Timeout (ie. call the methods)
because that will break in a browser. In practice, this involves making
wrappers or redefining the timeout functions or some similar kind of
hackery, the evils of which probably offset having perfect typing.

https://matrix.to/#/!bEWtlqtDwCLFIAKAcv:matrix.org/$vpFWl7p1_8A858RAccO0gud3sNVWIDNxNELRjdqaZQ4?via=matrix.org&via=mozilla.org&via=vector.modular.im
for the discussion
2020-11-09 15:59:54 +00:00
Bruno Windels 11c4337cfc Merge pull request #1537 from matrix-org/bwindels/dehydration
Support awaitable one-time dehydration
2020-11-04 15:22:09 +00:00
Bruno Windels 458164384d add client method for one-time dehydration that can be awaited 2020-11-04 16:05:31 +01:00
Bruno Windels 13c7f55a79 split up setKey and setKeyAndQueue
as dehydrating in the background prevents use-cases where you
want to await the creation of the dehydrated device
2020-11-04 16:02:31 +01:00
Bruno Windels 4ab675863a fix typo 2020-11-04 16:00:53 +01:00
Michael Telatynski f11103bfcc Merge pull request #1534 from matrix-org/t3chguy/fix/15604
Client set profile methods update own user
2020-11-04 13:59:58 +00:00
Bruno Windels b56936003d stop dehydration timer when stopping the client 2020-11-03 10:13:19 +01:00
Michael Telatynski 645842f0fd Update comments 2020-11-02 18:09:32 +00:00
Michael Telatynski 66aa9c4831 pass a presence event 2020-11-02 17:39:25 +00:00
Michael Telatynski 7de0ca2048 pass args 2020-11-02 17:29:14 +00:00
Michael Telatynski 5bbc5cad9f Client setProfile methods update own user 2020-11-02 17:20:03 +00:00
David Baker 33d1a33a17 Implement call holding functionality
Using m.call.negotiate
2020-10-29 17:54:54 +00:00
8 changed files with 321 additions and 119 deletions
+11
View File
@@ -1,3 +1,14 @@
Changes in [9.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.2.0-rc.1) (2020-11-18)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.1.0...v9.2.0-rc.1)
* Implement call holding functionality
[\#1532](https://github.com/matrix-org/matrix-js-sdk/pull/1532)
* Support awaitable one-time dehydration
[\#1537](https://github.com/matrix-org/matrix-js-sdk/pull/1537)
* Client set profile methods update own user
[\#1534](https://github.com/matrix-org/matrix-js-sdk/pull/1534)
Changes in [9.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0) (2020-11-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.1.0-rc.1...v9.1.0)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "9.1.0",
"version": "9.2.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
+1
View File
@@ -47,6 +47,7 @@ export enum EventType {
CallHangup = "m.call.hangup",
CallReject = "m.call.reject",
CallSelectAnswer = "m.call.select_answer",
CallNegotiate = "m.call.negotiate",
KeyVerificationRequest = "m.key.verification.request",
KeyVerificationStart = "m.key.verification.start",
KeyVerificationCancel = "m.key.verification.cancel",
+43 -7
View File
@@ -577,11 +577,33 @@ MatrixClient.prototype.setDehydrationKey = async function(
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
return await this._crypto._dehydrationManager.setDehydrationKey(
return await this._crypto._dehydrationManager.setKeyAndQueue(
key, keyInfo, deviceDisplayName,
);
};
/**
* Creates a new dehydrated device (without queuing periodic dehydration)
* @param {Uint8Array} key the dehydration key
* @param {object} [keyInfo] Information about the key. Primarily for
* information about how to generate the key from a passphrase.
* @param {string} [deviceDisplayName] The device display name for the
* dehydrated device.
* @return {Promise<String>} the device id of the newly created dehydrated device
*/
MatrixClient.prototype.createDehydratedDevice = async function(
key, keyInfo = {}, deviceDisplayName = undefined,
) {
if (!(this._crypto)) {
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
await this._crypto._dehydrationManager.setKey(
key, keyInfo, deviceDisplayName,
);
return await this._crypto._dehydrationManager.dehydrateDevice();
};
MatrixClient.prototype.exportDevice = async function() {
if (!(this._crypto)) {
logger.warn('not exporting device if crypto is not enabled');
@@ -3699,25 +3721,39 @@ MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
/**
* @param {string} name
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setDisplayName = function(name, callback) {
return this.setProfileInfo(
MatrixClient.prototype.setDisplayName = async function(name, callback) {
const prom = await this.setProfileInfo(
"displayname", { displayname: name }, callback,
);
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
const user = this.getUser(this.getUserId());
if (user) {
user.displayName = name;
user.emit("User.displayName", user.events.presence, user);
}
return prom;
};
/**
* @param {string} url
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setAvatarUrl = function(url, callback) {
return this.setProfileInfo(
MatrixClient.prototype.setAvatarUrl = async function(url, callback) {
const prom = await this.setProfileInfo(
"avatar_url", { avatar_url: url }, callback,
);
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
const user = this.getUser(this.getUserId());
if (user) {
user.avatarUrl = url;
user.emit("User.avatarUrl", user.events.presence, user);
}
return prom;
};
/**
+28 -5
View File
@@ -77,10 +77,23 @@ export class DehydrationManager {
},
);
}
async setDehydrationKey(
/** set the key, and queue periodic dehydration to the server in the background */
async setKeyAndQueueDehydration(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<void> {
const matches = await this.setKey(key, keyInfo, deviceDisplayName);
if (!matches) {
// start dehydration in the background
this.dehydrateDevice();
}
}
async setKey(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<boolean> {
if (!key) {
// unsetting the key -- cancel any pending dehydration task
if (this.timeoutId) {
@@ -104,7 +117,7 @@ export class DehydrationManager {
// Check to see if it's the same key as before. If it's different,
// dehydrate a new device. If it's the same, we can keep the same
// device. (Assume that keyInfo and deviceDisplayNamme will be the
// device. (Assume that keyInfo and deviceDisplayName will be the
// same if the key is the same.)
let matches: boolean = this.key && key.length == this.key.length;
for (let i = 0; matches && i < key.length; i++) {
@@ -116,11 +129,12 @@ export class DehydrationManager {
this.key = key;
this.keyInfo = keyInfo;
this.deviceDisplayName = deviceDisplayName;
// start dehydration in the background
this.dehydrateDevice();
}
return matches;
}
private async dehydrateDevice(): Promise<void> {
/** returns the device id of the newly created dehydrated device */
async dehydrateDevice(): Promise<string> {
if (this.inProgress) {
logger.log("Dehydration already in progress -- not starting new dehydration");
return;
@@ -258,8 +272,17 @@ export class DehydrationManager {
this.timeoutId = global.setTimeout(
this.dehydrateDevice.bind(this), oneweek,
);
return deviceId;
} finally {
this.inProgress = false;
}
}
private stop() {
if (this.timeoutId) {
global.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
}
}
+1
View File
@@ -1752,6 +1752,7 @@ Crypto.prototype.start = function() {
Crypto.prototype.stop = function() {
this._outgoingRoomKeyRequestManager.stop();
this._deviceList.stop();
this._dehydrationManager.stop();
};
/**
+227 -106
View File
@@ -91,6 +91,9 @@ export enum CallEvent {
State = 'state',
Error = 'error',
Replaced = 'replaced',
// The value of isLocalOnHold() has changed
HoldUnhold = 'hold_unhold',
}
enum MediaQueueId {
@@ -163,6 +166,11 @@ export enum CallErrorCode {
* The call was replaced by another call
*/
Replaced = 'replaced',
/**
* Signalling for the call could not be sent (other than the initial invite)
*/
SignallingFailed = 'signalling_timeout',
}
/**
@@ -236,7 +244,17 @@ export class MatrixCall extends EventEmitter {
// The party ID of the other side: undefined if we haven't chosen a partner
// yet, null if we have but they didn't send a party ID.
private opponentPartyId: string;
private inviteTimeout;
private inviteTimeout: NodeJS.Timeout; // in the browser it's 'number'
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
// This flag represents whether we want the other party to be on hold
private remoteOnHold;
private micMuted;
private vidMuted;
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
private makingOffer: boolean;
private ignoreOffer: boolean;
constructor(opts: CallOpts) {
super();
@@ -273,6 +291,11 @@ export class MatrixCall extends EventEmitter {
this.sentEndOfCandidates = false;
this.inviteOrAnswerSent = false;
this.makingOffer = false;
this.remoteOnHold = false;
this.micMuted = false;
this.vidMuted = false;
}
/**
@@ -421,7 +444,7 @@ export class MatrixCall extends EventEmitter {
async setRemoteAudioElement(element: HTMLAudioElement) {
if (element === this.remoteAudioElement) return;
this.remoteVideoElement.muted = true;
if (this.remoteVideoElement) this.remoteVideoElement.muted = true;
this.remoteAudioElement = element;
this.remoteAudioElement.muted = false;
@@ -586,14 +609,12 @@ export class MatrixCall extends EventEmitter {
}
/**
* Set whether the local video preview should be muted or not.
* @param {boolean} muted True to mute the local video.
* Set whether our outbound video should be muted or not.
* @param {boolean} muted True to mute the outbound video.
*/
setLocalVideoMuted(muted: boolean) {
if (!this.localAVStream) {
return;
}
setTracksEnabled(this.localAVStream.getVideoTracks(), !muted);
this.vidMuted = muted;
this.updateMuteStatus();
}
/**
@@ -606,10 +627,7 @@ export class MatrixCall extends EventEmitter {
* (including if the call is not set up yet).
*/
isLocalVideoMuted(): boolean {
if (!this.localAVStream) {
return false;
}
return !isTracksEnabled(this.localAVStream.getVideoTracks());
return this.vidMuted;
}
/**
@@ -617,10 +635,8 @@ export class MatrixCall extends EventEmitter {
* @param {boolean} muted True to mute the mic.
*/
setMicrophoneMuted(muted: boolean) {
if (!this.localAVStream) {
return;
}
setTracksEnabled(this.localAVStream.getAudioTracks(), !muted);
this.micMuted = muted;
this.updateMuteStatus();
}
/**
@@ -633,10 +649,63 @@ export class MatrixCall extends EventEmitter {
* is not set up yet).
*/
isMicrophoneMuted(): boolean {
if (!this.localAVStream) {
return false;
return this.micMuted;
}
/**
* @returns true if we have put the party on the other side of the call on hold
* (that is, we are signalling to them that we are not listening)
*/
isRemoteOnHold(): boolean {
return this.remoteOnHold;
}
setRemoteOnHold(onHold: boolean) {
if (this.isRemoteOnHold() === onHold) return;
this.remoteOnHold = onHold;
for (const tranceiver of this.peerConn.getTransceivers()) {
// We set 'inactive' rather than 'sendonly' because we're not planning on
// playing music etc. to the other side.
tranceiver.direction = onHold ? 'inactive' : 'sendrecv';
}
return !isTracksEnabled(this.localAVStream.getAudioTracks());
this.updateMuteStatus();
}
/**
* Indicates whether we are 'on hold' to the remote party (ie. if true,
* they cannot hear us). Note that this will return true when we put the
* remote on hold too due to the way hold is implemented (since we don't
* wish to play hold music when we put a call on hold, we use 'inactive'
* rather than 'sendonly')
* @returns true if the other party has put us on hold
*/
isLocalOnHold(): boolean {
if (this.state !== CallState.Connected) return false;
let callOnHold = true;
// We consider a call to be on hold only if *all* the tracks are on hold
// (is this the right thing to do?)
for (const tranceiver of this.peerConn.getTransceivers()) {
const trackOnHold = ['inactive', 'recvonly'].includes(tranceiver.currentDirection);
if (!trackOnHold) callOnHold = false;
}
return callOnHold;
}
private updateMuteStatus() {
if (!this.localAVStream) {
return;
}
const micShouldBeMuted = this.micMuted || this.remoteOnHold;
setTracksEnabled(this.localAVStream.getAudioTracks(), !micShouldBeMuted);
const vidShouldBeMuted = this.vidMuted || this.remoteOnHold;
setTracksEnabled(this.localAVStream.getVideoTracks(), !vidShouldBeMuted);
}
/**
@@ -651,6 +720,9 @@ export class MatrixCall extends EventEmitter {
if (this.callHasEnded()) {
return;
}
this.setState(CallState.CreateOffer);
logger.debug("gotUserMediaForInvite -> " + this.type);
const videoEl = this.getLocalVideoElement();
@@ -672,25 +744,21 @@ export class MatrixCall extends EventEmitter {
}
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = this.createPeerConnection();
for (const audioTrack of stream.getAudioTracks()) {
logger.info("Adding audio track with id " + audioTrack.id);
this.peerConn.addTrack(audioTrack, stream);
}
for (const videoTrack of (this.screenSharingStream || stream).getVideoTracks()) {
logger.info("Adding audio track with id " + videoTrack.id);
this.peerConn.addTrack(videoTrack, stream);
}
try {
const myOffer = await this.peerConn.createOffer();
this.gotLocalOffer(myOffer);
} catch (e) {
this.getLocalOfferFailed(e);
return;
}
this.setState(CallState.CreateOffer);
// Now we wait for the negotiationneeded event
};
private sendAnswer() {
@@ -747,6 +815,7 @@ export class MatrixCall extends EventEmitter {
}
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
setTracksEnabled(stream.getAudioTracks(), true);
for (const track of stream.getTracks()) {
this.peerConn.addTrack(track, stream);
@@ -850,7 +919,13 @@ export class MatrixCall extends EventEmitter {
return;
}
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
this.peerConn.addIceCandidate(cand);
try {
this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remore ICE candidate", err);
}
}
}
}
@@ -920,6 +995,53 @@ export class MatrixCall extends EventEmitter {
}
}
async onNegotiateReceived(event: MatrixEvent) {
const description = event.getContent().description;
if (!description || !description.sdp || !description.type) {
logger.info("Ignoring invalid m.call.negotiate event");
return;
}
// Politeness always follows the direction of the call: in a glare situation,
// we pick either the inbound or outbound call, so one side will always be
// inbound and one outbound
const polite = this.direction === CallDirection.Inbound;
// Here we follow the perfect negotiation logic from
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
const offerCollision = (
(description.type == 'offer') &&
(this.makingOffer || this.peerConn.signalingState != 'stable')
);
this.ignoreOffer = !polite && offerCollision;
if (this.ignoreOffer) {
logger.info("Ignoring colliding negotiate event because we're impolite");
return;
}
const prevOnHold = this.isLocalOnHold();
try {
await this.peerConn.setRemoteDescription(description);
if (description.type === 'offer') {
const localDescription = await this.peerConn.createAnswer();
await this.peerConn.setLocalDescription(localDescription);
this.sendVoipEvent(EventType.CallNegotiate, {
description: this.peerConn.localDescription,
});
}
} catch (err) {
logger.warn("Failed to complete negotiation", err);
}
const nowOnHold = this.isLocalOnHold();
if (prevOnHold !== nowOnHold) {
this.emit(CallEvent.HoldUnhold, nowOnHold);
}
}
private callHasEnded() : boolean {
// This exists as workaround to typescript trying to be clever and erroring
// when putting if (this.state === CallState.Ended) return; twice in the same
@@ -944,29 +1066,20 @@ export class MatrixCall extends EventEmitter {
return
}
// Allow a short time for initial candidates to be gathered
await new Promise(resolve => {
setTimeout(resolve, 200);
});
if (this.peerConn.iceGatheringState === 'gathering') {
// Allow a short time for initial candidates to be gathered
await new Promise(resolve => {
setTimeout(resolve, 200);
});
}
if (this.callHasEnded()) return;
const keyName = this.state === CallState.CreateOffer ? 'offer' : 'description';
const eventType = this.state === CallState.CreateOffer ? EventType.CallInvite : EventType.CallNegotiate;
const content = {
// OpenWebRTC appears to add extra stuff (like the DTLS fingerprint)
// to the description when setting it on the peerconnection.
// According to the spec it should only add ICE
// candidates. Any ICE candidates that have already been generated
// at this point will probably be sent both in the offer and separately.
// Also, note that we have to make a new object here, copying the
// type and sdp properties.
// Passing the RTCSessionDescription object as-is doesn't work in
// Chrome (as of about m43).
offer: {
sdp: this.peerConn.localDescription.sdp,
// type now deprecated in Matrix VoIP v1, but
// required to still be sent for backwards compat
type: this.peerConn.localDescription.type,
},
[keyName]: this.peerConn.localDescription,
lifetime: CALL_TIMEOUT_MS,
};
@@ -976,34 +1089,40 @@ export class MatrixCall extends EventEmitter {
this.candidateSendQueue = [];
try {
await this.sendVoipEvent(EventType.CallInvite, content);
await this.sendVoipEvent(eventType, content);
this.sendCandidateQueue();
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
}, CALL_TIMEOUT_MS);
if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
}, CALL_TIMEOUT_MS);
}
} catch (error) {
let code = CallErrorCode.SendInvite;
let message = "Failed to send invite";
this.client.cancelPendingEvent(error.event);
let code = CallErrorCode.SignallingFailed;
let message = "Signalling failed";
if (this.state === CallState.CreateOffer) {
code = CallErrorCode.SendInvite;
message = "Failed to send invite";
}
if (error.name == 'UnknownDeviceError') {
code = CallErrorCode.UnknownDevices;
message = "Unknown devices present in the room";
}
this.client.cancelPendingEvent(error.event);
this.terminate(CallParty.Local, code, false);
this.emit(CallEvent.Error, new CallError(code, message, error));
this.terminate(CallParty.Local, code, false);
}
};
private getLocalOfferFailed = (err: Error) => {
logger.error("Failed to get local offer", err);
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
this.emit(
CallEvent.Error,
new CallError(
@@ -1011,6 +1130,7 @@ export class MatrixCall extends EventEmitter {
"Failed to get local offer!", err,
),
);
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
};
private getUserMediaFailed = (err: Error) => {
@@ -1019,7 +1139,8 @@ export class MatrixCall extends EventEmitter {
return;
}
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
logger.warn("Failed to get user media - ending call", err);
this.emit(
CallEvent.Error,
new CallError(
@@ -1028,6 +1149,7 @@ export class MatrixCall extends EventEmitter {
"does this app have permission?", err,
),
);
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
};
onIceConnectionStateChanged = () => {
@@ -1054,41 +1176,30 @@ export class MatrixCall extends EventEmitter {
};
private onTrack = (ev: RTCTrackEvent) => {
logger.debug(`Track id ${ev.track.id} of kind ${ev.track.kind} added`);
// This is relatively complex as we may get any number of tracks that may
// be in any number of streams, or not in streams at all, etc.
// I'm not entirely sure how this API is supposed to be used: it would
// be nice to know when the browser is finished telling us about a bunch
// of tracks so we could go & figure out which ones to use in which streams,
// but it doesn't. There was an 'addstream' event, but that is now deprecated.
// The base case is that there will be one stream with one audio track, or in
// the case of a video call, and audio and video track.
// This algorithm is not perfect and will fail in edge cases such as a streamless
// track being added first, followed by a normal audio + video stream.
const haveStream = this.remoteStream !== undefined;
if (!haveStream) {
// If we don't currently have a stream, use one this track is already in
if (ev.streams.length > 0) {
this.remoteStream = ev.streams[0];
} else {
// ...unless it's a streamless track, in which case we'll need to make
// our own stream.
this.remoteStream = new MediaStream();
}
if (ev.streams.length === 0) {
logger.warn(`Streamless ${ev.track.kind} found: ignoring.`);
return;
}
// If we already have a stream, check this track is from the same one
if (this.remoteStream && ev.streams[0].id !== this.remoteStream.id) {
logger.warn(
`Ignoring new stream ID ${ev.streams[0].id}: we already have stream ID ${this.remoteStream.id}`,
);
return;
}
// if this track isn't in a stream, add it to the one we have.
// This basically assumes all the tracks are streamless, otherwise it
// will end up adding the track to a stream provided by the RTCPeerConnection,
// which would be weird.
if (ev.streams.length === 0) this.remoteStream.addTrack(ev.track);
if (!this.remoteStream) {
logger.info("Got remote stream with id " + ev.streams[0].id);
}
// If we've just gained our stream, wire it up to the media object
if (!haveStream) {
// Note that we check by ID above and always set the remote stream: Chrome appears
// to make new stream objects when tranciever directionality is changed and the 'active'
// status of streams change
this.remoteStream = ev.streams[0];
logger.debug(`Track id ${ev.track.id} of kind ${ev.track.kind} added`);
if (ev.track.kind === 'video') {
if (this.remoteVideoElement) {
this.queueMediaOperation(MediaQueueId.RemoteVideo, async () => {
this.remoteVideoElement.srcObject = this.remoteStream;
@@ -1099,10 +1210,28 @@ export class MatrixCall extends EventEmitter {
}
});
}
} else {
if (this.remoteAudioElement) this.playRemoteAudio();
}
};
if (this.remoteAudioElement) {
this.playRemoteAudio();
}
onNegotiationNeeded = async () => {
logger.info("Negotation is needed!");
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
logger.info("Opponent does not support renegotiation: ignoring negotiationneeded event");
return;
}
this.makingOffer = true;
try {
const myOffer = await this.peerConn.createOffer();
await this.gotLocalOffer(myOffer);
} catch (e) {
this.getLocalOfferFailed(e);
return;
} finally {
this.makingOffer = false;
}
};
@@ -1356,6 +1485,7 @@ export class MatrixCall extends EventEmitter {
pc.addEventListener('icecandidate', this.gotLocalIceCandidate);
pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
pc.addEventListener('track', this.onTrack);
pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
return pc;
}
@@ -1373,15 +1503,6 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
}
}
function isTracksEnabled(tracks: Array<MediaStreamTrack>) {
for (let i = 0; i < tracks.length; i++) {
if (tracks[i].enabled) {
return true; // at least one track is enabled
}
}
return false;
}
function getUserMediaVideoContraints(callType: CallType) {
const isWebkit = !!navigator.webkitGetUserMedia;
+9
View File
@@ -260,6 +260,15 @@ export class CallEventHandler {
}
call.onSelectAnswerReceived(event);
} else if (event.getType() === EventType.CallNegotiate) {
if (!call) return;
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo
return;
}
call.onNegotiateReceived(event);
}
}
}