Compare commits

..

67 Commits

Author SHA1 Message Date
RiotRobot 75dd9625a0 v9.4.0-rc.2 2020-12-16 15:53:01 +00:00
RiotRobot fd6110679e Prepare changelog for v9.4.0-rc.2 2020-12-16 15:53:00 +00:00
J. Ryan Stinnett c761019aca Merge pull request #1560 from matrix-org/jryans/rm-postinstall
Remove `postinstall` script which also runs as a dependency
2020-12-16 15:50:03 +00:00
J. Ryan Stinnett 853363fdf5 Remove postinstall script which also runs as a dependency
It seems I misunderstood the `postinstall` script and had thought it would only
run when installing the project root. Instead, it seems to run also as a
dependency as well.

This change should be fine for release, but it does mean when developing
the JS SDK, you'll need to manually build. CI pipelines will also need to be
changed to call an extra build step.
2020-12-16 15:43:06 +00:00
RiotRobot f7753f8be3 v9.4.0-rc.1 2020-12-16 14:17:25 +00:00
RiotRobot d2dfa29556 Prepare changelog for v9.4.0-rc.1 2020-12-16 14:17:25 +00:00
David Baker 035cb9fe08 Merge pull request #1553 from matrix-org/dbkr/line_1_2
Fixes to support line 1 / 2
2020-12-11 10:26:23 +00:00
David Baker 58d0018174 Merge remote-tracking branch 'origin/develop' into dbkr/line_1_2 2020-12-10 14:00:47 +00:00
David Baker 52ed0f8615 Merge pull request #1549 from matrix-org/dbkr/hold_ui
Add API for listening to remote hold status, advertise VoIP V1
2020-12-10 12:30:46 +00:00
David Baker 2a46513dfd remove outdated comment
(it did work - I just was not checking the flag in the right place)
2020-12-09 11:21:23 +00:00
David Baker 907567182d Mute speaker when putting a call on hold 2020-12-08 19:45:11 +00:00
David Baker 46cebcd1ca Merge pull request #1555 from matrix-org/dbkr/fix_hangup_from_other_client
A hangup from another client is still valid
2020-12-08 19:02:00 +00:00
David Baker 40198f95dc A hangup from another client is still valid
if we're in the ringing state

Fixes https://github.com/vector-im/element-web/issues/15933
2020-12-08 18:53:11 +00:00
David Baker 736b934b18 Merge remote-tracking branch 'origin/develop' into dbkr/hold_ui 2020-12-08 11:59:32 +00:00
J. Ryan Stinnett b009811ac9 Merge pull request #1554 from matrix-org/jryans/rm-temp-test-build
Remove temporary build step for tests
2020-12-08 10:48:56 +00:00
RiotRobot ff6612f9d0 Merge branch 'master' into develop 2020-12-07 12:10:31 +00:00
RiotRobot 565d446b1d v9.3.0 2020-12-07 12:07:32 +00:00
RiotRobot 044d334398 Prepare changelog for v9.3.0 2020-12-07 12:07:31 +00:00
J. Ryan Stinnett e31ef2dfb5 Remove temporary build step for tests
Now that the pipeline has been updated to build for tests, we can remove this
temporary build in the test script.

Related to https://github.com/matrix-org/pipelines/pull/113
2020-12-07 10:21:26 +00:00
J. Ryan Stinnett c20566083a Merge pull request #1552 from matrix-org/jryans/browser-prepublishonly
Move browser build steps to prepublish only
2020-12-07 10:19:07 +00:00
David Baker 9845553a5f playRemoteVideo should be with it's friend, playRemoteAudio 2020-12-04 20:03:58 +00:00
David Baker 03737546fe Remove the media operation queues
As per the comment on playRemoteVideo()
2020-12-04 20:03:11 +00:00
J. Ryan Stinnett 579cb00c98 Temporarily build browser mode for tests 2020-12-04 14:19:46 +00:00
David Baker a307950213 Merge remote-tracking branch 'origin/develop' into dbkr/hold_ui 2020-12-03 19:26:36 +00:00
David Baker 0f5c469be6 Merge remote-tracking branch 'origin/develop' into dbkr/line_1_2 2020-12-03 17:55:59 +00:00
David Baker 6f7e409e9a Some little fixes to support line 1 / 2
* Resume playing audio at the appropriate time
 * Re-emit call events (they were the exception before - all other events
   were re-emitted through the MatrixClient)
 * Fix an audio/video typo
2020-12-03 17:41:34 +00:00
J. Ryan Stinnett d707912d81 Move browser build steps to prepublish only
This speeds local development and CI runs by only running the browser build
steps at release time.
2020-12-03 14:58:28 +00:00
Michael Telatynski 97ab680f4e Merge pull request #1541 from matrix-org/t3chguy/socials
Extend getSsoLoginUrl for MSC2858
2020-12-02 15:48:40 +00:00
Michael Telatynski ea35a29bd8 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/socials 2020-12-02 15:18:37 +00:00
RiotRobot 63c182bc3e v9.3.0-rc.1 2020-12-02 14:20:20 +00:00
RiotRobot b1a0a12a5f Prepare changelog for v9.3.0-rc.1 2020-12-02 14:20:19 +00:00
David Baker cc242230be Update comment 2020-11-27 14:42:18 +00:00
David Baker aef9211ea8 Merge pull request #1551 from matrix-org/dbkr/user_media_error
Export CallError
2020-11-27 14:40:59 +00:00
David Baker de5d557882 Export CallError
So the types can be typed
2020-11-27 12:57:40 +00:00
J. Ryan Stinnett fa9adf199d Merge pull request #1550 from matrix-org/jryans/upgrade-deps-2020-11-25
Upgrade dependencies
2020-11-26 15:50:28 +00:00
J. Ryan Stinnett 0807066f1f Nested upgrades via yarn upgrade 2020-11-26 15:06:30 +00:00
J. Ryan Stinnett 142e79941d Upgrade to latest major version of direct deps 2020-11-26 15:04:52 +00:00
David Baker 5e9ce38a24 Add API for listening to remote hold status
And avoid isLocalOnHold() returning true whilst the remote side
is un-held.
2020-11-26 14:32:47 +00:00
J. Ryan Stinnett f42e6373c4 Merge pull request #1547 from dalcde/error
Don't log error when environment does not support WebRTC
2020-11-26 12:13:49 +00:00
Dexter Chua bc46609caa Don't log error when WebRTC not supported
This function is *always* called when a MatrixClient is created, e.g. in
an appservice. If the environment does not support WebRTC, this is not
necessarily an error; it is expected in many situations.

Fix a small typo

Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-11-26 13:55:36 +08:00
J. Ryan Stinnett cc44abe2d3 Nested upgrades via yarn upgrade 2020-11-25 17:22:20 +00:00
J. Ryan Stinnett db6398acdd Upgrade to latest minor version of direct deps 2020-11-25 17:16:22 +00:00
RiotRobot 6661bde608 Merge branch 'master' into develop 2020-11-23 16:23:01 +00:00
RiotRobot 69f6bba964 v9.2.0 2020-11-23 16:19:22 +00:00
RiotRobot 2a4e722f0f Prepare changelog for v9.2.0 2020-11-23 16:19:22 +00:00
J. Ryan Stinnett dd20828ded Merge pull request #1545 from matrix-org/jryans/fix-dehydration-method-release
[Release] Fix dehydration method name
2020-11-19 16:30:35 +00:00
J. Ryan Stinnett 5993dd588c Merge pull request #1544 from matrix-org/jryans/fix-dehydration-method
Fix dehydration method name
2020-11-19 16:24:25 +00:00
J. Ryan Stinnett 30f86e2437 Fix dehydration method name
https://github.com/matrix-org/matrix-js-sdk/pull/1537 changed some dehydration
method names, but one call site was missed.
2020-11-19 16:22:38 +00:00
J. Ryan Stinnett 3ef91e16d8 Fix dehydration method name
https://github.com/matrix-org/matrix-js-sdk/pull/1537 changed some dehydration
method names, but one call site was missed.
2020-11-19 16:16:40 +00:00
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
Michael Telatynski 99bff04ccc Update base-apis.js 2020-11-17 15:25:17 +00:00
Michael Telatynski bd906e619d Extend getSsoLoginUrl for MSC2858 2020-11-17 09:55:14 +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
10 changed files with 2722 additions and 2448 deletions
+61
View File
@@ -1,3 +1,64 @@
Changes in [9.4.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.2) (2020-12-16)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.1...v9.4.0-rc.2)
* Remove `postinstall` script which also runs as a dependency
[\#1560](https://github.com/matrix-org/matrix-js-sdk/pull/1560)
Changes in [9.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.1) (2020-12-16)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.3.0...v9.4.0-rc.1)
* Fixes to support line 1 / 2
[\#1553](https://github.com/matrix-org/matrix-js-sdk/pull/1553)
* Add API for listening to remote hold status, advertise VoIP V1
[\#1549](https://github.com/matrix-org/matrix-js-sdk/pull/1549)
* A hangup from another client is still valid
[\#1555](https://github.com/matrix-org/matrix-js-sdk/pull/1555)
* Remove temporary build step for tests
[\#1554](https://github.com/matrix-org/matrix-js-sdk/pull/1554)
* Move browser build steps to prepublish only
[\#1552](https://github.com/matrix-org/matrix-js-sdk/pull/1552)
* Extend getSsoLoginUrl for MSC2858
[\#1541](https://github.com/matrix-org/matrix-js-sdk/pull/1541)
Changes in [9.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.3.0) (2020-12-07)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.3.0-rc.1...v9.3.0)
* No changes since rc.1
Changes in [9.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.3.0-rc.1) (2020-12-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.2.0...v9.3.0-rc.1)
* Export CallError
[\#1551](https://github.com/matrix-org/matrix-js-sdk/pull/1551)
* Upgrade dependencies
[\#1550](https://github.com/matrix-org/matrix-js-sdk/pull/1550)
* Don't log error when environment does not support WebRTC
[\#1547](https://github.com/matrix-org/matrix-js-sdk/pull/1547)
* Fix dehydration method name
[\#1544](https://github.com/matrix-org/matrix-js-sdk/pull/1544)
Changes in [9.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.2.0) (2020-11-23)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.2.0-rc.1...v9.2.0)
* [Release] Fix dehydration method name
[\#1545](https://github.com/matrix-org/matrix-js-sdk/pull/1545)
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)
+23 -22
View File
@@ -1,13 +1,14 @@
{
"name": "matrix-js-sdk",
"version": "9.1.0",
"version": "9.4.0-rc.2",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
@@ -46,50 +47,50 @@
"release.sh"
],
"dependencies": {
"@babel/runtime": "^7.11.2",
"@babel/runtime": "^7.12.5",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"loglevel": "^1.7.0",
"loglevel": "^1.7.1",
"qs": "^6.9.4",
"request": "^2.88.2",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@babel/cli": "^7.11.6",
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/cli": "^7.12.8",
"@babel/core": "^7.12.9",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.11.5",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@babel/register": "^7.11.5",
"@types/jest": "^26.0.14",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.7",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.1",
"@types/jest": "^26.0.15",
"@types/node": "12",
"@types/request": "^2.48.5",
"babel-eslint": "^10.1.0",
"babel-jest": "^24.9.0",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
"better-docs": "^2.3.2",
"browserify": "^16.5.2",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.9.0",
"eslint": "7.14.0",
"eslint-config-matrix-org": "^0.1.2",
"eslint-plugin-babel": "^5.3.1",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^24.9.0",
"jest": "^26.6.3",
"jest-localstorage-mock": "^2.4.3",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"rimraf": "^3.0.2",
"terser": "^4.8.0",
"tsify": "^4.0.2",
"typescript": "^3.9.7"
"terser": "^5.5.0",
"tsify": "^5.0.2",
"typescript": "^4.1.2"
},
"jest": {
"testEnvironment": "node"
+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",
+11 -4
View File
@@ -356,15 +356,22 @@ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
* authenticates with the SSO.
* @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'.
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
* @return {string} The HS URL to hit to begin the SSO login process.
*/
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType, idpId) {
if (loginType === undefined) {
loginType = "sso";
}
return this._http.getUrl("/login/"+loginType+"/redirect", {
"redirectUrl": redirectUrl,
}, PREFIX_R0);
let prefix = PREFIX_R0;
let url = "/login/" + loginType + "/redirect";
if (idpId) {
url += "/" + idpId;
prefix = "/_matrix/client/unstable/org.matrix.msc2858";
}
return this._http.getUrl(url, { redirectUrl }, prefix);
};
/**
+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.setKeyAndQueueDehydration(
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();
};
/**
+368 -216
View File
@@ -26,6 +26,7 @@ import {EventEmitter} from 'events';
import * as utils from '../utils';
import MatrixEvent from '../models/event';
import {EventType} from '../@types/event';
import { RoomMember } from '../models/room-member';
// events: hangup, error(err), replaced(call), state(state, oldState)
@@ -91,12 +92,13 @@ export enum CallEvent {
State = 'state',
Error = 'error',
Replaced = 'replaced',
}
enum MediaQueueId {
RemoteVideo = 'remote_video',
RemoteAudio = 'remote_audio',
LocalVideo = 'local_video',
// The value of isLocalOnHold() has changed
LocalHoldUnhold = 'local_hold_unhold',
// The value of isRemoteOnHold() has changed
RemoteHoldUnhold = 'remote_hold_unhold',
// backwards compat alias for LocalHoldUnhold: remove in a major version bump
HoldUnhold = 'hold_unhold',
}
export enum CallErrorCode {
@@ -163,15 +165,17 @@ 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',
}
/**
* The version field that we set in m.call.* events
* Once we are able to speak v1 VoIP sufficiently, this
* bumped to 1. While we partially speak v1 VoIP, it remains
* as 0.
*/
const VOIP_PROTO_VERSION = 0;
const VOIP_PROTO_VERSION = 1;
/** The fallback ICE server to use for STUN or TURN protocols. */
const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
@@ -179,7 +183,7 @@ const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
/** The length of time a call can be ringing for. */
const CALL_TIMEOUT_MS = 60000;
class CallError extends Error {
export class CallError extends Error {
code : string;
constructor(code : CallErrorCode, msg: string, err: Error) {
@@ -216,7 +220,6 @@ export class MatrixCall extends EventEmitter {
private turnServers: Array<TurnServer>;
private candidateSendQueue: Array<RTCIceCandidate>;
private candidateSendTries: number;
private mediaPromises: { [queueId: string]: Promise<void>; };
private sentEndOfCandidates: boolean;
private peerConn: RTCPeerConnection;
private localVideoElement: HTMLVideoElement;
@@ -232,11 +235,27 @@ export class MatrixCall extends EventEmitter {
// XXX: I don't know why this is called 'config'.
private config: MediaStreamConstraints;
private successor: MatrixCall;
private opponentMember: RoomMember;
private opponentVersion: number;
// 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;
// and this one we set when we're transitioning out of the hold state because we
// can't tell the difference between that and the other party holding us
private unholdingRemote;
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();
@@ -265,14 +284,14 @@ export class MatrixCall extends EventEmitter {
this.candidateSendQueue = [];
this.candidateSendTries = 0;
// Lookup from opaque queue ID to a promise for media element operations that
// need to be serialised into a given queue. Store this per-MatrixCall on the
// assumption that multiple matrix calls will never compete for control of the
// same DOM elements.
this.mediaPromises = Object.create(null);
this.sentEndOfCandidates = false;
this.inviteOrAnswerSent = false;
this.makingOffer = false;
this.remoteOnHold = false;
this.unholdingRemote = false;
this.micMuted = false;
this.vidMuted = false;
}
/**
@@ -335,12 +354,8 @@ export class MatrixCall extends EventEmitter {
this.type = CallType.Video;
}
private queueMediaOperation(queueId: MediaQueueId, operation: () => any) {
if (this.mediaPromises[queueId] !== undefined) {
this.mediaPromises[queueId] = this.mediaPromises[queueId].then(operation, operation);
} else {
this.mediaPromises[queueId] = Promise.resolve(operation());
}
getOpponentMember() {
return this.opponentMember;
}
/**
@@ -374,17 +389,19 @@ export class MatrixCall extends EventEmitter {
* video will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
setLocalVideoElement(element : HTMLVideoElement) {
async setLocalVideoElement(element : HTMLVideoElement) {
this.localVideoElement = element;
if (element && this.localAVStream && this.type === CallType.Video) {
element.autoplay = true;
this.queueMediaOperation(MediaQueueId.LocalVideo, () => {
element.srcObject = this.localAVStream;
element.muted = true;
return element.play();
});
element.srcObject = this.localAVStream;
element.muted = true;
try {
await element.play();
} catch (e) {
logger.info("Failed to play local video element", e);
}
}
}
@@ -405,10 +422,7 @@ export class MatrixCall extends EventEmitter {
this.remoteVideoElement = element;
if (this.remoteStream) {
this.queueMediaOperation(MediaQueueId.RemoteVideo, () => {
element.srcObject = this.remoteStream;
return element.play();
});
this.playRemoteVideo();
}
}
@@ -421,9 +435,7 @@ export class MatrixCall extends EventEmitter {
async setRemoteAudioElement(element: HTMLAudioElement) {
if (element === this.remoteAudioElement) return;
this.remoteVideoElement.muted = true;
this.remoteAudioElement = element;
this.remoteAudioElement.muted = false;
if (this.remoteStream) this.playRemoteAudio();
}
@@ -458,6 +470,7 @@ export class MatrixCall extends EventEmitter {
this.direction = CallDirection.Inbound;
this.opponentVersion = this.msg.version;
this.opponentPartyId = this.msg.party_id || null;
this.opponentMember = event.sender;
if (event.getLocalAge()) {
setTimeout(() => {
@@ -586,14 +599,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 +617,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 +625,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 +639,81 @@ 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;
if (!onHold) this.unholdingRemote = true;
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';
}
this.updateMuteStatus();
if (!onHold) {
this.playRemoteAudio();
}
this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold);
}
/**
* 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;
if (this.unholdingRemote) 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);
if (this.remoteOnHold) {
if (this.remoteAudioElement && this.remoteAudioElement.srcObject === this.remoteStream) {
this.remoteAudioElement.muted = true;
} else if (this.remoteVideoElement && this.remoteVideoElement.srcObject === this.remoteStream) {
this.remoteVideoElement.muted = true;
}
} else {
this.playRemoteAudio();
}
return !isTracksEnabled(this.localAVStream.getAudioTracks());
}
/**
@@ -651,46 +728,47 @@ export class MatrixCall extends EventEmitter {
if (this.callHasEnded()) {
return;
}
this.setState(CallState.CreateOffer);
logger.debug("gotUserMediaForInvite -> " + this.type);
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type === CallType.Video) {
this.queueMediaOperation(MediaQueueId.LocalVideo, () => {
videoEl.autoplay = true;
if (this.screenSharingStream) {
logger.debug(
"Setting screen sharing stream to the local video element",
);
videoEl.srcObject = this.screenSharingStream;
} else {
videoEl.srcObject = stream;
}
videoEl.muted = true;
return videoEl.play();
});
videoEl.autoplay = true;
if (this.screenSharingStream) {
logger.debug(
"Setting screen sharing stream to the local video element",
);
videoEl.srcObject = this.screenSharingStream;
} else {
videoEl.srcObject = stream;
}
videoEl.muted = true;
try {
await videoEl.play();
} catch (e) {
logger.info("Failed to play local video element", e);
}
}
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() {
@@ -737,16 +815,19 @@ export class MatrixCall extends EventEmitter {
const localVidEl = this.getLocalVideoElement();
if (localVidEl && this.type === CallType.Video) {
this.queueMediaOperation(MediaQueueId.LocalVideo, () => {
localVidEl.autoplay = true;
localVidEl.srcObject = stream;
localVidEl.autoplay = true;
localVidEl.srcObject = stream;
localVidEl.muted = true;
return localVidEl.play();
});
localVidEl.muted = true;
try {
await localVidEl.play();
} catch (e) {
logger.info("Failed to play local video element", e);
}
}
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 +931,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);
}
}
}
}
@@ -873,6 +960,7 @@ export class MatrixCall extends EventEmitter {
this.opponentVersion = event.getContent().version;
this.opponentPartyId = event.getContent().party_id || null;
this.opponentMember = event.sender;
this.setState(CallState.Connecting);
@@ -920,6 +1008,61 @@ 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 prevLocalOnHold = this.isLocalOnHold();
if (description.type === 'answer') {
// whenever we get an answer back, clear the flag we set whilst trying to un-hold
// the other party: the state of the channels now reflects reality
this.unholdingRemote = false;
}
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 newLocalOnHold = this.isLocalOnHold();
if (prevLocalOnHold !== newLocalOnHold) {
this.emit(CallEvent.LocalHoldUnhold, newLocalOnHold);
// also this one for backwards compat
this.emit(CallEvent.HoldUnhold, newLocalOnHold);
}
}
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 +1087,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 +1110,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 +1151,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 +1160,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 +1170,7 @@ export class MatrixCall extends EventEmitter {
"does this app have permission?", err,
),
);
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
};
onIceConnectionStateChanged = () => {
@@ -1054,82 +1197,102 @@ 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;
try {
await this.remoteVideoElement.play();
} catch (e) {
logger.error("Failed to play remote video element", e);
}
});
}
if (this.remoteAudioElement) {
this.playRemoteAudio();
this.playRemoteVideo();
}
} else {
if (this.remoteAudioElement) this.playRemoteAudio();
}
};
playRemoteAudio() {
this.queueMediaOperation(MediaQueueId.RemoteAudio, async () => {
this.remoteAudioElement.srcObject = this.remoteStream;
onNegotiationNeeded = async () => {
logger.info("Negotation is needed!");
// if audioOutput is non-default:
try {
if (audioOutput) {
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
// it fails.
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
// back to the default after the call is over
logger.info("Setting audio sink to " + audioOutput + ", was " + this.remoteAudioElement.sinkId);
await this.remoteAudioElement.setSinkId(audioOutput);
}
} catch (e) {
logger.warn("Couldn't set requested audio output device: using default", e);
}
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
logger.info("Opponent does not support renegotiation: ignoring negotiationneeded event");
return;
}
try {
await this.remoteAudioElement.play();
} catch (e) {
logger.error("Failed to play remote video element", e);
this.makingOffer = true;
try {
const myOffer = await this.peerConn.createOffer();
await this.gotLocalOffer(myOffer);
} catch (e) {
this.getLocalOfferFailed(e);
return;
} finally {
this.makingOffer = false;
}
};
async playRemoteAudio() {
if (this.remoteVideoElement) this.remoteVideoElement.muted = true;
this.remoteAudioElement.muted = false;
this.remoteAudioElement.srcObject = this.remoteStream;
// if audioOutput is non-default:
try {
if (audioOutput) {
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
// it fails.
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
// back to the default after the call is over
logger.info("Setting audio sink to " + audioOutput + ", was " + this.remoteAudioElement.sinkId);
await this.remoteAudioElement.setSinkId(audioOutput);
}
});
} catch (e) {
logger.warn("Couldn't set requested audio output device: using default", e);
}
try {
await this.remoteAudioElement.play();
} catch (e) {
logger.error("Failed to play remote audio element", e);
}
}
private async playRemoteVideo() {
// A note on calling methods on media elements:
// We used to have queues per media element to serialise all calls on those elements.
// The reason given for this was that load() and play() were racing. However, we now
// never call load() explicitly so this seems unnecessary. However, serialising every
// operation was causing bugs where video would not resume because some play command
// had got stuck and all media operations were queued up behind it. If necessary, we
// should serialise the ones that need to be serialised but then be able to interrupt
// them with another load() which will cancel the pending one, but since we don't call
// load() explicitly, it shouldn't be a problem.
this.remoteVideoElement.srcObject = this.remoteStream;
logger.info("playing remote video. stream active? " + this.remoteStream.active);
try {
await this.remoteVideoElement.play();
} catch (e) {
logger.info("Failed to play remote video element", e);
}
}
onHangupReceived = (msg) => {
@@ -1137,13 +1300,12 @@ export class MatrixCall extends EventEmitter {
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
if (!this.partyIdMatches(msg) && this.opponentPartyId !== undefined) {
if (this.partyIdMatches(msg) || this.opponentPartyId === undefined || this.state === CallState.Ringing) {
// default reason is user_hangup
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
} else {
logger.info(`Ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`);
return;
}
// default reason is user_hangup
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
};
onRejectReceived = (msg) => {
@@ -1208,7 +1370,7 @@ export class MatrixCall extends EventEmitter {
}
}
private terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
if (this.callHasEnded()) return;
if (this.inviteTimeout) {
@@ -1221,29 +1383,23 @@ export class MatrixCall extends EventEmitter {
const localVid = this.getLocalVideoElement();
if (remoteVid) {
this.queueMediaOperation(MediaQueueId.RemoteVideo, () => {
remoteVid.pause();
remoteVid.srcObject = null;
});
remoteVid.pause();
remoteVid.srcObject = null;
}
if (remoteAud) {
this.queueMediaOperation(MediaQueueId.RemoteAudio, async () => {
remoteAud.pause();
remoteAud.srcObject = null;
try {
// As per comment in playRemoteAudio, setting the sink ID back to the default
// once the call is over makes setSinkId work reliably.
await this.remoteAudioElement.setSinkId('')
} catch (e) {
logger.warn("Failed to set sink ID back to default");
}
});
remoteAud.pause();
remoteAud.srcObject = null;
try {
// As per comment in playRemoteAudio, setting the sink ID back to the default
// once the call is over makes setSinkId work reliably.
await this.remoteAudioElement.setSinkId('')
} catch (e) {
logger.warn("Failed to set sink ID back to default");
}
}
if (localVid) {
this.queueMediaOperation(MediaQueueId.LocalVideo, () => {
localVid.pause();
localVid.srcObject = null;
});
localVid.pause();
localVid.srcObject = null;
}
this.hangupParty = hangupParty;
this.hangupReason = hangupReason;
@@ -1356,6 +1512,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 +1530,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;
@@ -1448,7 +1596,7 @@ export function setVideoInput(deviceId: string) { videoInput = deviceId; }
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts) {
// typeof prevents Node from erroring on an undefined reference
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') {
logger.error("No window or document object: WebRTC is not supported in this environement");
logger.info("No window or document object: WebRTC is not supported in this environment");
return null;
}
@@ -1479,5 +1627,9 @@ export function createNewMatrixCall(client: any, roomId: string, options?: CallO
// call level options
forceTURN: client._forceTURN || optionsForceTURN,
};
return new MatrixCall(opts);
const call = new MatrixCall(opts);
client.reEmitter.reEmit(call, Object.values(CallEvent));
return call;
}
+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);
}
}
}
+2177 -2194
View File
File diff suppressed because it is too large Load Diff