diff --git a/index.js b/index.js index 1187a08..2dcd0d3 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,29 @@ /* jshint node: true */ -'use strict'; +"use strict" -var EventEmitter = require('events').EventEmitter; -var Reporter = require('./lib/reporter'); -var MonitoredConnection = require('./lib/connection').MonitoredConnection; -var error = require('./lib/error'); -var util = require('./lib/util'); -var wildcard = require('wildcard'); -const detectProvider = require('./stats'); +var EventEmitter = require("events").EventEmitter +var Reporter = require("./lib/reporter") +var MonitoredConnection = require("./lib/connection").MonitoredConnection +var error = require("./lib/error") +var util = require("./lib/util") +var wildcard = require("wildcard") +const detectProvider = require("./stats") var OPTIONAL_MONITOR_EVENTS = [ - 'negotiate:request', 'negotiate:renegotiate', 'negotiate:abort', - 'negotiate:createOffer', 'negotiate:createOffer:created', - 'negotiate:createAnswer', 'negotiate:createAnswer:created', - 'negotiate:setlocaldescription', - 'icecandidate:local', 'icecandidate:remote', 'icecandidate:gathered', 'icecandidate:added', - 'sdp:received' -]; + "negotiate:request", + "negotiate:renegotiate", + "negotiate:abort", + "negotiate:createOffer", + "negotiate:createOffer:created", + "negotiate:createAnswer", + "negotiate:createAnswer:created", + "negotiate:setlocaldescription", + "icecandidate:local", + "icecandidate:remote", + "icecandidate:gathered", + "icecandidate:added", + "sdp:received", +] /** # rtc-health @@ -37,38 +44,43 @@ var OPTIONAL_MONITOR_EVENTS = [ This captures data for video streaming **/ -module.exports = function(qc, opts) { +module.exports = function (qc, opts) { + opts = opts || {} - opts = opts || {}; - - var provider = null; - var emitter = new EventEmitter(); - emitter.pollInterval = opts.pollInterval || 1000; - var connections = {}; - var timers = {}; - var logs = {}; + var provider = null + var emitter = new EventEmitter() + emitter.pollInterval = opts.pollInterval || 1000 + var connections = {} + var timers = {} + var logs = {} function log(peerId, pc, data) { - if (!provider) return; - return provider.getStats(pc).then((reports) => { - const tc = connections[data.id]; - - // Only reschedule while we are monitoring - if (tc) { - timers[data.id] = setTimeout(log.bind(this, peerId, pc, data), emitter.pollInterval); - } + if (!provider) return + return provider + .getStats(pc) + .then((reports) => { + const tc = connections[data.id] + + // Only reschedule while we are monitoring + if (tc) { + timers[data.id] = setTimeout( + log.bind(this, peerId, pc, data), + emitter.pollInterval + ) + } - var reporter = new Reporter({ - source: qc.id, - about: data, - pc: pc, - reports: reports - }); - - emitter.emit('health:report', reporter, pc); - }).catch((err) => { - // No operation - }); + var reporter = new Reporter({ + source: qc.id, + about: data, + pc: pc, + reports: reports, + }) + + emitter.emit("health:report", reporter, pc) + }) + .catch((err) => { + // No operation + }) } /** @@ -76,25 +88,25 @@ module.exports = function(qc, opts) { within quickconnect **/ function notify(eventName, opts) { - var args = Array.prototype.slice.call(arguments, 2); - emitter.emit('health:notify', eventName, opts, args); - emitter.emit.apply(emitter, (['health:' + eventName, opts].concat(args))); + var args = Array.prototype.slice.call(arguments, 2) + emitter.emit("health:notify", eventName, opts, args) + emitter.emit.apply(emitter, ["health:" + eventName, opts].concat(args)) } function connectionFailure() { - var args = Array.prototype.slice.call(arguments); - emitter.emit.apply(emitter, ['health:connection:failure'].concat(args)); + var args = Array.prototype.slice.call(arguments) + emitter.emit.apply(emitter, ["health:connection:failure"].concat(args)) } function trackConnection(peerId, pc, data) { var tc = new MonitoredConnection(qc, pc, data, { timeUntilFailure: opts.connectionFailureTime, onFailure: connectionFailure, - }); - connections[data.id] = tc; - notify('started', { source: qc.id, about: data.id, tracker: tc }); - log(peerId, pc, data); - return tc; + }) + connections[data.id] = tc + notify("started", { source: qc.id, about: data.id, tracker: tc }) + log(peerId, pc, data) + return tc } /** @@ -102,129 +114,148 @@ module.exports = function(qc, opts) { disconnecting, or the call ending (prior to a PC being created) **/ function connectionClosed(peerId) { - var tc = connections[peerId]; - if (!tc) return; - tc.closed(); + var tc = connections[peerId] + if (!tc) return + tc.closed() // Stop the reporting for this peer connection - if (timers[peerId]) clearTimeout(timers[peerId]); - delete connections[peerId]; + if (timers[peerId]) clearTimeout(timers[peerId]) + delete connections[peerId] // Emit a closure status update - emitter.emit('health:report', new Reporter({ - source: qc.id, - about: { - id: peerId, - room: tc.room - }, - status: 'closed', - force: true - })); - - notify('closed', { source: qc.id, about: peerId, tracker: tc }); + emitter.emit( + "health:report", + new Reporter({ + source: qc.id, + about: { + id: peerId, + room: tc.room, + }, + status: "closed", + force: true, + }) + ) + + notify("closed", { source: qc.id, about: peerId, tracker: tc }) } /** Handle the peer connection being created **/ - qc.on('peer:connect', trackConnection); - - qc.on('peer:couple', function(peerId, pc, data, monitor) { + qc.on("peer:connect", trackConnection) + qc.on("peer:couple", function (peerId, pc, data, monitor) { // Store that we are currently tracking the target peer - var tc = connections[data.id]; - var status = util.toStatus(pc.iceConnectionState); - if (!tc) tc = trackConnection(peerId, pc, data); - - monitor.on('statechange', function(pc, state) { - var iceConnectionState = pc.iceConnectionState; - var newStatus = util.toStatus(iceConnectionState); - notify('icestatus', { - source: qc.id, about: data.id, tracker: tc - }, iceConnectionState); - emitter.emit('health:changed', tc, iceConnectionState); + var tc = connections[data.id] + var status = util.toStatus(pc.iceConnectionState) + if (!tc) tc = trackConnection(peerId, pc, data) + + monitor.on("statechange", function (pc, state) { + var iceConnectionState = pc.iceConnectionState + var newStatus = util.toStatus(iceConnectionState) + notify( + "icestatus", + { + source: qc.id, + about: data.id, + tracker: tc, + }, + iceConnectionState + ) + emitter.emit("health:changed", tc, iceConnectionState) if (status != newStatus) { - emitter.emit('health:connection:status', tc, newStatus, status); - status = newStatus; - if (status === 'connected') { - tc.connected(); - } else if (status === 'error') { - tc.failed('ICE connection state error'); + emitter.emit("health:connection:status", tc, newStatus, status) + status = newStatus + if (status === "connected") { + tc.connected() + } else if (status === "error") { + tc.failed("ICE connection state error") } } - }); + }) - monitor.on('closed', connectionClosed.bind(this, peerId)); - }); + monitor.on("closed", connectionClosed.bind(this, peerId)) + }) - qc.on('call:failed', function(peerId) { - var tc = connections[peerId]; - tc.failed('Call failed to connect'); - }); + qc.on("call:failed", function (peerId) { + var tc = connections[peerId] + tc.failed("Call failed to connect") + }) // Close tracked connections on call:ended as well - qc.on('call:ended', connectionClosed); + qc.on("call:ended", connectionClosed) // Setup to listen to the entire feed - qc.feed(function(evt) { - var name = evt.name; + qc.feed(function (evt) { + var name = evt.name if (util.SIGNALLER_EVENTS.indexOf(name) >= 0) { return notify.apply( notify, - [name, { source: qc.id, about: 'signaller' }].concat(evt.args) - ); + [name, { source: qc.id, about: "signaller" }].concat(evt.args) + ) } // Listen for the optional verbose events - var matching = opts.verbose && wildcard('pc.*', name); + var matching = opts.verbose && wildcard("pc.*", name) if (matching) { - var peerId = matching[1]; - var shortName = matching.slice(2).join('.'); - var tc = connections[peerId]; + var peerId = matching[1] + var shortName = matching.slice(2).join(".") + var tc = connections[peerId] return notify.apply( notify, - [shortName, { source: qc.id, about: peerId, tracker: tc }].concat(evt.args) - ); + [shortName, { source: qc.id, about: peerId, tracker: tc }].concat( + evt.args + ) + ) } - }); + }) // Helper method to safely close all connections - emitter.closeConnections = function() { + emitter.closeConnections = function () { for (var connId in connections) { - connections[connId].close(); + connections[connId].close() } - }; + } // Helper method to expose the current tracked connections - emitter.getConnections = function() { - var results = []; + emitter.getConnections = function () { + var results = [] for (var target in connections) { - results.push(connections[target]); + results.push(connections[target]) } - return results; - }; + return results + } // Get the tracked connection for the given target peer - emitter.getConnection = function(target) { - return connections && connections[target]; - }; + emitter.getConnection = function (target) { + return connections && connections[target] + } + + // Override provider's getStats function with provided getStats function + emitter.setGetStatsFn = function (getStatsFn) { + if (!provider) return + + provider.provider.setGetStatsFn?.(getStatsFn) + } // Provider detection function detect() { - provider = detectProvider(opts); + provider = detectProvider(opts) if (!provider) { - console.log('WARNING! No WebRTC provider detected - rtc-health is disabled until a provider is detected'); + console.log( + "WARNING! No WebRTC provider detected - rtc-health is disabled until a provider is detected" + ) } } // In the case of some plugins, we don't get notified that it's using a plugin, and // have no means to reasonably identify the existence of a plugin // So as a final fallback, rerun the detection on a local announce - qc.once('local:announce', detect); + qc.once("local:announce", detect) // Attempt to detect initially - detect(); - return emitter; -}; + detect() + return emitter +} diff --git a/lib/provider.js b/lib/provider.js index 74f29cb..0f58775 100644 --- a/lib/provider.js +++ b/lib/provider.js @@ -2,26 +2,29 @@ Detects and loads the correct provider for this browser **/ var providers = [ - require('./providers/standard'), - require('./providers/google'), - require('./providers/mozilla'), - require('./providers/temasys'), - require('./providers/unsupported'), -]; + require("./providers/standard"), + require("./providers/twilio"), + require("./providers/google"), + require("./providers/mozilla"), + require("./providers/temasys"), + require("./providers/unsupported"), +] -module.exports = function(opts) { - var detected = null; - var requested = opts && opts.provider; +module.exports = function (opts) { + var detected = null + var requested = opts && opts.provider + // Check for the existing of the RTCPeerConnection + for (var i = 0; i < providers.length; i++) { + var pv = providers[i] + var RTCPeerConnection = window[(pv.RTC_PREFIX || "") + "RTCPeerConnection"] + if ( + (requested && requested === pv.id && RTCPeerConnection) || + (!requested && pv.check && pv.check()) + ) { + detected = pv + break + } + } - // Check for the existing of the RTCPeerConnection - for (var i = 0; i < providers.length; i++) { - var pv = providers[i]; - var RTCPeerConnection = window[(pv.RTC_PREFIX || '') + 'RTCPeerConnection']; - if ((requested && requested === pv.id) || RTCPeerConnection || (!requested && pv.check && pv.check())) { - detected = pv; - break; - } - } - - return detected; -}; \ No newline at end of file + return detected +} diff --git a/lib/providers/twilio.js b/lib/providers/twilio.js new file mode 100644 index 0000000..8dafcb0 --- /dev/null +++ b/lib/providers/twilio.js @@ -0,0 +1,149 @@ +/** + * The standard provider deals with StatsReport that adhere to the common standard + */ +var bowser = require("bowser") +var StatsReport = require("../statsreport") +var util = require("../util") +var EXCLUDE_FIELDS = ["id", "type"] +// Don't send reports about the certificate or codec information +var EXCLUDE_TYPES = ["certificate", "codec"] +var FIELD_PREFIX = "" +var getStats = undefined + +/** + Convert the Chrome RTCStatsReport to a StatsReport + **/ +function convertToStatsReport(report, compare) { + if (!report || !report.type || EXCLUDE_TYPES.indexOf(report.type) !== -1) + return + + var result = new StatsReport({ + id: report.id, + type: util.standardizeKey(FIELD_PREFIX, report.type), + subType: + report.type === "ssrc" + ? report.id.indexOf("send") > 0 + ? "send" + : "receive" + : undefined, + timestamp: report.timestamp, + version: "1.0", // WebRTC 1.0 + }) + + Object.keys(report) + .filter((k) => EXCLUDE_FIELDS.indexOf(k) === -1) + .map((key) => { + var standardKey = util.standardizeKey(FIELD_PREFIX, key) + var value = report[key] + result.set(standardKey, value) + }) + return result +} + +/** + Twilio WebRTC Stats Report + **/ +exports.id = "twilio" + +/** + + **/ +exports.getStats = function (pc, opts, callback) { + return new Promise((resolve, reject) => { + if (!getStats || typeof getStats !== "function") return resolve() + // Handle stats requests hanging on Firefox if the connection is closing + let timer = setTimeout(() => reject("Timed out"), 1000) + getStats().then((stats) => { + console.log("khoa debug stats", stats) + clearTimeout(timer) + if (!stats) return callback("Could not getStats") + let localAudioStats = stats[0].localAudioTrackStats[0] + let localVideoStats = stats[0].localVideoTrackStats[0] + let remoteAudioStats = stats[0].remoteAudioTrackStats[0] + let remoteVideoStats = stats[0].remoteVideoTrackStats[0] + + let candidatePair = localAudioStats + ? convertToStatsReport({ + id: stats[0].peerConnectionId, + type: "candidate-pair", + timestamp: localAudioStats.timestamp, + bytesSent: + (localAudioStats?.bytesSent || 0) + + (localVideoStats?.bytesSent || 0), + bytesSentPerSec: 0, + bytesReceived: + (remoteAudioStats?.bytesReceived || 0) + + (remoteVideoStats?.bytesReceived || 0), + bytesReceivedPerSec: 0, + currentRoundTripTime: 0, + writable: true, + nominated: true, + }) + : undefined + let inboundRtpAudio = remoteAudioStats + ? convertToStatsReport({ + id: remoteAudioStats.trackId, + type: "inbound-rtp", + timestamp: remoteAudioStats.timestamp, + ...remoteAudioStats, + mediaType: "audio", + kind: "audio", + }) + : undefined + let inboundRtpVideo = remoteVideoStats + ? convertToStatsReport({ + id: remoteVideoStats.trackId, + type: "inbound-rtp", + timestamp: remoteVideoStats.timestamp, + ...remoteVideoStats, + mediaType: "video", + kind: "video", + }) + : undefined + let outboundRtpAudio = localAudioStats + ? convertToStatsReport({ + id: localAudioStats.trackId, + type: "outbound-rtp", + timestamp: localAudioStats.timestamp, + ...localAudioStats, + mediaType: "audio", + kind: "audio", + }) + : undefined + let outboundRtpVideo = localVideoStats + ? convertToStatsReport({ + id: localVideoStats.trackId, + type: "outbound-rtp", + timestamp: localVideoStats.timestamp, + ...localVideoStats, + mediaType: "video", + kind: "video", + }) + : undefined + return resolve( + [ + inboundRtpAudio, + inboundRtpVideo, + outboundRtpAudio, + outboundRtpVideo, + candidatePair, + ].filter((s) => !!s) + ) + }) + }) + .then((data) => callback(null, data)) + .catch((err) => { + callback("Could not getStats") + }) +} + +/** + Check if we are using a browser that is supporting the standardized stats reports + **/ +exports.check = function () { + return bowser && bowser.check({ safari: "12" }, true) +} + +exports.setGetStatsFn = function (getStatsFn) { + getStats = getStatsFn +}