Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added support for video bandwidth from getStats() #38

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/js/rtc_session.js
Original file line number Diff line number Diff line change
Expand Up @@ -895,9 +895,9 @@ export default class RtcSession {
var timestamp = new Date();

var impl = async (stream, streamType) => {
var tracks = [];
let tracks = [];

if (! stream) {
if (!stream) {
return [];
}

Expand All @@ -910,13 +910,16 @@ export default class RtcSession {
case 'video_output':
tracks = stream.getVideoTracks();
break;
case 'video_bandwidth':
tracks = stream.getVideoTracks();
break;
default:
throw new Error('Unsupported stream type while trying to get stats: ' + streamType);
}

return await Promise.all(tracks.map(async (track) => {
var rawStats = await this._pc.getStats(track);
var digestedStats = extractMediaStatsFromStats(timestamp, rawStats, streamType);
const rawStats = await this._pc.getStats(track);
const digestedStats = extractMediaStatsFromStats(timestamp, rawStats, streamType);
if (! digestedStats) {
throw new Error('Failed to extract MediaRtpStats from RTCStatsReport for stream type ' + streamType);
}
Expand All @@ -933,8 +936,10 @@ export default class RtcSession {

video: {
input: await impl(this._remoteVideoStream, 'video_input'),
output: await impl(this._localStream, 'video_output')
}
output: await impl(this._localStream, 'video_output'),
// Choose either stream as the underlying data is the same
bandwidth: await impl(this._remoteVideoStream || this._localStream, 'video_bandwidth'),
},
};

// For consistency's sake, coalesce rttMilliseconds into the output for audio and video.
Expand Down
119 changes: 89 additions & 30 deletions src/js/rtp-stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,121 @@
*/

import { is_defined, when_defined } from './utils';

export function extractMediaStatsFromStats(timestamp, stats, streamType) {
var extractedStats = null;
let extractedStats = null;

for (var key in stats) {
for (let key in stats) {
var statsReport = stats[key];
if (statsReport) {
if (statsReport.type === 'ssrc') {
//chrome, opera case. chrome reports stats for all streams, not just the stream passed in.
if (is_defined(statsReport.packetsSent) && statsReport.mediaType == 'audio' && streamType === 'audio_input') {
extractedStats = {
extractedStats = new MediaRtpStats({
timestamp: timestamp,
packetsCount: statsReport.packetsSent,
bytesSent: statsReport.bytesSent,
audioLevel: when_defined(statsReport.audioInputLevel),
packetsLost: is_defined(statsReport.packetsLost) ? Math.max(0, statsReport.packetsLost) : 0,
procMilliseconds: is_defined(statsReport.googCurrentDelayMs),
rttMilliseconds: when_defined(statsReport.googRtt)
};
}, statsReport.type, streamType);

} else if (is_defined(statsReport.packetsReceived) && statsReport.mediaType == 'audio' && streamType === 'audio_output') {
extractedStats = {
extractedStats = new MediaRtpStats({
timestamp: timestamp,
packetsCount: statsReport.packetsReceived,
bytesReceived: statsReport.bytesReceived,
audioLevel: when_defined(statsReport.audioOutputLevel),
packetsLost: is_defined(statsReport.packetsLost) ? Math.max(0, statsReport.packetsLost) : 0,
procMilliseconds: is_defined(statsReport.googCurrentDelayMs),
jbMilliseconds: when_defined(statsReport.googJitterBufferMs)
};
}, statsReport.type, streamType);

} else if (is_defined(statsReport.packetsSent) && statsReport.mediaType == 'video' && streamType === 'video_input') {
extractedStats = {
extractedStats = new MediaRtpStats({
timestamp: timestamp,
packetsCount: statsReport.packetsSent,
bytesSent: statsReport.bytesSent,
packetsLost: is_defined(statsReport.packetsLost) ? Math.max(0, statsReport.packetsLost) : 0,
rttMilliseconds: when_defined(statsReport.googRtt),
procMilliseconds: is_defined(statsReport.googCurrentDelayMs),
frameRateSent: when_defined(statsReport.googFrameRateSent),
};
}, statsReport.type, streamType);

} else if (typeof statsReport.packetsReceived !== 'undefined' && statsReport.mediaType == 'video' && streamType === 'video_output') {
extractedStats = {
extractedStats = new MediaRtpStats({
timestamp: timestamp,
packetsCount: statsReport.packetsSent,
bytesReceived: statsReport.bytesReceived,
packetsLost: is_defined(statsReport.packetsLost) ? Math.max(0, statsReport.packetsLost) : 0,
frameRateReceived: when_defined(statsReport.googFrameRateReceived),
procMilliseconds: is_defined(statsReport.googCurrentDelayMs),
jbMilliseconds: when_defined(statsReport.googJitterBufferMs)
};
}, statsReport.type, streamType);

}
} else if (statsReport.type === 'inboundrtp') {
// Firefox case. Firefox reports packetsLost parameter only in inboundrtp type, and doesn't report in outboundrtp type.
// So we only pull from inboundrtp. Firefox reports only stats for the stream passed in.
if (is_defined(statsReport.packetsLost) && is_defined(statsReport.packetsReceived)) {
extractedStats = {
extractedStats = new MediaRtpStats({
packetsLost: statsReport.packetsLost,
packetsCount: statsReport.packetsReceived,
audioLevel: when_defined(statsReport.audioInputLevel),
rttMilliseconds: streamType === 'audio_ouptut' || streamType === 'video_output' ? when_defined(statsReport.roundTripTime) : null,
rttMilliseconds: streamType === 'audio_output' || streamType === 'video_output' ? when_defined(statsReport.roundTripTime) : null,
jbMilliseconds: streamType === 'audio_output' || streamType === 'video_output' ? when_defined(statsReport.jitter, 0) * 1000 : null
};
}, statsReport.type, streamType);
}
} else if (statsReport.type === 'VideoBwe') {
if (streamType === 'video_bandwidth') {
extractedStats = new MediaBWEForVideoStats({
timestamp: timestamp,
encBitrate: statsReport.googActualEncBitrate,
availReceiveBW: statsReport.googAvailableReceiveBandwidth,
availSendBW: statsReport.googAvailableSendBandwidth,
bucketDelay: statsReport.googBucketDelay,
retransmitBitrate: statsReport.googRetransmitBitrate,
targetEncBitrate: statsReport.googTargetEncBitrate,
transmitBitrate: statsReport.googTransmitBitrate
}, statsReport.type, streamType);
}
}
}
}
return extractedStats ? extractedStats : null;
}

return extractedStats ? new MediaRtpStats(extractedStats, statsReport.type, streamType) : null;
/**
* Basic statistics object, represents core properties of any statistic
*/
class BaseStats {
constructor(params = {}, statsReportType, streamType) {
this._timestamp = params.timestamp || new Date().getTime();
this._statsReportType = statsReportType || params._statsReportType || "unknown";
this._streamType = streamType || params.streamType || "unknown";
}
/** Timestamp when stats are collected. */
get timestamp() {
return this._timestamp;
}
/** {string} the type of the stats report */
get statsReportType() {
return this._statsReportType;
}
/** {string} the type of the stream */
get streamType() {
return this._streamType;
}
}

/**
* Basic RTP statistics object, represents statistics of an audio or video stream.
*/
class MediaRtpStats {
constructor(paramsIn, statsReportType, streamType) {
var params = paramsIn || {};
class MediaRtpStats extends BaseStats {
constructor(params = {}, statsReportType, streamType) {
super(params, statsReportType, streamType);

this._timestamp = params.timestamp || new Date().getTime();
this._packetsLost = when_defined(params.packetsLost);
this._packetsCount = when_defined(params.packetsCount);
this._audioLevel = when_defined(params.audioLevel);
Expand All @@ -96,8 +131,6 @@ class MediaRtpStats {
this._framesDecoded = when_defined(params.framesDecoded);
this._frameRateSent = when_defined(params.frameRateSent);
this._frameRateReceived = when_defined(params.frameRateReceived);
this._statsReportType = statsReportType || params._statsReportType || "unknown";
this._streamType = streamType || params.streamType || "unknown";
}

/** {number} number of packets sent to the channel */
Expand All @@ -118,10 +151,6 @@ class MediaRtpStats {
get audioLevel() {
return this._audioLevel;
}
/** Timestamp when stats are collected. */
get timestamp() {
return this._timestamp;
}
/** {number} Round trip time calculated with RTCP reports */
get rttMilliseconds() {
return this._rttMilliseconds;
Expand Down Expand Up @@ -154,12 +183,42 @@ class MediaRtpStats {
get frameRateReceived() {
return this._frameRateReceived;
}
/** {string} the type of the stats report */
get statsReportType() {
return this._statsReportType;
}

/**
* Basic BWEForVideo statistics object, represents statistics of an audio or video stream.
*/
class MediaBWEForVideoStats extends BaseStats {
constructor(params = {}, statsReportType, streamType) {
super(params, statsReportType, streamType);

this._encBitrate = when_defined(params.encBitrate);
this._availReceiveBW = when_defined(params.availReceiveBW);
this._availSendBW = when_defined(params.availSendBW);
this._bucketDelay = when_defined(params.bucketDelay);
this._retransmitBitrate = when_defined(params.retransmitBitrate);
this._targetEncBitrate = when_defined(params.targetEncBitrate);
this._transmitBitrate = when_defined(params.transmitBitrate);
}
/** {string} the type of the stream */
get streamType() {
return this._streamType;
get encBitrate() {
return this._encBitrate;
}
get availReceiveBW() {
return this._availReceiveBW;
}
get availSendBW() {
return this._availSendBW;
}
get bucketDelay() {
return this._bucketDelay;
}
get retransmitBitrate() {
return this._retransmitBitrate;
}
get targetEncBitrate() {
return this._targetEncBitrate;
}
get transmitBitrate() {
return this._transmitBitrate;
}
}