Skip to content
This repository has been archived by the owner on Jul 17, 2024. It is now read-only.

Commit

Permalink
Update rtc score layer
Browse files Browse the repository at this point in the history
  • Loading branch information
kamil-stasiak committed Mar 19, 2024
1 parent b2c37d1 commit 0ff49a4
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 30 deletions.
2 changes: 1 addition & 1 deletion assets/src/contexts/DeveloperInfoContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useContext, useState } from "react";
import { AudioStats, VideoStats } from "../pages/room/components/StreamPlayer/rtcMosScore.ts";
import { AudioStats, VideoStats } from "../pages/room/components/StreamPlayer/rtcMOS1.ts";

export type VideoStatistics = VideoStats & { type: "video" }
export type AudioStatistics = AudioStats & { type: "audio" }
Expand Down
2 changes: 1 addition & 1 deletion assets/src/pages/room/RoomPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { getTokenAndAddress } from "../../room.api";
import { useStreaming } from "../../features/streaming/StreamingContext.tsx";
import { useLocalPeer } from "../../features/devices/LocalPeerMediaContext.tsx";
import { InboundRtpId, useDeveloperInfo } from "../../contexts/DeveloperInfoContext.tsx";
import { AudioStatsSchema, VideoStatsSchema } from "./components/StreamPlayer/rtcMosScore.ts";
import { AudioStatsSchema, VideoStatsSchema } from "./components/StreamPlayer/rtcMOS1.ts";

type ConnectComponentProps = {
username: string;
Expand Down
105 changes: 80 additions & 25 deletions assets/src/pages/room/components/StreamPlayer/StatisticsLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AudioStatistics, useDeveloperInfo, VideoStatistics } from "../../../../contexts/DeveloperInfoContext.tsx";
import { calculateAudioScore, calculateVideoScore } from "./rtcMosScore.ts";
import { calculateAudioScore, calculateVideoScore } from "./rtcMOS1.ts";
import { SIMULCAST_BANDWIDTH_LIMITS } from "../../bandwidth.tsx";
import { calculateOpenTokAudioScore, calculateOpenTokVideoScore } from "./rtcMOS2.ts";


const maxScoreHighLayer = calculateVideoScore({
const maxScore1HighLayer = calculateVideoScore({
codec: "",
bitrate: SIMULCAST_BANDWIDTH_LIMITS.get("h")! * 1024,
bufferDelay: 0,
Expand All @@ -14,7 +15,7 @@ const maxScoreHighLayer = calculateVideoScore({
expectedHeight: 720
});

const maxScoreMediumLayer = calculateVideoScore({
const maxScore1MediumLayer = calculateVideoScore({
codec: "",
bitrate: SIMULCAST_BANDWIDTH_LIMITS.get("m")! * 1024,
bufferDelay: 0,
Expand All @@ -25,7 +26,7 @@ const maxScoreMediumLayer = calculateVideoScore({
expectedHeight: 720
});

const maxScoreLowLayer = calculateVideoScore({
const maxScore1LowLayer = calculateVideoScore({
codec: "",
bitrate: SIMULCAST_BANDWIDTH_LIMITS.get("l")! * 1024,
bufferDelay: 0,
Expand All @@ -36,7 +37,25 @@ const maxScoreLowLayer = calculateVideoScore({
expectedHeight: 720
});

const maxAudioScore = calculateAudioScore(
const maxScore2HighLayer = calculateOpenTokVideoScore({
width: 1280,
height: 720,
bitrate: SIMULCAST_BANDWIDTH_LIMITS.get("h")! * 1024
});

const maxScore2MediumLayer = calculateOpenTokVideoScore({
width: 1280,
height: 720,
bitrate: SIMULCAST_BANDWIDTH_LIMITS.get("m")! * 1024
});

const maxScore2LowLayer = calculateOpenTokVideoScore({
width: 1280,
height: 720,
bitrate: SIMULCAST_BANDWIDTH_LIMITS.get("l")! * 1024
});

const maxAudioScore1 = calculateAudioScore(
{
bitrate: 30_000,
bufferDelay: 0,
Expand All @@ -46,10 +65,24 @@ const maxAudioScore = calculateAudioScore(
dtx: false
});

const maxAudioScore2 = calculateOpenTokAudioScore(
{
roundTripTime: 0,
packetLoss: 0
});


export type Props = { videoTrackId: string | null, audioTrackId: string | null }

const numberFormatter = new Intl.NumberFormat("pl-PL", { minimumFractionDigits: 3 });
const decimal3FractionFormatter = new Intl.NumberFormat("pl-PL", {
minimumFractionDigits: 3,
maximumFractionDigits: 3
});
const decimal2FractionFormatter = new Intl.NumberFormat("pl-PL", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
const integerFormatter = new Intl.NumberFormat("pl-PL", { maximumFractionDigits: 0 });

export const StatisticsLayer = ({ videoTrackId, audioTrackId }: Props) => {

Expand All @@ -70,6 +103,12 @@ export const StatisticsLayer = ({ videoTrackId, audioTrackId }: Props) => {
expectedHeight: 720
});

const openTokVideoScore = !videoStats ? 0 : calculateOpenTokVideoScore({
width: 1280,
height: 720,
bitrate: videoStats.bitrate
});

const audioRawStats = statistics.data[audioTrackId || ""];
const audioStats: AudioStatistics | undefined = audioRawStats?.type === "audio" ? audioRawStats : undefined;

Expand All @@ -83,6 +122,12 @@ export const StatisticsLayer = ({ videoTrackId, audioTrackId }: Props) => {
dtx: audioStats.dtx
});

const openTokAudioScore = !audioStats ? 0 : calculateOpenTokAudioScore(
{
roundTripTime: audioStats.roundTripTime,
packetLoss: audioStats.packetLoss
});

return <div className="absolute right-0 bottom-0 z-50 !text-xs text-black md:text-base bg-white/50 p-2">
<table className="border-separate border-spacing-x-2">
<thead>
Expand All @@ -94,32 +139,37 @@ export const StatisticsLayer = ({ videoTrackId, audioTrackId }: Props) => {
</thead>
<tbody>
<tr>
<th>score</th>
<td>{videoScore.toString()}</td>
<td>{audioScore.toString()}</td>
<th>score 1 [unit]</th>
<td>{decimal2FractionFormatter.format(videoScore).toString()} ({decimal2FractionFormatter.format(maxScore1HighLayer > 0 ? videoScore * 100 / maxScore1HighLayer : NaN).toString()}%)</td>
<td>{decimal2FractionFormatter.format(audioScore).toString()}</td>
</tr>
<tr>
<th>score 2 [unit]</th>
<td>{decimal2FractionFormatter.format(openTokVideoScore).toString()} ({decimal2FractionFormatter.format(maxScore2HighLayer > 0 ? openTokVideoScore * 100 / maxScore2HighLayer : NaN).toString()}%)</td>
<td>{decimal2FractionFormatter.format(openTokAudioScore).toString()} ({decimal2FractionFormatter.format(maxAudioScore2 > 0 ? openTokAudioScore * 100 / maxAudioScore2 : NaN).toString()}%)</td>
</tr>
<tr>
<th>bitrate</th>
<td>{numberFormatter.format(videoStats?.bitrate ?? NaN).toString()}</td>
<td>{numberFormatter.format(audioStats?.bitrate ?? NaN).toString()}</td>
<th>bitrate [bps]</th>
<td>{integerFormatter.format(videoStats?.bitrate ?? NaN).toString()}</td>
<td>{integerFormatter.format(audioStats?.bitrate ?? NaN).toString()}</td>
</tr>
<tr>
<th>bufferDelay</th>
<td>{numberFormatter.format(videoStats?.bufferDelay ?? NaN).toString()}</td>
<td>{numberFormatter.format(audioStats?.bufferDelay ?? NaN).toString()}</td>
<th>bufferDelay [s]</th>
<td>{decimal3FractionFormatter.format(videoStats?.bufferDelay ?? NaN).toString()}</td>
<td>{decimal3FractionFormatter.format(audioStats?.bufferDelay ?? NaN).toString()}</td>
</tr>
<tr>
<th>roundTripTime</th>
<td>{numberFormatter.format(videoStats?.roundTripTime ?? NaN).toString()}</td>
<td>{numberFormatter.format(audioStats?.roundTripTime ?? NaN).toString()}</td>
<th>roundTripTime [s]</th>
<td>{decimal3FractionFormatter.format(videoStats?.roundTripTime ?? NaN).toString()}</td>
<td>{decimal3FractionFormatter.format(audioStats?.roundTripTime ?? NaN).toString()}</td>
</tr>
<tr>
<th>packetLoss</th>
<td>{videoStats?.packetLoss ?? NaN.toString()}</td>
<td>{audioStats?.packetLoss ?? NaN.toString()}</td>
<th>packetLoss [%]</th>
<td>{decimal3FractionFormatter.format(videoStats?.packetLoss ?? NaN).toString()}</td>
<td>{decimal3FractionFormatter.format(audioStats?.packetLoss ?? NaN).toString()}</td>
</tr>
<tr>
<th>frameRate</th>
<th>frameRate [fps]</th>
<td>{videoStats?.frameRate ?? NaN.toString()}</td>
<td></td>
</tr>
Expand All @@ -134,9 +184,14 @@ export const StatisticsLayer = ({ videoTrackId, audioTrackId }: Props) => {
<td>{audioStats?.fec?.toString()}</td>
</tr>
<tr>
<th>max score</th>
<td>{maxScoreHighLayer}, {maxScoreMediumLayer}, {maxScoreLowLayer}</td>
<td>{maxAudioScore}</td>
<th>est. max score 1</th>
<td>{maxScore1HighLayer}, {maxScore1MediumLayer}, {maxScore1LowLayer}</td>
<td>{maxAudioScore1}</td>
</tr>
<tr>
<th>est. max score 2</th>
<td>{decimal2FractionFormatter.format(maxScore2HighLayer).toString()}, {decimal2FractionFormatter.format(maxScore2MediumLayer).toString()}, {decimal2FractionFormatter.format(maxScore2LowLayer).toString()}</td>
<td>{decimal2FractionFormatter.format(maxAudioScore2).toString()}</td>
</tr>
</tbody>
</table>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// This is a rewritten implementation of https://github.com/ggarber/rtcscore
// Original implementation: https://github.com/ggarber/rtcscore
// I just adjusted the code to our needs.
// This implementation reflects the score for the last second rather than for the entire session.

import { z } from "zod";

Expand All @@ -8,14 +10,14 @@ export const VideoStatsSchema = z.object({
bufferDelay: z.number().default(0),
codec: z.string().optional(),
frameRate: z.number().default(0),
packetLoss: z.number().default(0)
packetLoss: z.number().default(0) // %
});

export const AudioStatsSchema = z.object({
bitrate: z.number().default(0),
roundTripTime: z.number().default(0),
bufferDelay: z.number().default(0),
packetLoss: z.number().default(0),
packetLoss: z.number().default(0), // %
fec: z.boolean().default(false),
dtx: z.boolean().default(false)
});
Expand Down
78 changes: 78 additions & 0 deletions assets/src/pages/room/components/StreamPlayer/rtcMOS2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Original implementation: https://github.com/wobbals/opentok-mos-estimator
// I just adjusted the code to our needs.
// This implementation reflects the score for the last second rather than for the entire session.

type VideoParams = {
width: number,
height: number,
bitrate: number,
}

const targetBitrateForPixelCount = function(pixelCount: number) {
// power function maps resolution to target bitrate, based on rumor config
// values, with r^2 = 0.98. We're ignoring frame rate, assume 30.
const y = 2.069924867 * Math.pow(Math.log10(pixelCount), 0.6250223771);
return Math.pow(10, y);
};

const MIN_VIDEO_BITRATE = 30000;

export const calculateOpenTokVideoScore = ({ width, height, bitrate }: VideoParams) => {
const targetBitrate = targetBitrateForPixelCount(width * height);

if (bitrate < MIN_VIDEO_BITRATE) {
return 0;
}
const newBitrate = Math.min(bitrate, targetBitrate);

return (Math.log(newBitrate / MIN_VIDEO_BITRATE) /
Math.log(targetBitrate / MIN_VIDEO_BITRATE)) * 4 + 1;
};

type AudioParams = {
packetLoss: number,
roundTripTime: number,
}

export const calculateOpenTokAudioScore = ({ packetLoss, roundTripTime }: AudioParams) => {
const audioScore = function(rtt: number, plr: number) {
const LOCAL_DELAY = 20; //20 msecs: typical frame duration
function H(x) {
return (x < 0 ? 0 : 1);
}

const a = 0; // ILBC: a=10
const b = 19.8;
const c = 29.7;

//R = 94.2 − Id − Ie
const R = function(rtt: number, packetLoss: number) {
const d = rtt + LOCAL_DELAY;
const Id = 0.024 * d + 0.11 * (d - 177.3) * H(d - 177.3);

const P = packetLoss;
const Ie = a + b * Math.log(1 + c * P);

const R = 94.2 - Id - Ie;

return R;
};

//For R < 0: MOS = 1
//For 0 R 100: MOS = 1 + 0.035 R + 7.10E-6 R(R-60)(100-R)
//For R > 100: MOS = 4.5
const MOS = function(R) {
if (R < 0) {
return 1;
}
if (R > 100) {
return 4.5;
}
return 1 + 0.035 * R + 7.10 / 1000000 * R * (R - 60) * (100 - R);
};

return MOS(R(rtt, plr));
};

return audioScore(roundTripTime, packetLoss);
};

0 comments on commit 0ff49a4

Please sign in to comment.