Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a27ddfaaaf | |||
| b53f616015 | |||
| 22dc175879 | |||
| 5f23e4699c | |||
| a1bd258a7b | |||
| 39a9c54589 | |||
| 45f3a2f909 | |||
| 5904378170 | |||
| 50ee489079 | |||
| b60e5f40ab | |||
| 65d4015300 | |||
| b692cd109e | |||
| 0f90f055ba | |||
| 28d5ce288c | |||
| 6039066e7f | |||
| c9a2f8b170 | |||
| b34a36d853 | |||
| e25ae546fc | |||
| 5b73bf3e5d | |||
| c16b093bd7 | |||
| e406f32386 | |||
| 489d188966 | |||
| 79fb7bab0b | |||
| c410954bad | |||
| 53a8a7d50f | |||
| 1c7f95c0ee | |||
| 1717fcf499 | |||
| b25453cf87 | |||
| 07b596bf30 | |||
| 1166947c21 | |||
| 6e7b9ca6c0 | |||
| 5b1e3537cc | |||
| 44843d418d | |||
| a103ffa038 | |||
| 712335789e | |||
| cdf8186f44 | |||
| d06942d602 | |||
| 45ac3a60dc | |||
| 4f244da3ec | |||
| 4eefa05d3f | |||
| 40fb31099a | |||
| bcd85f5397 | |||
| 89aeda45c6 | |||
| 7581e5ffdc | |||
| 7046fa3224 | |||
| ef392785e8 | |||
| 5bd029115c | |||
| b6f42b25dd | |||
| 75dd9625a0 | |||
| fd6110679e | |||
| c761019aca | |||
| 853363fdf5 | |||
| f7753f8be3 | |||
| d2dfa29556 | |||
| c0a88b7f4e | |||
| 150e5fede4 | |||
| bb3ec322fb | |||
| f3ee164a7d | |||
| 035cb9fe08 | |||
| 58d0018174 | |||
| 52ed0f8615 | |||
| 2a46513dfd | |||
| 907567182d | |||
| 46cebcd1ca | |||
| 40198f95dc | |||
| 736b934b18 | |||
| b009811ac9 | |||
| ff6612f9d0 | |||
| 565d446b1d | |||
| 044d334398 | |||
| e31ef2dfb5 | |||
| c20566083a | |||
| 9845553a5f | |||
| 03737546fe | |||
| 579cb00c98 | |||
| a307950213 | |||
| 0f5c469be6 | |||
| 6f7e409e9a | |||
| d707912d81 | |||
| 97ab680f4e | |||
| ea35a29bd8 | |||
| 63c182bc3e | |||
| b1a0a12a5f | |||
| cc242230be | |||
| aef9211ea8 | |||
| de5d557882 | |||
| fa9adf199d | |||
| 0807066f1f | |||
| 142e79941d | |||
| 5e9ce38a24 | |||
| f42e6373c4 | |||
| bc46609caa | |||
| cc44abe2d3 | |||
| db6398acdd | |||
| 6661bde608 | |||
| 69f6bba964 | |||
| 2a4e722f0f | |||
| dd20828ded | |||
| 5993dd588c | |||
| 30f86e2437 | |||
| 3ef91e16d8 | |||
| 45e9b3ac68 | |||
| a076e3f0fc | |||
| 99bff04ccc | |||
| bd906e619d | |||
| 34882cc438 | |||
| 5ac00e3465 | |||
| 622dd065ff | |||
| c5c98a6ac1 | |||
| da423ed508 | |||
| 11c4337cfc | |||
| 458164384d | |||
| 13c7f55a79 | |||
| 4ab675863a | |||
| f11103bfcc | |||
| b56936003d | |||
| 645842f0fd | |||
| 66aa9c4831 | |||
| 7de0ca2048 | |||
| 5bbc5cad9f | |||
| 33d1a33a17 | |||
| 6ec7e3a0b7 | |||
| 1a1fe759c3 | |||
| a919c798f8 | |||
| 80fe66c481 | |||
| 4577dc8f44 | |||
| cf0a5305e0 | |||
| e69e6e1981 | |||
| 0febd99fbe |
+121
@@ -1,3 +1,124 @@
|
||||
Changes in [9.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.1) (2021-01-26)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0...v9.5.1)
|
||||
|
||||
* [Release] Fix compatibility with v0 calls
|
||||
[\#1585](https://github.com/matrix-org/matrix-js-sdk/pull/1585)
|
||||
|
||||
Changes in [9.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0) (2021-01-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0-rc.1...v9.5.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0-rc.1) (2021-01-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.1...v9.5.0-rc.1)
|
||||
|
||||
* Don't log if no WebRTC
|
||||
[\#1574](https://github.com/matrix-org/matrix-js-sdk/pull/1574)
|
||||
* Add _unstable_getSharedRooms
|
||||
[\#1417](https://github.com/matrix-org/matrix-js-sdk/pull/1417)
|
||||
* Bump node-notifier from 8.0.0 to 8.0.1
|
||||
[\#1568](https://github.com/matrix-org/matrix-js-sdk/pull/1568)
|
||||
* Ignore party ID if opponent is v0
|
||||
[\#1567](https://github.com/matrix-org/matrix-js-sdk/pull/1567)
|
||||
* Basic call transfer initiation support
|
||||
[\#1566](https://github.com/matrix-org/matrix-js-sdk/pull/1566)
|
||||
* Room version 6 is now a thing
|
||||
[\#1572](https://github.com/matrix-org/matrix-js-sdk/pull/1572)
|
||||
* Store keys with same index but better trust level
|
||||
[\#1571](https://github.com/matrix-org/matrix-js-sdk/pull/1571)
|
||||
* Use TypeScript source for development, swap to build during release
|
||||
[\#1561](https://github.com/matrix-org/matrix-js-sdk/pull/1561)
|
||||
* Revert "Ignore party ID if opponent is v0"
|
||||
[\#1565](https://github.com/matrix-org/matrix-js-sdk/pull/1565)
|
||||
* Basic call transfer initiation support
|
||||
[\#1558](https://github.com/matrix-org/matrix-js-sdk/pull/1558)
|
||||
* Ignore party ID if opponent is v0
|
||||
[\#1559](https://github.com/matrix-org/matrix-js-sdk/pull/1559)
|
||||
* Honour a call reject event from another of our own devices
|
||||
[\#1562](https://github.com/matrix-org/matrix-js-sdk/pull/1562)
|
||||
|
||||
Changes in [9.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.1) (2020-12-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0...v9.4.1)
|
||||
|
||||
* Further script tweaks to get all layers building again
|
||||
|
||||
Changes in [9.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0) (2020-12-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.2...v9.4.0)
|
||||
|
||||
* Revert `postinstall` script change, causes issues for other layers
|
||||
|
||||
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)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0-rc.1) (2020-11-04)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.1...v9.1.0-rc.1)
|
||||
|
||||
+27
-24
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "9.1.0-rc.1",
|
||||
"version": "9.5.1",
|
||||
"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",
|
||||
@@ -27,10 +28,11 @@
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
@@ -46,52 +48,53 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+38
@@ -178,6 +178,19 @@ echo "yarn version"
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# For the published and dist versions of the package, we copy the
|
||||
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
done
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
@@ -353,5 +366,30 @@ if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master --no-edit
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if
|
||||
# we adjusted them previously.
|
||||
for i in main typings
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
@@ -971,4 +971,60 @@ describe("megolm", function() {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice receives an untrusted megolm key, only to receive the trusted one shortly after", function() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(groupSession.session_key());
|
||||
const rawEvent = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
return testClient.client.initCrypto().then(() => {
|
||||
const keys = [{
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: inboundGroupSession.export_session(0),
|
||||
sender_key: testSenderKey,
|
||||
}];
|
||||
return testClient.client.importRoomKeys(keys, { untrusted: true });
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
||||
});
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
event._senderCurve25519Key = testSenderKey;
|
||||
return testClient.client._crypto._onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Call', function() {
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
@@ -156,7 +156,7 @@ describe('Call', function() {
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
@@ -173,7 +173,7 @@ describe('Call', function() {
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
|
||||
@@ -47,6 +47,8 @@ export enum EventType {
|
||||
CallHangup = "m.call.hangup",
|
||||
CallReject = "m.call.reject",
|
||||
CallSelectAnswer = "m.call.select_answer",
|
||||
CallNegotiate = "m.call.negotiate",
|
||||
CallReplaces = "m.call.replaces",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
|
||||
+11
-4
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+75
-7
@@ -180,6 +180,9 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* @param {boolean} [opts.forceTURN]
|
||||
* Optional. Whether relaying calls through a TURN server should be forced.
|
||||
*
|
||||
* @param {boolean} [opts.supportsCallTransfer]
|
||||
* Optional. True to advertise support for call transfers to other parties on Matrix calls.
|
||||
*
|
||||
* @param {boolean} [opts.fallbackICEServerAllowed]
|
||||
* Optional. Whether to allow a fallback ICE server should be used for negotiating a
|
||||
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
|
||||
@@ -364,6 +367,7 @@ export function MatrixClient(opts) {
|
||||
this._cryptoCallbacks = opts.cryptoCallbacks || {};
|
||||
|
||||
this._forceTURN = opts.forceTURN || false;
|
||||
this._supportsCallTransfer = opts.supportsCallTransfer || false;
|
||||
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
|
||||
|
||||
// List of which rooms have encryption enabled: separate from crypto because
|
||||
@@ -577,11 +581,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');
|
||||
@@ -674,6 +700,14 @@ MatrixClient.prototype.setForceTURN = function(forceTURN) {
|
||||
this._forceTURN = forceTURN;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether to advertise transfer support to other parties on Matrix calls.
|
||||
* @param {bool} supportsCallTransfer True to advertise the 'm.call.transferee' capability
|
||||
*/
|
||||
MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer) {
|
||||
this._supportsCallTransfer = supportsCallTransfer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current sync state.
|
||||
* @return {?string} the sync state, which may be null.
|
||||
@@ -3699,25 +3733,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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -5095,6 +5143,26 @@ MatrixClient.prototype._storeClientOptions = function() {
|
||||
return this.store.storeClientOptions(serializableOpts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a set of room IDs in common with another user
|
||||
* @param {string} userId The userId to check.
|
||||
* @return {Promise<string[]>} Resolves to a set of rooms
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype._unstable_getSharedRooms = async function(userId) {
|
||||
if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) {
|
||||
throw Error('Server does not support shared_rooms API');
|
||||
}
|
||||
const path = utils.encodeUri("/uk.half-shot.msc2666/user/shared_rooms/$userId", {
|
||||
$userId: userId,
|
||||
});
|
||||
const res = await this._http.authedRequest(
|
||||
undefined, "GET", path, undefined, undefined,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
return res.joined;
|
||||
};
|
||||
|
||||
/**
|
||||
* High level helper method to stop the client from polling and allow a
|
||||
* clean shutdown.
|
||||
|
||||
@@ -1059,9 +1059,14 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()) {
|
||||
<= session.first_known_index()
|
||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
||||
&& !extraSessionData.untrusted
|
||||
&& existingSessionData.untrusted)) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), so keep it
|
||||
// decrypt more), or they have the same index and
|
||||
// the new sessions trust does not win over the old
|
||||
// sessions trust, so keep it
|
||||
logger.log(
|
||||
`Keeping existing megolm session ${sessionId}`,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1752,6 +1752,7 @@ Crypto.prototype.start = function() {
|
||||
Crypto.prototype.stop = function() {
|
||||
this._outgoingRoomKeyRequestManager.stop();
|
||||
this._deviceList.stop();
|
||||
this._dehydrationManager.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -142,6 +142,7 @@ export interface ICreateClientOpts {
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
verificationMethods?: Array<any>;
|
||||
forceTURN?: boolean;
|
||||
supportsCallTransfer?: boolean,
|
||||
fallbackICEServerAllowed?: boolean;
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
}
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ import {ReEmitter} from '../ReEmitter';
|
||||
// room versions which are considered okay for people to run without being asked
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '5';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5'];
|
||||
const KNOWN_SAFE_ROOM_VERSION = '6';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
|
||||
+447
-224
@@ -26,6 +26,9 @@ 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';
|
||||
import { randomString } from '../randomstring';
|
||||
import { MCallReplacesEvent, MCallAnswer, MCallOfferNegotiate, CallCapabilities } from './callEventTypes';
|
||||
|
||||
// events: hangup, error(err), replaced(call), state(state, oldState)
|
||||
|
||||
@@ -91,12 +94,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 +167,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 +185,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) {
|
||||
@@ -190,6 +196,10 @@ class CallError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function genCallID() {
|
||||
return Date.now() + randomString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Matrix Call.
|
||||
* @constructor
|
||||
@@ -216,7 +226,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 +241,28 @@ 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 opponentCaps: CallCapabilities;
|
||||
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();
|
||||
@@ -245,6 +271,9 @@ export class MatrixCall extends EventEmitter {
|
||||
this.type = null;
|
||||
this.forceTURN = opts.forceTURN;
|
||||
this.ourPartyId = this.client.deviceId;
|
||||
// We compare this to null to checks the presence of a party ID:
|
||||
// make sure it's null, not undefined
|
||||
this.opponentPartyId = null;
|
||||
// Array of Objects with urls, username, credential keys
|
||||
this.turnServers = opts.turnServers || [];
|
||||
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
|
||||
@@ -256,7 +285,7 @@ export class MatrixCall extends EventEmitter {
|
||||
utils.checkObjectHasKeys(server, ["urls"]);
|
||||
}
|
||||
|
||||
this.callId = "c" + new Date().getTime() + Math.random();
|
||||
this.callId = genCallID();
|
||||
this.state = CallState.Fledgling;
|
||||
|
||||
// A queue for candidates waiting to go out.
|
||||
@@ -265,14 +294,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 +364,12 @@ 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;
|
||||
}
|
||||
|
||||
opponentCanBeTransferred() {
|
||||
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,17 +403,19 @@ export class MatrixCall extends EventEmitter {
|
||||
* video will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></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 +436,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 +449,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();
|
||||
}
|
||||
@@ -434,6 +460,7 @@ export class MatrixCall extends EventEmitter {
|
||||
*/
|
||||
async initWithInvite(event: MatrixEvent) {
|
||||
this.msg = event.getContent();
|
||||
this.direction = CallDirection.Inbound;
|
||||
this.peerConn = this.createPeerConnection();
|
||||
try {
|
||||
await this.peerConn.setRemoteDescription(this.msg.offer);
|
||||
@@ -455,9 +482,13 @@ export class MatrixCall extends EventEmitter {
|
||||
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
|
||||
|
||||
this.setState(CallState.Ringing);
|
||||
this.direction = CallDirection.Inbound;
|
||||
this.opponentVersion = this.msg.version;
|
||||
this.opponentPartyId = this.msg.party_id || null;
|
||||
if (this.opponentVersion !== 0) {
|
||||
// ignore party ID in v0 calls: party ID isn't a thing until v1
|
||||
this.opponentPartyId = this.msg.party_id || null;
|
||||
}
|
||||
this.opponentCaps = this.msg.capabilities || {};
|
||||
this.opponentMember = event.sender;
|
||||
|
||||
if (event.getLocalAge()) {
|
||||
setTimeout(() => {
|
||||
@@ -586,14 +617,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 +635,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 +643,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 +657,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 +746,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() {
|
||||
@@ -701,7 +797,14 @@ export class MatrixCall extends EventEmitter {
|
||||
// required to still be sent for backwards compat
|
||||
type: this.peerConn.localDescription.type,
|
||||
},
|
||||
};
|
||||
} as MCallAnswer;
|
||||
|
||||
if (this.client._supportsCallTransfer) {
|
||||
answerContent.capabilities = {
|
||||
'm.call.transferee': true,
|
||||
}
|
||||
}
|
||||
|
||||
// We have just taken the local description from the peerconnection which will
|
||||
// contain all the local candidates added so far, so we can discard any candidates
|
||||
// we had queued up because they'll be in the answer.
|
||||
@@ -737,16 +840,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 +956,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +975,7 @@ export class MatrixCall extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.opponentPartyId !== undefined) {
|
||||
if (this.opponentPartyId !== null) {
|
||||
logger.info(
|
||||
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
|
||||
`we already have an answer/reject from ${this.opponentPartyId}`,
|
||||
@@ -872,7 +984,11 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
this.opponentVersion = event.getContent().version;
|
||||
this.opponentPartyId = event.getContent().party_id || null;
|
||||
if (this.opponentVersion !== 0) {
|
||||
this.opponentPartyId = event.getContent().party_id || null;
|
||||
}
|
||||
this.opponentCaps = event.getContent().capabilities || {};
|
||||
this.opponentMember = event.sender;
|
||||
|
||||
this.setState(CallState.Connecting);
|
||||
|
||||
@@ -920,6 +1036,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,31 +1115,33 @@ 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 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,
|
||||
},
|
||||
lifetime: CALL_TIMEOUT_MS,
|
||||
};
|
||||
} as MCallOfferNegotiate;
|
||||
|
||||
// clunky because TypeScript can't folow the types through if we use an expression as the key
|
||||
if (this.state === CallState.CreateOffer) {
|
||||
content.offer = this.peerConn.localDescription;
|
||||
} else {
|
||||
content.description = this.peerConn.localDescription;
|
||||
}
|
||||
|
||||
if (this.client._supportsCallTransfer) {
|
||||
content.capabilities = {
|
||||
'm.call.transferee': true,
|
||||
}
|
||||
}
|
||||
|
||||
// Get rid of any candidates waiting to be sent: they'll be included in the local
|
||||
// description we just got and will send in the offer.
|
||||
@@ -976,34 +1149,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 +1190,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 +1199,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 +1209,7 @@ export class MatrixCall extends EventEmitter {
|
||||
"does this app have permission?", err,
|
||||
),
|
||||
);
|
||||
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
|
||||
};
|
||||
|
||||
onIceConnectionStateChanged = () => {
|
||||
@@ -1054,82 +1236,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 +1339,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.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) => {
|
||||
@@ -1152,7 +1353,16 @@ export class MatrixCall extends EventEmitter {
|
||||
// No need to check party_id for reject because if we'd received either
|
||||
// an answer or reject, we wouldn't be in state InviteSent
|
||||
|
||||
if (this.state === CallState.InviteSent) {
|
||||
const shouldTerminate = (
|
||||
// reject events also end the call if it's ringing: it's another of
|
||||
// our devices rejecting the call.
|
||||
([CallState.InviteSent, CallState.Ringing].includes(this.state)) ||
|
||||
// also if we're in the init state and it's an inbound call, since
|
||||
// this means we just haven't entered the ringing state yet
|
||||
this.state === CallState.Fledgling && this.direction === CallDirection.Inbound
|
||||
);
|
||||
|
||||
if (shouldTerminate) {
|
||||
this.terminate(CallParty.Remote, CallErrorCode.UserHangup, true);
|
||||
} else {
|
||||
logger.debug(`Call is in state: ${this.state}: ignoring reject`);
|
||||
@@ -1208,7 +1418,29 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
|
||||
async transfer(targetUserId: string, targetRoomId?: string) {
|
||||
// Fetch the target user's global profile info: their room avatar / displayname
|
||||
// could be different in whatever room we shae with them.
|
||||
const profileInfo = await this.client.getProfileInfo(targetUserId);
|
||||
|
||||
const replacementId = genCallID();
|
||||
|
||||
const body = {
|
||||
replacement_id: genCallID(),
|
||||
target_user: {
|
||||
id: targetUserId,
|
||||
display_name: profileInfo.display_name,
|
||||
avatar_url: profileInfo.avatar_url,
|
||||
},
|
||||
create_call: replacementId,
|
||||
} as MCallReplacesEvent;
|
||||
|
||||
if (targetRoomId) body.target_room = targetRoomId;
|
||||
|
||||
return this.sendVoipEvent(EventType.CallReplaces, body);
|
||||
}
|
||||
|
||||
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
|
||||
if (this.callHasEnded()) return;
|
||||
|
||||
if (this.inviteTimeout) {
|
||||
@@ -1221,29 +1453,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 +1582,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 +1600,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 +1666,8 @@ 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");
|
||||
// NB. We don't log here as apps try to create a call object as a test for
|
||||
// whether calls are supported, so we shouldn't fill the logs up.
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1479,5 +1698,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// allow camelcase as these are events type that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
interface CallOfferAnswer {
|
||||
type: string;
|
||||
sdp: string;
|
||||
}
|
||||
|
||||
export interface CallCapabilities {
|
||||
'm.call.transferee': boolean;
|
||||
}
|
||||
|
||||
export interface MCallAnswer {
|
||||
answer: CallOfferAnswer;
|
||||
capabilities: CallCapabilities;
|
||||
}
|
||||
|
||||
export interface MCallOfferNegotiate {
|
||||
offer: CallOfferAnswer;
|
||||
description: CallOfferAnswer;
|
||||
lifetime: number;
|
||||
capabilities: CallCapabilities;
|
||||
}
|
||||
|
||||
export interface MCallReplacesTarget {
|
||||
id: string;
|
||||
display_name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export interface MCallReplacesEvent {
|
||||
replacement_id: string;
|
||||
target_user: MCallReplacesTarget;
|
||||
create_call: string;
|
||||
target_room: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
Reference in New Issue
Block a user