Как подружиться

со статистикой WebRTC

и сэкономить

тысячи часов на отладке

Привет, Мы

Игорь Шеко
Lead of Front-end team

Ирина Ламарр
Senior SDK developer

Что мы делаем

WebRTC Media Pipeline

Input devices

(camera, microphone)

Output devices

(screen, speaker)

Sender device

Web browser

Receiver device

Web browser

Network interface controller

Network interface controller

Sender

buffer

Receiver

buffer

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

Статистика WebRTC

Стандарт WebRTC

Статистика WebRTC

311 метрик

  • Сетевая статистика
  • Статистика энкодинга
  • Статистика декодинга
  • Качество потока
  • Синхронизация
  • Шифрование

Статистика WebRTC

311 метрик

204

188

101

88

Статистика WebRTC

Откуда мы об этом знаем?

https://wpt.fyi/results/webrtc-stats

  • Собирали только сеть
  • Делали это максимально часто
  • Собирали только статистику, которую поддерживают все браузеры
  • Генерировали специальные QualityIssue события

Наша первая статистика

Какие проблемы можно увидеть

Сетевые потери

packetsLost/(packetsReceived+packetsLost)

packetLost

packetLoss (%)

Stats graphs for RTCInboundRTPVideoStream  (inbound-rtp)

Сетевые потери

Что можно диагностировать

  • Плохое качество сетевого соединения
  • Важен именно % потерь:
  • до 5% - можно игнорировать
  • 5-10% - заметное для пользователя снижение качества
  • > 15%  - пикселизация/размытие видео, искажение голоса

Переполнение jitter buffer

Jitter - размер буффера в секунду

Stats graphs for RTCInboundRTPVideoStream  (inbound-rtp)

Переполнение jitter buffer

  • Если есть потери пакетов:
  • - теряем ключевые кадры

Что можно диагностировать

  • Без потери пакетов:
  • - происходит постоянный реордеринг 
  • Если проблема у единичных пользователей - предложить сменить сеть
  • Если проблема массовая - возможно проблемы на нашем сервере 
  • В итоге приводит к рассинхрону между участниками и сетевым задержка

Сетевые задержки

totalRoundTripTime - накопительная метрика, за все время соединение

roundTripTime - последний замер

Что можно диагностировать:
 

- можно понять проседал ли сигнал

- если jitter небольшой - проблема скорее всего серверная

Изменение битрейта

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 продолжительность.

Изменение битрейта

  • Видео:
  • - если есть сетевые потери - WebRTC решило снизить битрейт из-за недостаточной пропускной способности канала
  • - нет сетевых потерь - можно предположить, что девайс отправителя не справляется с нагрузкой

 

  • Аудио:
  • - можно отследить говорил человек или нет
  • при вкл FEC и тишине битрейт будет резко падать

Что можно диагностировать

Резкое падение битрейта аудио

  • - не декодируется аудио
  • - проблемы с аудио девайсами на стороне remote пользователя

Что можно диагностировать

Что нельзя было увидеть у нас, но можно по статистике

  • - низкое качество аудио/видео
  • - сетевые задержки

Что можно диагностировать

Смена ICE/DTLS и последствия

  • Как диагностировать:
  • - новая пара в candidate-pair
  • - часть статистики считается с нуля (bytesSent, bytesReceived, roundTripTime, totalRoundTripTime, availableOutgoingBitrate и др.)
  • Что и зачем?
  • - скорее всего был ice-restart
  • - может меняться DTLS и может согласоваться неправильно
  • - выбрано соединение с худшей пропускной способностью
  • - выбрана новая пара relay candidates

availableOutgoingBitrate

суммарный максимальный битрейт для отправки (по данным самого WebRTC)

Что можем диагностировать

- низкий битрейт / низкое качество аудио/видео

 

всего 193 Мбит/с

из них для WebRTC доступно лишь 3.2 Мбит/с

Video compression picture types

I-frame

P-frame

B-frame

I-frame

P-frame

I-frame

Согласованные кодеки

Statistics RTCInboundRTPVideoStream_2006534193

Скорость установки соединения

Как правильно считать?

  • Обычно считают от запроса пользователя на начало звонка до iceState connected
  • ! В реальности это не значит, что в этот момент удаленная сторона начала слышать/видеть данного участника
  • Необходимо считать до framesDecoded >= 1
  • если говорить про вход нового участника в конференцию, то от создания нового трансивера до framesDecoded >= 1

Frames && KeyFrames

Что можно диагностировать

  • framesReceived - сколько всего фреймов получили
  • framesDecoded - сколько смогли декодировать

 

  • - framesReceived есть, framesDecoded = 0 - видео не декодируется
  • - необходимо обратить внимание на keyFramesDecoded: скорее всего не получили I-frame
  • - или мы получили от сервера неправильно закодированные кадры

Frames && KeyFrames

Что можно диагностировать

  • - framesReceived есть, framesDecoded есть, keyFrameDecoded есть, но все полученные фреймы были отброшены (framesDropped)
  • - скорее всего забыли отрендерить видео элемент в DOM-дереве
  • - или девайс клиента не справляется по CPU

Большой объем I-frames

Что можно диагностировать

  • На стороне отправителя - keyFramesEncoded
  • На стороне получателя - keyFramesDecoded
  • + pliCount, firCount
  • - если pliCount/firCount равны 0, проверяем настройки сервера/кодеков

    - если pliCount/firCount растут, скорее всего что-то пошло не так на стороне получателя и он перезапрашивает ключевики

Реальный FPS

  • - изначальное качество видео от камеры
  • -  сравнить frameEncoded/s и frameSents/s,        если есть значительная разница - низкая пропускная способность сети

Что можно диагностировать

Реальный FPS

  •  Кодеки без SVС - frameEncoded/s равны framePerSecond вVideoSource -
  • - если нет, не справляется encoder и дропает
  • Кодеки с SVC - framePerSecond * на количество слоев
  • - если нет, значит какие-то слои не кодируются

Что можно диагностировать

Реальный FPS

  • - Кодек не позволяет кодировать в заданном битрейте
  • Проверить сколько времени тратится на кодирование одного фрейма:
  • delta totalEncodeTime / delta frameEncoded
  • сравнить с max временем для 60fps = 16ms
  • - Если время выше - не справляется девайс пользователя

Возможные причины:

Разрешение исходного трека больше разрешения отправляемого

  • qualityLimitationReason - причина ограничения битрейта
  • qualityLimitation ResolutionsChanges - сколько раз за соединение происходила смена qualityLimitationReason

 

  • - можем попробовать снизить frameRate
  • - или уменьшить количество видеопоток, которые декодируются

Что можно диагностировать

Что такое simulcast?

Media

Server

1080p

720p

360p

Переключение слоев симулкаста

  • По статистике RTCOutboundRTPVideoStream можно отследить наличие слоев и их отключение/подключение, т.ч. изменения сделанные WebRTC без нашего участия
  • В RTCInboundRTPVideoStream:

    frameWidth и frameHeight - изменение разрешения входящего видео (переключение отправляемых слоев на стороне сервера/удаленного участника)

Audio samples metrics

  • insertedSamplesForDeceleration -  сколько фреймов было вставлено, чтобы замедлить аудио
  • removedSamplesForAcceleration - сколько удалено, чтобы ускорить аудио
  • - скорее всего в данный момент рассинхрон аудио/видео

Что можно диагностировать

Не о формате, а о способе

Стратегия адаптивного сбора

//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Х

Статистика WebRTC

Вопросы 

Зачем сервис

Задачи

  • Сбор статистики для решения конкретных инцидентов

  • Мониторинг инцидентов при обновлениях сервиса

  • Мониторинг инцидентов при обновлении браузеров

  • Определение качества работы сети

  • Помощь в разработке

Что хочется разработчику

  • Красивые графики

  • Простые флаги и healthcheck

  • Подсветка типичных проблем и советы по решению

  • Законно

  • White label

Наша первая попытка

GW

ClickHouse

Проблемы

GW

ClickHouse

0.8 Gbps

1.1 TB / day

Не очень...

1.1 TB / day

Готовые сервисы

  • Callstats.io

  • TestRTC

  • не расширяется

  • написаны под конкретные кейсы

  • писали не мы

Анализ объема

  • 204 уникальных метрик в браузере
  • Метрики повторяются на каждый входящий и исходящий поток
  • ~ 32 на входящий аудио
  • ~ 41 на входящий видео поток
  • Исходящие потоки в симулкасте тоже в тройном размере
  • В конференции на 10 человек ~1700 метрик
  • У каждого участника
  • Итого всего ~17000 параметров со всех на замер

Анализ объема

  • Частота сбора: 1 сек

  • Средний объем конференции: 10 человек

  • Конференций на инстанс: 300

  • Хотим хранить:  30 дней

  • ~168 Kbps

  • ~1680 Kbps

  • ~ 165 Mbps

  • ~ 5.06 TB

Оптимизируем формат

  • Уменьшаем json

  • Меняем формат

  • Переход к дельтам

  • Два типа кадров

Уменьшаем json

  • certificate
  • codec
  • не активные candidate-pair
  • не активные *-candidate
  • peer-connection
  • в основном transport

Что выкинуть?

~ 600 метрик

Меняем формат

    {
      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',
    },

Минусы дельт

  • Важен порядок
  • Потерянный "кадр" портит статистику

Два типа кадров

  • Счетчик "кадров"
  • Ключевые "кадры" с полной статистикой раз в 60 секунд

Итоги оптимизации

  • Частота сбора: 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

Выбираем канал передачи

Data channel

  • Тот же канал, что и media
  • UDP
  • Механизм возобновления соединения
  • Готовые реализации
  • Нагружает тот же сервер, что и media
  • Нет гарантированной доставки или порядка
  • Нет сжатия

Выбираем канал передачи

fetch

  • Самый "простой" протокол - https
  • Доставка и порядок гарантированы
  • Сжатие из коробки
  • SSL хендшейки
  • Высокий приоритет доставки

Выбираем канал передачи

WebSocket

  • Почти как fetch, но лучше!
  • Нет затрат на лишние SSL хендшейки
  • Сложно балансировать
  • Множество активных коннектов

Выбираем канал передачи

Beacon API

  • fetch без ожидания доставки 
  • минимальный приоритет
  • легко балансировать на сервере

Масштабирование

GW

ClickHouse

Масштабирование

GW

ClickHouse

GW

GW

k8s

Направления анализа

  • Потоковый анализ

  • Анализ клиента

  • Анализ сессии

Направления анализа

Потоковый анализ

  • Сетевые потери
  • Переполнение jitter
  • Сетевые задержки
  • Изменение битрейта
  • Relay
  • Скорость кодирования /декодирования

Масштабирование

GW

ClickHouse

GW

GW

k8s

SA

SA

SA

SA

Направления анализа

Анализ клиента

  • ICE/DTLS рестарт
  • availableOutgoingBitrate
  • Скорость установки соединения
  • Отсутствие Iframes
  • Много IFrames
  • реальный FPS
  • разрешения видео

Масштабирование

GW

ClickHouse

GW

GW

k8s

SA

SA

SA

SA

CA

Направления анализа

Анализ сессии

  • Слои simulcast
  • Pассинхрон аудио

Масштабирование

GW

ClickHouse

GW

GW

k8s

SA

SA

SA

SA

CA

GA

Выводы

Вопросы 

deck

By Irina Lamarr