Included new getStats monitor for audio and video
This commit is contained in:
parent
a434f61052
commit
5c71e2d0bb
@ -21,6 +21,10 @@ export default class BaseAudioBridge {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPeerConnection() {
|
||||||
|
console.error('The Bridge must implement getPeerConnection');
|
||||||
|
}
|
||||||
|
|
||||||
exitAudio() {
|
exitAudio() {
|
||||||
console.error('The Bridge must implement exitAudio');
|
console.error('The Bridge must implement exitAudio');
|
||||||
}
|
}
|
||||||
|
@ -246,6 +246,13 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
|||||||
return this.media.outputDeviceId || value;
|
return this.media.outputDeviceId || value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPeerConnection() {
|
||||||
|
const { webRtcPeer } = window.kurentoManager.kurentoAudio;
|
||||||
|
if (webRtcPeer) {
|
||||||
|
return webRtcPeer.peerConnection;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
exitAudio() {
|
exitAudio() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -561,6 +561,14 @@ export default class SIPBridge extends BaseAudioBridge {
|
|||||||
return this.activeSession.transferCall(onTransferSuccess);
|
return this.activeSession.transferCall(onTransferSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPeerConnection() {
|
||||||
|
const { currentSession } = this.activeSession;
|
||||||
|
if (currentSession && currentSession.mediaHandler) {
|
||||||
|
return currentSession.mediaHandler.peerConnection;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
exitAudio() {
|
exitAudio() {
|
||||||
return this.activeSession.exitAudio();
|
return this.activeSession.exitAudio();
|
||||||
}
|
}
|
||||||
|
@ -504,6 +504,7 @@ class VideoProvider extends Component {
|
|||||||
if (peer && peer.peerConnection) {
|
if (peer && peer.peerConnection) {
|
||||||
const conn = peer.peerConnection;
|
const conn = peer.peerConnection;
|
||||||
conn.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(cameraId, isLocal);
|
conn.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(cameraId, isLocal);
|
||||||
|
VideoService.monitor(conn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,14 @@ import VideoStreams from '/imports/api/video-streams';
|
|||||||
import UserListService from '/imports/ui/components/user-list/service';
|
import UserListService from '/imports/ui/components/user-list/service';
|
||||||
import { makeCall } from '/imports/ui/services/api';
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
import { notify } from '/imports/ui/services/notification';
|
import { notify } from '/imports/ui/services/notification';
|
||||||
|
import { monitorVideoConnection } from '/imports/utils/stats';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
|
|
||||||
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
|
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
|
||||||
|
|
||||||
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
|
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
|
||||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||||
|
const ENABLE_NETWORK_MONITORING = Meteor.settings.public.networkMonitoring.enableNetworkMonitoring;
|
||||||
|
|
||||||
const TOKEN = '_';
|
const TOKEN = '_';
|
||||||
|
|
||||||
@ -279,6 +281,10 @@ class VideoService {
|
|||||||
getRole(isLocal) {
|
getRole(isLocal) {
|
||||||
return isLocal ? 'share' : 'viewer';
|
return isLocal ? 'share' : 'viewer';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
monitor(conn) {
|
||||||
|
if (ENABLE_NETWORK_MONITORING) monitorVideoConnection(conn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoService = new VideoService();
|
const videoService = new VideoService();
|
||||||
@ -301,6 +307,7 @@ export default {
|
|||||||
processInboundIceQueue: (peer, cameraId) => videoService.processInboundIceQueue(peer, cameraId),
|
processInboundIceQueue: (peer, cameraId) => videoService.processInboundIceQueue(peer, cameraId),
|
||||||
getRole: isLocal => videoService.getRole(isLocal),
|
getRole: isLocal => videoService.getRole(isLocal),
|
||||||
getSharedDevices: () => videoService.getSharedDevices(),
|
getSharedDevices: () => videoService.getSharedDevices(),
|
||||||
|
monitor: conn => videoService.monitor(conn),
|
||||||
onBeforeUnload: () => videoService.onBeforeUnload(),
|
onBeforeUnload: () => videoService.onBeforeUnload(),
|
||||||
notify: message => notify(message, 'error', 'video'),
|
notify: message => notify(message, 'error', 'video'),
|
||||||
};
|
};
|
||||||
|
@ -8,8 +8,11 @@ import { notify } from '/imports/ui/services/notification';
|
|||||||
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
|
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
|
||||||
import iosWebviewAudioPolyfills from '/imports/utils/ios-webview-audio-polyfills';
|
import iosWebviewAudioPolyfills from '/imports/utils/ios-webview-audio-polyfills';
|
||||||
import { tryGenerateIceCandidates } from '/imports/utils/safari-webrtc';
|
import { tryGenerateIceCandidates } from '/imports/utils/safari-webrtc';
|
||||||
|
import { monitorAudioConnection } from '/imports/utils/stats';
|
||||||
import AudioErrors from './error-codes';
|
import AudioErrors from './error-codes';
|
||||||
|
|
||||||
|
const ENABLE_NETWORK_MONITORING = Meteor.settings.public.networkMonitoring.enableNetworkMonitoring;
|
||||||
|
|
||||||
const MEDIA = Meteor.settings.public.media;
|
const MEDIA = Meteor.settings.public.media;
|
||||||
const MEDIA_TAG = MEDIA.mediaTag;
|
const MEDIA_TAG = MEDIA.mediaTag;
|
||||||
const ECHO_TEST_NUMBER = MEDIA.echoTestNumber;
|
const ECHO_TEST_NUMBER = MEDIA.echoTestNumber;
|
||||||
@ -49,6 +52,7 @@ class AudioManager {
|
|||||||
this.useKurento = Meteor.settings.public.kurento.enableListenOnly;
|
this.useKurento = Meteor.settings.public.kurento.enableListenOnly;
|
||||||
this.failedMediaElements = [];
|
this.failedMediaElements = [];
|
||||||
this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this);
|
this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this);
|
||||||
|
this.monitor = this.monitor.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(userData) {
|
init(userData) {
|
||||||
@ -307,6 +311,7 @@ class AudioManager {
|
|||||||
window.parent.postMessage({ response: 'joinedAudio' }, '*');
|
window.parent.postMessage({ response: 'joinedAudio' }, '*');
|
||||||
this.notify(this.intl.formatMessage(this.messages.info.JOINED_AUDIO));
|
this.notify(this.intl.formatMessage(this.messages.info.JOINED_AUDIO));
|
||||||
logger.info({ logCode: 'audio_joined' }, 'Audio Joined');
|
logger.info({ logCode: 'audio_joined' }, 'Audio Joined');
|
||||||
|
if (ENABLE_NETWORK_MONITORING) this.monitor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,6 +518,12 @@ class AudioManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
monitor() {
|
||||||
|
const bridge = (this.useKurento && this.isListenOnly) ? this.listenOnlyBridge : this.bridge;
|
||||||
|
const peer = bridge.getPeerConnection();
|
||||||
|
monitorAudioConnection(peer);
|
||||||
|
}
|
||||||
|
|
||||||
handleAllowAutoplay() {
|
handleAllowAutoplay() {
|
||||||
window.removeEventListener('audioPlayFailed', this.handlePlayElementFailed);
|
window.removeEventListener('audioPlayFailed', this.handlePlayElementFailed);
|
||||||
|
|
||||||
|
167
bigbluebutton-html5/imports/utils/stats.js
Normal file
167
bigbluebutton-html5/imports/utils/stats.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import logger from '/imports/startup/client/logger';
|
||||||
|
|
||||||
|
STATS_LENGTH = 5;
|
||||||
|
STATS_INTERVAL = 2000;
|
||||||
|
|
||||||
|
const collect = (conn, callback) => {
|
||||||
|
let stats = [];
|
||||||
|
|
||||||
|
const monitor = (conn, stats) => {
|
||||||
|
if (!conn) {
|
||||||
|
callback(clearResult());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.getStats().then(results => {
|
||||||
|
if (!results) {
|
||||||
|
callback(clearResult());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inboundRTP;
|
||||||
|
let remoteInboundRTP;
|
||||||
|
|
||||||
|
results.forEach(res => {
|
||||||
|
switch (res.type) {
|
||||||
|
case 'inbound-rtp':
|
||||||
|
inboundRTP = res;
|
||||||
|
break;
|
||||||
|
case 'remote-inbound-rtp':
|
||||||
|
remoteInboundRTP = res;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inboundRTP || remoteInboundRTP) {
|
||||||
|
if (!inboundRTP) {
|
||||||
|
const { peerIdentity } = conn;
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
logCode: 'missing_inbound_rtc',
|
||||||
|
extraInfo: { peerIdentity }
|
||||||
|
},
|
||||||
|
'Missing local inbound RTC. Using remote instead'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.push(buildData(inboundRTP || remoteInboundRTP));
|
||||||
|
while (stats.length > STATS_LENGTH) stats.shift();
|
||||||
|
|
||||||
|
const interval = calculateInterval(stats);
|
||||||
|
callback(buildResult(interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(monitor, STATS_INTERVAL, conn, stats);
|
||||||
|
}).catch(error => logger.error(error));
|
||||||
|
};
|
||||||
|
monitor(conn, stats);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildData = inboundRTP => {
|
||||||
|
return {
|
||||||
|
packets: {
|
||||||
|
received: inboundRTP.packetsReceived,
|
||||||
|
lost: inboundRTP.packetsLost
|
||||||
|
},
|
||||||
|
bytes: {
|
||||||
|
received: inboundRTP.bytesReceived
|
||||||
|
},
|
||||||
|
jitter: inboundRTP.jitter
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildResult = (interval) => {
|
||||||
|
const rate = calculateRate(interval.packets);
|
||||||
|
return {
|
||||||
|
packets: {
|
||||||
|
received: interval.packets.received,
|
||||||
|
lost: interval.packets.lost
|
||||||
|
},
|
||||||
|
bytes: {
|
||||||
|
received: interval.bytes.received
|
||||||
|
},
|
||||||
|
jitter: interval.jitter,
|
||||||
|
rate: rate,
|
||||||
|
loss: calculateLoss(rate),
|
||||||
|
MOS: calculateMOS(rate)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearResult = () => {
|
||||||
|
return { loss: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const diff = (single, first, last) => Math.abs((single ? 0 : last) - first);
|
||||||
|
|
||||||
|
const calculateInterval = (stats) => {
|
||||||
|
const single = stats.length === 1;
|
||||||
|
const first = stats[0];
|
||||||
|
const last = stats[stats.length - 1];
|
||||||
|
return {
|
||||||
|
packets: {
|
||||||
|
received: diff(single, first.packets.received, last.packets.received),
|
||||||
|
lost: diff(single, first.packets.lost, last.packets.lost)
|
||||||
|
},
|
||||||
|
bytes: {
|
||||||
|
received: diff(single, first.bytes.received, last.bytes.received)
|
||||||
|
},
|
||||||
|
jitter: diff(single, first.jitter, last.jitter)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRate = (packets) => {
|
||||||
|
const { received, lost } = packets;
|
||||||
|
const rate = (received > 0) ? ((received - lost) / received) * 100 : 100;
|
||||||
|
if (rate < 0 || rate > 100) return 100;
|
||||||
|
return rate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateLoss = (rate) => {
|
||||||
|
return 1 - (rate / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateMOS = (rate) => {
|
||||||
|
return 1 + (0.035) * rate + (0.000007) * rate * (rate - 60) * (100 - rate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitorAudioConnection = conn => {
|
||||||
|
if (!conn) return;
|
||||||
|
|
||||||
|
const { peerIdentity } = conn;
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
logCode: 'stats_audio_monitor',
|
||||||
|
extraInfo: { peerIdentity }
|
||||||
|
},
|
||||||
|
'Starting to monitor audio connection'
|
||||||
|
);
|
||||||
|
|
||||||
|
collect(conn, (result) => {
|
||||||
|
const event = new CustomEvent('audiostats', { detail: result });
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitorVideoConnection = conn => {
|
||||||
|
if (!conn) return;
|
||||||
|
|
||||||
|
const { peerIdentity } = conn;
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
logCode: 'stats_video_monitor',
|
||||||
|
extraInfo: { peerIdentity }
|
||||||
|
},
|
||||||
|
'Starting to monitor video connection'
|
||||||
|
);
|
||||||
|
|
||||||
|
collect(conn, (result) => {
|
||||||
|
const event = new CustomEvent('videostats', { detail: result });
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
monitorAudioConnection,
|
||||||
|
monitorVideoConnection
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user