Public
Edited
Mar 21, 2023
Insert cell
Insert cell
score({
audio: {
packetLoss: 5,
roundTripTime: 50,
},
video: {
bitrate: 100_000,
roundTripTime: 50,
codec: 'vp9',
frameRate: 60,
expectedFrameRate: 30,
width: 320,
height: 240
}
})
Insert cell
function score(stats) {
/*
* Stats
* packetLoss: 0-100%
* bitrate: bps
* roundTripTime: ms
* bufferDelay: ms
* codec: opus / vp8 / vp9 / h264 (only used for video)
* fec: boolean (ony used for audio)
* dtx: boolean (ony used for audio)
* qp: number (not used yet)
* keyFrames: number (not used yet)
* width: number; Resolution of the video received
* expectedWidth: number; Resolution of the rendering widget
* height: number; Resolution of the video received
* expectedHeight: number; Resolution of the rendering widget
* frameRate: number; FrameRate of the video received
* expectedFrameRate: number; FrameRate of the video source
*/
const scores = {};
const { audio, video } = normalize(stats);
if (audio) {
// Audio MOS calculation is based on E-Model algorithm
// Assume 20 packetization delay
const delay = 20 + audio.bufferDelay + audio.roundTripTime / 2;
const pl = audio.packetLoss;
const R0 = 100;
// Ignore audio bitrate in dtx mode
const Ie = audio.dtx
? 8
: audio.bitrate
? clamp(55 - 4.6 * Math.log(audio.bitrate), 0, 30)
: 6;
const Bpl = audio.fec ? 20 : 10;
const Ipl = Ie + (100 - Ie) * (pl / (pl + Bpl));

const Id = delay * 0.03 + (delay > 150 ? 0.1 * delay - 150 : 0);
const R = clamp(R0 - Ipl - Id, 0, 100);
const MOS = 1 + 0.035 * R + (R * (R - 60) * (100 - R) * 7) / 1000000;

scores.audio = clamp(Math.round(MOS * 100) / 100, 1, 5);
}
if (video) {
const pixels = video.expectedWidth * video.expectedHeight;
const codecFactor = video.codec === 'vp9' ? 1.2 : 1.0;
const delay = video.bufferDelay + video.roundTripTime / 2;
// These parameters are generated with a logaritmic regression
// on some very limited test data for now
// They are based on the bits per pixel per frame (bPPPF)
if (video.frameRate !== 0) {
const bPPPF = (codecFactor * video.bitrate) / pixels / video.frameRate;
const base = clamp(0.56 * Math.log(bPPPF) + 5.36, 1, 5);
const MOS =
base -
1.9 * Math.log(video.expectedFrameRate / video.frameRate) -
delay * 0.002;
scores.video = clamp(Math.round(MOS * 100) / 100, 1, 5);
} else {
scores.video = 1;
}
}
return scores;
}
Insert cell
Insert cell
function normalize(stats) {
return {
audio: stats.audio
? {
packetLoss: 0,
bufferDelay: 50,
roundTripTime: 50,
fec: true,
...stats.audio,
}
: undefined,
video: stats.video
? {
packetLoss: 0,
bufferDelay: 0,
roundTripTime: 50,
fec: false,
expectedHeight: stats.video.height || 640,
expectedWidth: stats.video.width || 480,
frameRate: stats.video.expectedFrameRate || 30,
expectedFrameRate: stats.video.frameRate || 30,
...stats.video,
}
: undefined,
};
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more