From 3ee0ac058086aae6c811e9078763eeca13ac8f4e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 20 May 2021 10:43:08 +0200 Subject: Initial Talkbuchet version Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 576 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 docs/Talkbuchet.js (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js new file mode 100644 index 000000000..18323cbd3 --- /dev/null +++ b/docs/Talkbuchet.js @@ -0,0 +1,576 @@ +/** + * + * @copyright Copyright (c) 2021, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * HOW TO SETUP: + * ----------------------------------------------------------------------------- + * - Set the right values in "talkOcsApiUrl", "user" and "password" (a user must + * be used; guests do not work). + * - If HPB clustering is enabled, set the token of a conversation in "token" + * (otherwise leave empty). + * - Set whether to use audio, video or both in the "getUserMedia" call. + * - Set the desired numbers in "publishersCount" and + * "subscribersPerPublisherCount" (in a regular call with N participants you + * would have N publishers and N-1 subscribers). + * + * HOW TO RUN: + * ----------------------------------------------------------------------------- + * - In the browser, log in the Nextcloud server (with the same user as in this + * script). + * - Copy and paste the full script in the console of the browser to run it. + * - To run it again execute "closeConnections()" in the console; then you must + * reload the page and copy and paste the script again. + * + * HOW TO ENABLE AND DISABLE THE MEDIA DURING A TEST: + * ----------------------------------------------------------------------------- + * You can manually enable and disable the media during a test by copying and + * pasting in the browser console the following commands: + * - For audio: + * stream.getAudioTracks()[0].enabled = TRUE_OR_FALSE + * - For video: + * stream.getVideoTracks()[0].enabled = TRUE_OR_FALSE + * + * Note that you can only enable and disable the original media specified in the + * "getUserMedia" call. + * + * HOW TO CALIBRATE: + * ----------------------------------------------------------------------------- + * The script starts as many publishers and subscribers for each publisher as + * specified, each one being a real and independent peer connection to Janus. + * Therefore, how many peer connections can be established with a single script + * run depends on the client machine, both its CPU and its network. + * + * You should first "calibrate" the client by running it with different numbers + * of publishers and subscribers to check how many peer connections are + * supported before doing the actual stress test on Janus. + * + * It is most likely that the CPU or network of a single client will be + * saturated before Janus is. The script will write to the console when a peer + * could not be connected or if it was disconnected after being connected; if + * there is a large number of those messages or the CPU consumption is very high + * the client has probably reached its limit. + * + * Besides the messages written by the script itself you can manually check the + * connection state by copying and pasting in the browser console the following + * commands: + * - For the publishers: + * Object.values(publishers).forEach(publisher => { console.log(publisher.peerConnection.iceConnectionState) }) + * - For the subscribers: + * subscribers.forEach(subscriber => { console.log(subscriber.peerConnection.iceConnectionState) }) + * + * DISCLAIMER: + * ----------------------------------------------------------------------------- + * Talk performs some optimizations during calls, like reducing the video + * quality based on the number of participants. This script does not take into + * account those things; it is meant to be used for stress tests of Janus rather + * than to accurately simulate Talk behaviour. + * + * Independently of that, please note that an accurate simulation would not be + * possible given that a single client has to behave as several different + * clients. In a real call each client could have a different behaviour (not + * only due to sending different media streams, but also from having different + * CPU and network conditions), and that might even also affect Janus and the + * server regarding very low level things (but relevant for performance on + * highly loaded systems) like caches. + * + * Nevertheless, if those things are kept in mind the script can be anyway used + * as a rough performance test of specific call scenarios. Just remember that in + * regular calls peer connections increase quadratically with the number of + * participants; specifically, publishers increase linearly while subscribers + * increase quadratically. + * + * For example a call between 10 participants has 10 publishers and 90 + * subscribers (9 for each publisher) for a total of 100 peer connections, while + * a call between 30 participants has 30 publishers and 870 subscribers (29 for + * each publisher) for a total of 900 peer connections. + + * Due to this, if you have several clients that can only withstand ~100 peer + * connections each in order to simulate a 30 participants call you could run + * the script with 3 publishers and 29 subscribers per publisher on 10 clients + * at the same time. + */ + +talkOcsApiUrl = 'https://cloud.example.tld/ocs/v2.php/apps/spreed/api/' +signalingSettingsUrl = talkOcsApiUrl + 'v2/signaling/settings' +signalingBackendUrl = talkOcsApiUrl + 'v2/signaling/backend' + +// Guest users do not currently work, as they must join the conversation to not +// be kicked out from the signaling server. However, joining the conversation +// a second time causes the first guest to be unregistered. +// Regular users do not need to join the conversation, so the same user can be +// connected several times to the HPB. +user = '' +password = '' + +// The conversation token is only strictly needed for guests or if HPB +// clustering is enabled. +token = '' + +joinRoomUrl = talkOcsApiUrl + 'v3/room/' + token + '/participants/active' + +async function getSignalingSettings(user, password, token) { + const fetchOptions = { + headers: { + 'OCS-ApiRequest': true, + 'Accept': 'json', + }, + } + + if (user) { + fetchOptions.headers['Authorization'] = 'Basic ' + btoa(user + ':' + password) + } + + const signalingSettingsResponse = await fetch(signalingSettingsUrl + '?token=' + token, fetchOptions) + const signalingSettings = await signalingSettingsResponse.json() + + return signalingSettings.ocs.data +} + +/** + * Helper class to interact with the signaling server. + * + * A new signaling session is started when a Signaling object is created. + * Messages can be sent using the sendXXX methods. Received messages are emitted + * using events, so a listener for the type of received message can be set to + * listen to them; the message data is provided in the "detail" attribute of the + * event. + */ +class Signaling extends EventTarget { + constructor(user, signalingSettings) { + super(); + + this.user = user + this.signalingTicket = signalingSettings.ticket + + let resolveSessionId + this.sessionId = new Promise((resolve, reject) => { + resolveSessionId = resolve + }) + + const signalingUrl = this.sanitizeSignalingUrl(signalingSettings.server) + + this.socket = new WebSocket(signalingUrl) + this.socket.onopen = () => { + this.sendHello() + } + this.socket.onmessage = event => { + const data = JSON.parse(event.data) + + this.dispatchEvent(new CustomEvent(data.type, { detail: data[data.type] })) + } + this.socket.onclose = () => { + console.warn('Socket closed') + } + + this.addEventListener('hello', async event => { + const hello = event.detail + const sessionId = hello.sessionid + + this.sessionId = sessionId + resolveSessionId(sessionId) + + if (!user) { + // If the current user is a guest the room needs to be joined, + // as guests are kicked out if they just open a session in the + // signaling server. + this.joinRoom() + } + }) + + this.addEventListener('error', event => { + const error = event.detail + + console.warn(error) + }) + } + + sanitizeSignalingUrl(url) { + if (url.indexOf('https://') === 0) { + url = 'wss://' + url.substr(8) + } else if (url.indexOf('http://') === 0) { + url = 'ws://' + url.substr(7) + } + if (url[url.length - 1] === '/') { + url = url.substr(0, url.length - 1) + } + + return url + '/spreed' + } + + /** + * Returns the session ID. + * + * It can return either the actual session ID or a promise that is fulfilled + * once the session ID is available. Therefore this should be called with + * something like "sessionId = await signaling.getSessionId()". + */ + async getSessionId() { + return this.sessionId + } + + send(message) { + this.socket.send(JSON.stringify(message)) + } + + sendHello() { + this.send({ + 'type': 'hello', + 'hello': { + 'version': '1.0', + 'auth': { + 'url': signalingBackendUrl, + 'params': { + 'userid': this.user, + 'ticket': this.signalingTicket, + }, + }, + }, + }) + } + + sendMessage(data) { + this.send({ + 'type': 'message', + 'message': { + 'recipient': { + 'type': 'session', + 'sessionid': data.to, + }, + 'data': data + } + }) + } + + sendRequestOffer(publisherSessionId) { + this.send({ + 'type': 'message', + 'message': { + 'recipient': { + 'type': 'session', + 'sessionid': publisherSessionId, + }, + 'data': { + 'type': 'requestoffer', + 'roomType': 'video', + }, + }, + }) + } + + async joinRoom() { + const fetchOptions = { + headers: { + 'OCS-ApiRequest': true, + 'Accept': 'json', + }, + method: 'POST', + } + + const joinRoomResponse = await fetch(joinRoomUrl, fetchOptions) + const joinRoomResult = await joinRoomResponse.json() + const nextcloudSessionId = joinRoomResult.ocs.data.sessionId + + this.send({ + 'type': 'room', + 'room': { + 'roomid': token, + 'sessionid': nextcloudSessionId, + }, + }) + } +} + +/** + * Base class for publishers and subscribers. + * + * After a Peer is created it must be explicitly connected to the HPB by calling + * "connect()". This method returns a promise that is fulfilled once the peer + * has connected, or rejected if the peer has not connected yet after some time. + * "connect()" must be called once the signaling is already connected; this can + * be done by waiting for "signaling.getSessionId()". + * + * Subclasses must set the "sessionId" attribute. + */ +class Peer { + constructor(user, signalingSettings, signaling) { + this.signaling = signaling + + let iceServers = signalingSettings.stunservers + iceServers = iceServers.concat(signalingSettings.turnservers) + + this.peerConnection = new RTCPeerConnection({ iceServers: iceServers }) + this.peerConnection.onicecandidate = async event => { + const candidate = event.candidate + + if (candidate) { + this.send('candidate', candidate) + } + } + + this.connectedPromiseResolve = undefined + this.connectedPromiseReject = undefined + this.connectedPromise = new Promise((resolve, reject) => { + this.connectedPromiseResolve = resolve + this.connectedPromiseReject = reject + }) + } + + async connect() { + this.peerConnection.addEventListener('iceconnectionstatechange', () => { + if (this.peerConnection.iceConnectionState === 'connected' || this.peerConnection.iceConnectionState === 'completed') { + this.connectedPromiseResolve() + this.connected = true + } + }) + + const connectionTimeout = 5 + + setTimeout(() => { + if (!this.connected) { + this.connectedPromiseReject('Peer has not connected in ' + connectionTimeout + ' seconds') + } + }, connectionTimeout * 1000) + + return this.connectedPromise + } + + send(type, data) { + this.signaling.sendMessage({ + to: this.sessionId, + roomType: 'video', + type: type, + payload: data + }) + } +} + +/** + * Helper class for publishers. + * + * A single publisher can be used on each signaling session. + * + * A publisher is connected to the HPB by sending an offer and handling the + * returned answer. + */ +class Publisher extends Peer { + constructor(user, signalingSettings, signaling, stream) { + super(user, signalingSettings, signaling) + + stream.getTracks().forEach(track => { + this.peerConnection.addTrack(track, stream) + }) + + this.signaling.addEventListener('message', event => { + const message = event.detail + + if (message.data.type === 'answer') { + const answer = message.data.payload + this.peerConnection.setRemoteDescription(answer) + } + }) + } + + async connect() { + this.sessionId = await this.signaling.getSessionId() + + const offer = await this.peerConnection.createOffer({ offerToReceiveAudio: 0, offerToReceiveVideo: 0 }) + await this.peerConnection.setLocalDescription(offer) + + this.send('offer', offer) + + return super.connect() + } +} + +async function newPublisher(signalingSettings, signaling, stream) { + const publisher = new Publisher(user, signalingSettings, signaling, stream) + const sessionId = await publisher.signaling.getSessionId() + + return [sessionId, publisher] +} + +/** + * Helper class for subscribers. + * + * Several subscribers can be used on a single signaling session provided that + * each subscriber subscribes to a different publisher. + * + * A subscriber is connected to the HPB by requesting an offer, handling the + * returned offer and sending back an answer. + */ +class Subscriber extends Peer { + constructor(user, signalingSettings, signaling, publisherSessionId) { + super(user, signalingSettings, signaling) + + this.sessionId = publisherSessionId + + this.signaling.addEventListener('message', async event => { + const message = event.detail + + if (message.data.type === 'offer' && message.data.from === this.sessionId) { + const offer = message.data.payload + await this.peerConnection.setRemoteDescription(offer) + + const answer = await this.peerConnection.createAnswer() + await this.peerConnection.setLocalDescription(answer) + + this.send('answer', answer) + } + }) + } + + async connect() { + this.signaling.sendRequestOffer(this.sessionId) + + return super.connect() + } +} + +const connectionWarningTimeout = 5000 + +stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false, +}) + +publishersCount = 5 +publishers = [] + +function listenToPublisherConnectionChanges() { + Object.values(publishers).forEach(publisher => { + publisher.peerConnection.addEventListener('iceconnectionstatechange', event => { + if (publisher.peerConnection.iceConnectionState === 'connected' + || publisher.peerConnection.iceConnectionState === 'completed') { + clearTimeout(publisher.connectionWarning) + publisher.connectionWarning = null + + return + } + + if (publisher.peerConnection.iceConnectionState === 'disconnected') { + // Brief disconnections are normal and expected; they are only + // relevant if the connection has not been restored after some + // seconds. + publisher.connectionWarning = setTimeout(() => { + console.warn('Publisher disconnected', publisher.sessionId) + }, connectionWarningTimeout) + } else if (publisher.peerConnection.iceConnectionState === 'failed') { + console.warn('Publisher connection failed', publisher.sessionId) + } + }) + }) +} + +async function initPublishers() { + for (let i = 0; i < publishersCount; i++) { + const signalingSettings = await getSignalingSettings(user, password, token) + const signaling = new Signaling(user, signalingSettings) + + const [publisherSessionId, publisher] = await newPublisher(signalingSettings, signaling, stream) + + try { + await publisher.connect() + } catch (exception) { + console.warn('Publisher ' + i + ' error: ' + exception) + } + + publishers[publisherSessionId] = publisher + } + + console.info('Publishers started the siege') + + listenToPublisherConnectionChanges() +} + +await initPublishers() + +subscribersPerPublisherCount = 40 +subscribers = [] + +function listenToSubscriberConnectionChanges() { + subscribers.forEach(subscriber => { + subscriber.peerConnection.addEventListener('iceconnectionstatechange', event => { + if (subscriber.peerConnection.iceConnectionState === 'connected' + || subscriber.peerConnection.iceConnectionState === 'completed') { + clearTimeout(subscriber.connectionWarning) + subscriber.connectionWarning = null + + return + } + + if (subscriber.peerConnection.iceConnectionState === 'disconnected') { + // Brief disconnections are normal and expected; they are only + // relevant if the connection has not been restored after some + // seconds. + subscriber.connectionWarning = setTimeout(() => { + console.warn('Subscriber disconnected', subscriber.sessionId) + }, connectionWarningTimeout) + } else if (subscriber.peerConnection.iceConnectionState === 'failed') { + console.warn('Subscriber connection failed', subscriber.sessionId) + } + }) + }) +} + +async function initSubscribers() { + for (let i = 0; i < subscribersPerPublisherCount; i++) { + // The same signaling session can be shared between subscribers to + // different publishers. + const signalingSettings = await getSignalingSettings(user, password, token) + const signaling = new Signaling(user, signalingSettings) + + await signaling.getSessionId() + + Object.keys(publishers).forEach(async publisherSessionId => { + const subscriber = new Subscriber(user, signalingSettings, signaling, publisherSessionId) + + subscribers.push(subscriber) + }) + } + + for (let i = 0; i < subscribers.length; i++) { + try { + await subscribers[i].connect() + } catch (exception) { + console.warn('Subscriber ' + i + ' error: ' + exception) + } + } + + console.info('Subscribers started the siege') + + listenToSubscriberConnectionChanges() +} + +await initSubscribers() + +const closeConnections = function() { + subscribers.forEach(subscriber => { + subscriber.peerConnection.close() + }) + + Object.values(publishers).forEach(publisher => { + publisher.peerConnection.close() + }) + + stream.getTracks().forEach(track => { + track.stop() + }) +} -- cgit v1.2.3 From 436b636d61f135d25b84a88d72b15aa301bd97f5 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 20 May 2021 10:44:40 +0200 Subject: Define closeConnections() before starting siege Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 18323cbd3..93887e3e3 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -225,7 +225,7 @@ class Signaling extends EventTarget { async getSessionId() { return this.sessionId } - + send(message) { this.socket.send(JSON.stringify(message)) } @@ -559,8 +559,6 @@ async function initSubscribers() { listenToSubscriberConnectionChanges() } -await initSubscribers() - const closeConnections = function() { subscribers.forEach(subscriber => { subscriber.peerConnection.close() @@ -574,3 +572,7 @@ const closeConnections = function() { track.stop() }) } + +console.info('Preparing to siege') + +await initSubscribers() -- cgit v1.2.3 From 07cd488ce20b5da8b9a2df9e6a1b4c101d951bc1 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 20 May 2021 10:46:53 +0200 Subject: Move config values to the top Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 93887e3e3..5f1069b03 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -124,6 +124,11 @@ password = '' // clustering is enabled. token = '' +// Number of streams to send +publishersCount = 5 +// Number of streams to receive +subscribersPerPublisherCount = 40 + joinRoomUrl = talkOcsApiUrl + 'v3/room/' + token + '/participants/active' async function getSignalingSettings(user, password, token) { @@ -451,7 +456,6 @@ stream = await navigator.mediaDevices.getUserMedia({ video: false, }) -publishersCount = 5 publishers = [] function listenToPublisherConnectionChanges() { @@ -502,7 +506,6 @@ async function initPublishers() { await initPublishers() -subscribersPerPublisherCount = 40 subscribers = [] function listenToSubscriberConnectionChanges() { -- cgit v1.2.3 From 9a6af81a52d1978da073330ff3839fcdb310d2d2 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 21 May 2021 10:24:06 +0200 Subject: First define all things before starting to call functions Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 5f1069b03..9fb9e7ba7 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -457,6 +457,7 @@ stream = await navigator.mediaDevices.getUserMedia({ }) publishers = [] +subscribers = [] function listenToPublisherConnectionChanges() { Object.values(publishers).forEach(publisher => { @@ -504,10 +505,6 @@ async function initPublishers() { listenToPublisherConnectionChanges() } -await initPublishers() - -subscribers = [] - function listenToSubscriberConnectionChanges() { subscribers.forEach(subscriber => { subscriber.peerConnection.addEventListener('iceconnectionstatechange', event => { @@ -578,4 +575,5 @@ const closeConnections = function() { console.info('Preparing to siege') +await initPublishers() await initSubscribers() -- cgit v1.2.3 From ee191d3f242e54e663f601745f34dd4ad8647dcf Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 21 May 2021 10:47:26 +0200 Subject: Simplify configuration Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 57 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 23 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 9fb9e7ba7..2894349d9 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -22,11 +22,11 @@ /** * HOW TO SETUP: * ----------------------------------------------------------------------------- - * - Set the right values in "talkOcsApiUrl", "user" and "password" (a user must - * be used; guests do not work). + * - Set the right values in "user" and "password" (a user must be used; guests + * do not work). * - If HPB clustering is enabled, set the token of a conversation in "token" * (otherwise leave empty). - * - Set whether to use audio, video or both in the "getUserMedia" call. + * - Set whether to use audio, video or both in "mediaConstraints". * - Set the desired numbers in "publishersCount" and * "subscribersPerPublisherCount" (in a regular call with N participants you * would have N publishers and N-1 subscribers). @@ -108,28 +108,47 @@ * at the same time. */ -talkOcsApiUrl = 'https://cloud.example.tld/ocs/v2.php/apps/spreed/api/' -signalingSettingsUrl = talkOcsApiUrl + 'v2/signaling/settings' -signalingBackendUrl = talkOcsApiUrl + 'v2/signaling/backend' - // Guest users do not currently work, as they must join the conversation to not // be kicked out from the signaling server. However, joining the conversation // a second time causes the first guest to be unregistered. // Regular users do not need to join the conversation, so the same user can be // connected several times to the HPB. -user = '' -password = '' +const user = '' +const password = '' + +const signalingApiVersion = 2 // FIXME get from capabilities endpoint +const conversationApiVersion = 3 // FIXME get from capabilities endpoint // The conversation token is only strictly needed for guests or if HPB // clustering is enabled. -token = '' +const token = '' // Number of streams to send -publishersCount = 5 +const publishersCount = 5 // Number of streams to receive -subscribersPerPublisherCount = 40 +const subscribersPerPublisherCount = 40 + +const mediaConstraints = { + audio: true, + video: false, +} + +const connectionWarningTimeout = 5000 + +/* + * End of configuration section + */ + +// To run the script the current page in the browser must be a page of the +// target Nextcloud instance, as cross-doman requests are not allowed, so the +// host is directly got from the current location. +const talkOcsApiUrl = 'https://' + window.location.host + '/ocs/v2.php/apps/spreed/api/' +const signalingSettingsUrl = talkOcsApiUrl + 'v' + signalingApiVersion + '/signaling/settings' +const signalingBackendUrl = talkOcsApiUrl + 'v' + signalingApiVersion + '/signaling/backend' +const joinRoomUrl = talkOcsApiUrl + 'v' + conversationApiVersion + '/room/' + token + '/participants/active' -joinRoomUrl = talkOcsApiUrl + 'v3/room/' + token + '/participants/active' +const publishers = [] +const subscribers = [] async function getSignalingSettings(user, password, token) { const fetchOptions = { @@ -449,16 +468,6 @@ class Subscriber extends Peer { } } -const connectionWarningTimeout = 5000 - -stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: false, -}) - -publishers = [] -subscribers = [] - function listenToPublisherConnectionChanges() { Object.values(publishers).forEach(publisher => { publisher.peerConnection.addEventListener('iceconnectionstatechange', event => { @@ -575,5 +584,7 @@ const closeConnections = function() { console.info('Preparing to siege') +const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) + await initPublishers() await initSubscribers() -- cgit v1.2.3 From 2a702c6da10f1b6174f795eadd999a8774eb830d Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Mon, 31 May 2021 12:28:59 +0200 Subject: Add support for trickle candidates. This is option "full_trickle" in Janus. Signed-off-by: Joachim Bauch --- docs/Talkbuchet.js | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 2894349d9..45642fc1f 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -349,6 +349,15 @@ class Peer { } } + this.signaling.addEventListener('message', event => { + const message = event.detail + + if (message.data.type === 'candidate' && message.data.from === this.sessionId) { + const candidate = message.data.payload + this.peerConnection.addIceCandidate(candidate.candidate) + } + }) + this.connectedPromiseResolve = undefined this.connectedPromiseReject = undefined this.connectedPromise = new Promise((resolve, reject) => { -- cgit v1.2.3 From cd7e849d78a92ccecb84117c96575a2aa233cdaa Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 7 Jun 2021 16:02:52 +0200 Subject: Catch the exception on creating the signaling connection Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 45642fc1f..01c07adf4 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -505,7 +505,13 @@ function listenToPublisherConnectionChanges() { async function initPublishers() { for (let i = 0; i < publishersCount; i++) { const signalingSettings = await getSignalingSettings(user, password, token) - const signaling = new Signaling(user, signalingSettings) + let signaling = null + try { + signaling = new Signaling(user, signalingSettings) + } catch (exception) { + console.error('Publisher ' + i + ' init error: ' + exception) + continue + } const [publisherSessionId, publisher] = await newPublisher(signalingSettings, signaling, stream) @@ -553,7 +559,13 @@ async function initSubscribers() { // The same signaling session can be shared between subscribers to // different publishers. const signalingSettings = await getSignalingSettings(user, password, token) - const signaling = new Signaling(user, signalingSettings) + let signaling = null + try { + signaling = new Signaling(user, signalingSettings) + } catch (exception) { + console.error('Subscriber ' + i + ' init error: ' + exception) + continue + } await signaling.getSessionId() -- cgit v1.2.3 From 4b0a2afbf689aeb48940d7c7e9280691a1980e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 10 Jun 2021 13:40:56 +0200 Subject: Add helper functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- docs/Talkbuchet.js | 138 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 128 insertions(+), 10 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 01c07adf4..535aaa0f1 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -41,16 +41,30 @@ * * HOW TO ENABLE AND DISABLE THE MEDIA DURING A TEST: * ----------------------------------------------------------------------------- - * You can manually enable and disable the media during a test by copying and - * pasting in the browser console the following commands: + * You can manually enable and disable the media during a test by running the + * following commands in the browser console: * - For audio: - * stream.getAudioTracks()[0].enabled = TRUE_OR_FALSE + * setAudioEnabled(TRUE_OR_FALSE) * - For video: - * stream.getVideoTracks()[0].enabled = TRUE_OR_FALSE + * setVideoEnabled(TRUE_OR_FALSE) * * Note that you can only enable and disable the original media specified in the * "getUserMedia" call. * + * Additionally, you can also enable and disable the sent media streams during + * a test by running the following commands in the browser console: + * - For audio: + * setSentAudioStreamEnabled(TRUE_OR_FALSE) + * - For video: + * setSentVideoStreamEnabled(TRUE_OR_FALSE) + * + * Currently Firefox behaviour is the same whether the media is disabled or the + * sent media stream is disabled, so this makes no difference. Chromium, on the + * other hand, sends some media data when the media is disabled, but stops it + * when the sent media stream is disabled. In any case, please note that some + * data will be always sent as long as there is a connection open, even if no + * media is being sent. + * * HOW TO CALIBRATE: * ----------------------------------------------------------------------------- * The script starts as many publishers and subscribers for each publisher as @@ -69,12 +83,11 @@ * the client has probably reached its limit. * * Besides the messages written by the script itself you can manually check the - * connection state by copying and pasting in the browser console the following - * commands: + * connection state by running the following commands in the browser console: * - For the publishers: - * Object.values(publishers).forEach(publisher => { console.log(publisher.peerConnection.iceConnectionState) }) + * checkPublishersConnections() * - For the subscribers: - * subscribers.forEach(subscriber => { console.log(subscriber.peerConnection.iceConnectionState) }) + * checkSubscribersConnections() * * DISCLAIMER: * ----------------------------------------------------------------------------- @@ -150,6 +163,8 @@ const joinRoomUrl = talkOcsApiUrl + 'v' + conversationApiVersion + '/room/' + to const publishers = [] const subscribers = [] +const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) + async function getSignalingSettings(user, password, token) { const fetchOptions = { headers: { @@ -603,9 +618,112 @@ const closeConnections = function() { }) } -console.info('Preparing to siege') +const setAudioEnabled = function(enabled) { + if (!stream.getAudioTracks().length) { + console.error('Audio was not initialized') -const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) + return + } + + // There will be at most a single audio track. + stream.getAudioTracks()[0].enabled = enabled +} + +const setVideoEnabled = function(enabled) { + if (!stream.getVideoTracks().length) { + console.error('Video was not initialized') + + return + } + + // There will be at most a single video track. + stream.getVideoTracks()[0].enabled = enabled +} + +const setSentAudioStreamEnabled = function(enabled) { + if (!stream.getAudioTracks().length) { + console.error('Audio was not initialized') + + return + } + + Object.values(publishers).forEach(publisher => { + // For simplicity it is assumed that if audio is enabled the audio + // sender will always be the first one. + const audioSender = publisher.peerConnection.getSenders()[0] + if (enabled) { + audioSender.replaceTrack(stream.getAudioTracks()[0]) + } else { + audioSender.replaceTrack(null) + } + }) +} + +const setSentVideoStreamEnabled = function(enabled) { + if (!stream.getVideoTracks().length) { + console.error('Video was not initialized') + + return + } + + Object.values(publishers).forEach(publisher => { + // For simplicity it is assumed that if audio is not enabled the video + // sender will always be the first one, otherwise the second one. + let videoIndex = 0 + if (stream.getAudioTracks().length) { + videoIndex = 1 + } + + const videoSender = publisher.peerConnection.getSenders()[videoIndex] + if (enabled) { + videoSender.replaceTrack(stream.getVideoTracks()[0]) + } else { + videoSender.replaceTrack(null) + } + }) +} + +const checkPublishersConnections = function() { + const iceConnectionStateCount = {} + + Object.values(publishers).forEach(publisher => { + console.info(publisher.peerConnection.iceConnectionState) + + if (iceConnectionStateCount[publisher.peerConnection.iceConnectionState] === undefined) { + iceConnectionStateCount[publisher.peerConnection.iceConnectionState] = 1 + } else { + iceConnectionStateCount[publisher.peerConnection.iceConnectionState]++ + } + }) + + console.info('Summary:') + console.info(' - New: ' + (iceConnectionStateCount['new'] ?? 0)) + console.info(' - Connected: ' + ((iceConnectionStateCount['connected'] ?? 0) + (iceConnectionStateCount['completed'] ?? 0))) + console.info(' - Disconnected: ' + (iceConnectionStateCount['disconnected'] ?? 0)) + console.info(' - Failed: ' + (iceConnectionStateCount['failed'] ?? 0)) +} + +const checkSubscribersConnections = function() { + const iceConnectionStateCount = {} + + subscribers.forEach(subscriber => { + console.info(subscriber.peerConnection.iceConnectionState) + + if (iceConnectionStateCount[subscriber.peerConnection.iceConnectionState] === undefined) { + iceConnectionStateCount[subscriber.peerConnection.iceConnectionState] = 1 + } else { + iceConnectionStateCount[subscriber.peerConnection.iceConnectionState]++ + } + }) + + console.info('Summary:') + console.info(' - New: ' + (iceConnectionStateCount['new'] ?? 0)) + console.info(' - Connected: ' + ((iceConnectionStateCount['connected'] ?? 0) + (iceConnectionStateCount['completed'] ?? 0))) + console.info(' - Disconnected: ' + (iceConnectionStateCount['disconnected'] ?? 0)) + console.info(' - Failed: ' + (iceConnectionStateCount['failed'] ?? 0)) +} + +console.info('Preparing to siege') await initPublishers() await initSubscribers() -- cgit v1.2.3 From 32b3737ccd1a7459d985ca359e0d77783b0dc8c6 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 14 Jun 2021 11:33:01 +0200 Subject: Replace password with apptoken Signed-off-by: Joas Schilling --- docs/Talkbuchet.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'docs') diff --git a/docs/Talkbuchet.js b/docs/Talkbuchet.js index 535aaa0f1..ba5017eb8 100644 --- a/docs/Talkbuchet.js +++ b/docs/Talkbuchet.js @@ -22,8 +22,8 @@ /** * HOW TO SETUP: * ----------------------------------------------------------------------------- - * - Set the right values in "user" and "password" (a user must be used; guests - * do not work). + * - Set the right values in "user" and "appToken" (a user must be used; guests + * do not work; generate an apptoken at index.php/settings/user/security ). * - If HPB clustering is enabled, set the token of a conversation in "token" * (otherwise leave empty). * - Set whether to use audio, video or both in "mediaConstraints". @@ -127,7 +127,7 @@ // Regular users do not need to join the conversation, so the same user can be // connected several times to the HPB. const user = '' -const password = '' +const appToken = '' const signalingApiVersion = 2 // FIXME get from capabilities endpoint const conversationApiVersion = 3 // FIXME get from capabilities endpoint @@ -165,7 +165,7 @@ const subscribers = [] const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) -async function getSignalingSettings(user, password, token) { +async function getSignalingSettings(user, appToken, token) { const fetchOptions = { headers: { 'OCS-ApiRequest': true, @@ -174,7 +174,7 @@ async function getSignalingSettings(user, password, token) { } if (user) { - fetchOptions.headers['Authorization'] = 'Basic ' + btoa(user + ':' + password) + fetchOptions.headers['Authorization'] = 'Basic ' + btoa(user + ':' + appToken) } const signalingSettingsResponse = await fetch(signalingSettingsUrl + '?token=' + token, fetchOptions) @@ -519,7 +519,7 @@ function listenToPublisherConnectionChanges() { async function initPublishers() { for (let i = 0; i < publishersCount; i++) { - const signalingSettings = await getSignalingSettings(user, password, token) + const signalingSettings = await getSignalingSettings(user, appToken, token) let signaling = null try { signaling = new Signaling(user, signalingSettings) @@ -573,7 +573,7 @@ async function initSubscribers() { for (let i = 0; i < subscribersPerPublisherCount; i++) { // The same signaling session can be shared between subscribers to // different publishers. - const signalingSettings = await getSignalingSettings(user, password, token) + const signalingSettings = await getSignalingSettings(user, appToken, token) let signaling = null try { signaling = new Signaling(user, signalingSettings) -- cgit v1.2.3