Игорь Шеко
Lead of Front-end team
Ирина Ламарр
Senior SDK developer
Input devices
(camera, microphone)
Output devices
(screen, speaker)
Sender device
Web browser
Receiver device
Web browser
Network interface controller
Network interface controller
Packets
Packets
Encoder
Decoder
Echo canceller
Noise reduction
Image enhancements
Packetizer
Depacketizer
Jitter buffer
Frames samples
Packets
Raw
signal
Raw
signal
https://www.w3.org/TR/webrtc-stats
https://wpt.fyi/results/webrtc-stats
function calculateBitrate(
bytesNow: number,
bytesBefore: number,
timestampNow: number,
timestampBefore: number
): number {
const deltaBytes = bytesSentNow - bytesSentBefore;
const deltaMs = timestampNow - timestampBefore;
return (deltaBytes / deltaMs) * 8000;
}
Oпределяет размер и качество видео- и аудиофайлов: чем выше битрейт, тем лучше качество и больше размер файла.
Размер файла = битрейт (кбит/с) x продолжительность.
I-frame
P-frame
B-frame
I-frame
P-frame
I-frame
//we can't use user-agent because
// 1. browsers often improve API,
// 2. user-agent is legacy
function detectStrategy(statsSample: RTCStatsReport): StatsStrategy {
//Browsers will check by global usage
// strategy for a chromium-based browsers
if (checkBlink(statsSample, IS_DEBUG)) {
return blinkStatsStrategy;
}
// strategy for a webkit-based browsers
if (checkWebkit(statsSample, IS_DEBUG)) {
return webkitStatsStrategy;
}
// strategy for a firefox-based browsers
if (checkGecko(statsSample, IS_DEBUG)) {
return geckoStatsStrategy;
}
// fallback rfc-like strategy
return baseStatsStrategy;
}
Default interval time | 1000 ms |
> 16ms |
2000 ms |
> 32ms |
3000 ms |
> 48ms |
4000 ms |
// This will only working in the Chrome. For other browsers we can't get battery.
//@ts-ignore because it is deprecated API
try {
const batteryFunction = navigator['getBattery'];
if (batteryFunction) {
const batteryInfo = await batteryFunction();
if (!batteryInfo.charging) {
if (batteryInfo.level && batteryInfo.level <= 0.3)
return LOW_BATTERY_COLLECT_INTERVAL;
return BATTERY_COLLECT_INTERVAL;
}
}
} catch (e){}
не ориентироваться на стандарт
описано много, в реальности работает мало что из этого или написано что-то похожее, но свое
собирать динамически и адаптивно
не все браузеры охотно сообщают, что они там поменяли
возможно придется самостоятельно пересчитывать какие-то метрики, если они вам нужны
не забывать про слабые девайсы и мобильники - слишком частый запрос статистики ведет к плохому UХ
Сбор статистики для решения конкретных инцидентов
Мониторинг инцидентов при обновлениях сервиса
Мониторинг инцидентов при обновлении браузеров
Определение качества работы сети
Помощь в разработке
Красивые графики
Простые флаги и healthcheck
Подсветка типичных проблем и советы по решению
Законно
White label
GW
ClickHouse
GW
ClickHouse
0.8 Gbps
1.1 TB / day
1.1 TB / day
Callstats.io
TestRTC
не расширяется
написаны под конкретные кейсы
писали не мы
Частота сбора: 1 сек
Средний объем конференции: 10 человек
Конференций на инстанс: 300
Хотим хранить: 30 дней
~168 Kbps
~1680 Kbps
~ 165 Mbps
~ 5.06 TB
Уменьшаем json
Меняем формат
Переход к дельтам
Два типа кадров
{
id: 'RTCInboundRTPVideoStream_1635808784',
timestamp: 1628606438021.0002,
type: 'inbound-rtp',
codecId: 'RTCCodec_v4_Inbound_104',
kind: 'video',
mediaType: 'video',
ssrc: 1635808784,
transportId: 'RTCTransport_0_1',
packetsLost: 49,
packetsReceived: 5198,
bytesReceived: 5720775,
estimatedPlayoutTimestamp: 3837595237710,
firCount: 0,
frameHeight: 180,
frameWidth: 320,
framesDecoded: 516,
framesPerSecond: 17,
framesReceived: 518,
headerBytesReceived: 240747,
keyFramesDecoded: 38,
lastPacketReceivedTimestamp: 46062.656,
nackCount: 43,
pliCount: 3,
qpSum: 9966,
totalDecodeTime: 1.038,
totalInterFrameDelay: 60.22499999999982,
totalSquaredInterFrameDelay: 57.38087100000012,
trackId: 'RTCMediaStreamTrack_receiver_11',
},
{
timestamp: 1628606438021.0002,
type: 'inbound-rtp',
mediaType: 'video',
ssrc: 1635808784,
packetsLost: 49,
packetsReceived: 5198,
bytesReceived: 5720775,
estimatedPlayoutTimestamp: 3837595237710,
firCount: 0,
frameHeight: 180,
frameWidth: 320,
framesDecoded: 516,
framesPerSecond: 17,
framesReceived: 518,
headerBytesReceived: 240747,
keyFramesDecoded: 38,
lastPacketReceivedTimestamp: 46062.656,
nackCount: 43,
pliCount: 3,
qpSum: 9966,
totalDecodeTime: 1.038,
totalInterFrameDelay: 60.22499999999982,
totalSquaredInterFrameDelay: 57.38087100000012,
},
[
1628606438021.0002,
"inbound-rtp",
"video",
1635808784,
49,
5198,
5720775,
3837595237710,
0,
180,
320,
516,
17,
518,
240747,
38,
46062.656,
43,
3,
9966,
1.038,
60.22499999999982,
57.38087100000012,
],
{
timestamp: 1628606438021.0002,
type: 'inbound-rtp',
mediaType: 'video',
ssrc: 1635808784,
packetsLost: 49,
packetsReceived: 5198,
bytesReceived: 5720775,
estimatedPlayoutTimestamp: 3837595237710,
firCount: 0,
frameHeight: 180,
frameWidth: 320,
framesDecoded: 516,
framesPerSecond: 17,
framesReceived: 518,
headerBytesReceived: 240747,
keyFramesDecoded: 38,
lastPacketReceivedTimestamp: 46062.656,
nackCount: 43,
pliCount: 3,
qpSum: 9966,
totalDecodeTime: 1.038,
totalInterFrameDelay: 60.22499999999982,
totalSquaredInterFrameDelay: 57.38087100000012,
},
{
type: 'inbound-rtp',
mediaType: 'video',
ssrc: 1635808784,
framesPerSecond: 17,
delta:[
[
timestamp: 1628606438021.0002,
packetsReceived: 5198,
bytesReceived: 5720775,
estimatedPlayoutTimestamp: 3837595237710,
framesDecoded: 516,
framesReceived: 518,
headerBytesReceived: 240747,
lastPacketReceivedTimestamp: 46062.656,
qpSum: 9966,
totalDecodeTime: 1.038,
totalInterFrameDelay: 60.22499999999982,
totalSquaredInterFrameDelay: 57.38087100000012,
keyFramesDecoded: 38,
],[
packetsLost: 49,
nackCount: 43,
pliCount: 3,
frameHeight: 180,
frameWidth: 320,
firCount: 0,
]
],
},
[
'inbound-rtp',
'video',
1635808784,
17,
[
[
1628606438021.0002,
5198,
5720775,
3837595237710,
516,
518,
240747,
46062.656,
9966,
1.038,
60.22499999999982,
57.38087100000012,
38,
],[
49,
43,
3,
180,
320,
0,
]
],
},
[
'inbound-rtp',
'video',
1635808784,
17,
[
[
1021.0001,
5198,
775,
710,
17,
17,
1747,
62.256,
367,
0.316,
1.22499999999982,
2.38087100000012,
0,
],[
1,
2,
0,
0,
0,
0,
]
],
},
[
'inbound-rtp',
'video',
1635808784,
17,
[
[
1021.0001,
5198,
775,
710,
17,
17,
1747,
62.256,
367,
0.316,
1.22499999999982,
2.38087100000012,
],[
1,
2,
]
],
},
[
'inbound-rtp','video',1635808784,17,
[[1021.0001,5198,775,710,17,17,1747,62.256,367,0.316,1.22499999999982,2.38087100000012],[1,2]]
],
{
id: 'RTCInboundRTPVideoStream_1635808784',
timestamp: 1628606438021.0002,
type: 'inbound-rtp',
codecId: 'RTCCodec_v4_Inbound_104',
kind: 'video',
mediaType: 'video',
ssrc: 1635808784,
transportId: 'RTCTransport_0_1',
packetsLost: 49,
packetsReceived: 5198,
bytesReceived: 5720775,
estimatedPlayoutTimestamp: 3837595237710,
firCount: 0,
frameHeight: 180,
frameWidth: 320,
framesDecoded: 516,
framesPerSecond: 17,
framesReceived: 518,
headerBytesReceived: 240747,
keyFramesDecoded: 38,
lastPacketReceivedTimestamp: 46062.656,
nackCount: 43,
pliCount: 3,
qpSum: 9966,
totalDecodeTime: 1.038,
totalInterFrameDelay: 60.22499999999982,
totalSquaredInterFrameDelay: 57.38087100000012,
trackId: 'RTCMediaStreamTrack_receiver_11',
},
Частота сбора: 1 сек
Средний объем конференции: 10 человек
Конференций на инстанс: 100
Хотим хранить: 30 дней
~2 Kbps
~20 Kbps
~1.95 Mbps
~ 0.6 TB
~168 Kbps
~1680 Kbps
~ 165 Mbps
~ 5.06 TB
Data channel
fetch
WebSocket
Beaсon API
GW
ClickHouse
GW
ClickHouse
GW
GW
k8s
Потоковый анализ
Анализ клиента
Анализ сессии
GW
ClickHouse
GW
GW
k8s
SA
SA
SA
SA
GW
ClickHouse
GW
GW
k8s
SA
SA
SA
SA
CA
GW
ClickHouse
GW
GW
k8s
SA
SA
SA
SA
CA
GA
interface StatsReport {
connection: ConnectionStatsReport;
outbound: Record<string, BaseOutboundStatsReport[]>;
inbound: Record<string, InboundStatsReport>;
}
//we can't use user-agent because
// 1. browsers often improve API,
// 2. user-agent is legacy
function detectStrategy(statsSample: RTCStatsReport): StatsStrategy {
//Browsers will check by global usage
// strategy for a chromium-based browsers
if (checkBlink(statsSample, IS_DEBUG)) {
return blinkStatsStrategy;
}
// strategy for a webkit-based browsers
if (checkWebkit(statsSample, IS_DEBUG)) {
return webkitStatsStrategy;
}
// strategy for a firefox-based browsers
if (checkGecko(statsSample, IS_DEBUG)) {
return geckoStatsStrategy;
}
// fallback rfc-like strategy
return baseStatsStrategy;
}
QualityIssueCodecMismatch
QualityIssueHighMediaLatency
QualityIssueICEDisconnected
Заменен на события CallEvents.Reconnectiong и CallEvents.Reconnected
QualityIssueLocalVideoDegradation
Для определения деградации видео теперь лучше всего использовать OutboundVideoStatsReport.qualityLimitationResolutionChanges если значение не четное, значит сейчас наблюдается ограничение качества исходящего видео. В поле OutboundVideoStatsReport.qualityLimitationReason будет находиться причина.
QualityIssuePacketLoss
function calculateBitrate(
bytesNow: number,
bytesBefore: number,
timestampNow: number,
timestampBefore: number
): number {
const deltaBytes = bytesNow - bytesBefore;
const deltaMs = timestampNow - timestampBefore;
return (deltaBytes / deltaMs) * 8000;
}
// Safari
let frameRate = 0;
if (prevReport && (prevReport as InboundVideoStatsReport).framesReceived) {
const lastReceived =
(prevReport as InboundVideoStatsReport).framesReceived || report.framesReceived;
const deltaFrames = report.framesReceived - lastReceived;
const deltaMs = report.timestamp - prevReport.timestamp;
frameRate = ((deltaFrames / deltaMs) * 1000) | 0;
}
Эта секция статистики описывает транспортную часть, ICE и текущий канал.
! Показывается именно текущее активное ICE соединение. Если WebRTC решит сменить пару кандидатов на ходу, или произойдет ICE restart, то данные изменятся и отсчет bytesSent и bytesReceived будет начат заново. Также обновится расчет rtt и availableOutgoingBitrate
interface ConnectionStatsReport {
timestamp: number;
remoteType: 'host' | 'srflx' | 'relay';
remoteIp?: string; // only Chromium + FF
remoteProtocol: 'udp' | 'tcp';
remotePort: number;
localType: 'host' | 'srflx' | 'relay';
localIp?: string; // only Chromium + FF
localProtocol: 'udp' | 'tcp';
localPort: number;
bytesSent: number;
bytesReceived: number;
availableOutgoingBitrate?: number; // only Chromium + Safari
rtt?: number; // only Chromium + Safari
currentRtt?: number; // only Chromium + Safari
}
interface OutboundAudioStatsReport extends BaseOutboundStatsReport {
kind: 'audio';
audioLevel?: number;
totalAudioEnergy?: number;
}
interface OutboundVideoStatsReport extends BaseOutboundStatsReport {
kind: 'video';
firCount: number;
pliCount: number;
nackCount: number;
width?: number;
baseWidth?: number;
height: number;
baseHeight?: number;
framesPerSecond?: number;
baseFramesPerSecond?: number;
qualityLimitationReason?: 'cpu' | 'bandwidth' | 'other' | 'none';
qualityLimitationResolutionChanges?: number;
qpSum?: number;
framesEncoded?: number;
keyFramesEncoded?: number;
}
interface BaseOutboundStatsReport {
timestamp: number;
bytesSent: number;
packetsSent: number;
rid?: string;
jitter: number;
rtt: number;
packetsLost: number;
loss: number;
codec?: string;
pt?: number;
totalRtt?: number;
bitrate: number;
}
Передается как объект, ключом которого является id media track, а значением - массив статистики для этого трека. Массив будет состоять либо из 1 элемента для аудио и видео без симулкаста, и из 2-3 элементов для видео с симулкастом.
interface InboundAudioStatsReport extends BasicInboundStatsReport {
audioLevel?: number;
totalAudioEnergy?: number;
totalSamplesReceived?: number;
totalSamplesDuration?: number;
insertedSamplesForDeceleration?: number;
silentConcealedSamples?: number;
}
interface InboundVideoStatsReport extends BasicInboundStatsReport {
height?: number;
width?: number;
framesReceived?: number;
framesDecoded?: number;
framesDropped?: number;
framesPerSecond?: number;
freezeCount?: number;
pauseCount?: number;
totalFramesDuration?: number;
totalFreezesDuration?: number;
totalPausesDuration?: number;
}
interface BasicInboundStatsReport {
timestamp: number;
kind: 'audio' | 'video';
bytesReceived: number;
packetsReceived: number;
packetsLost: number;
loss: number;
jitter: number;
bitrate: number;
endpoint?: string;
}
Передается как объект, ключом которого является id mediaRenderer для которого получается статистика, а значением - объект с данными статистики
Для видео конференций только сети уже не достаточно
Проблемой становится не только канал, но и CPU
Мы закладывались только на 1 поток видео, а не на 3 потока видео с симулкастом
Статистика не работала с Safari
adapter.js создавал боль