Перейти к содержанию

Статистика без выдуманных измерений

Состояния peer помогают понять общий этап консультации, но не объясняют текущую нагрузку и выбранный маршрут. WebRTC предоставляет getStats: набор отчётов со счётчиками и временными отметками. В этом уроке добавим небольшую диагностическую панель и вычислим скорость отправки из разности двух выборок. Значения будут получаться из настоящего соединения при самостоятельном запуске, а не из заранее придуманного примера успешной связи.

Состояние lesson-11 сохраняет существующий peer и запускает периодический сбор статистики для него. Панель выводит оценку отправляемых килобит в секунду и тип выбранного локального кандидата. Адреса участников и полные SDP на страницу не попадают. При отсутствии достаточных данных интерфейс сообщает ожидание следующей выборки, а не рисует красивый нулевой график, который пользователь примет за измерение.

Отчёт как коллекция

getStats возвращает коллекцию записей с разными type и идентификаторами. Одни записи относятся к RTP, другие — к транспорту и кандидатам. Нельзя считать любую запись одинаковой строкой общей статистики. Мы выбираем outbound-rtp с числовым bytesSent, поскольку исследуем именно отправку собственного медиа. Для приёма понадобилась бы другая группа отчётов и другое объяснение.

Счётчик байтов накопительный. Если просто вывести его значение рядом с надписью «скорость», единицы окажутся неправильными. Нужно запомнить прежнее значение той же записи, вычислить прирост байтов и поделить на прошедшее время. Первая выборка поэтому ещё не предоставляет скорости: у неё нет предыдущей точки сравнения. Отсутствующее сравнение не должно подменяться вымышленной константой.

Ниже дан полный startStats из src/app.js. Карта previous хранит отдельную прежнюю выборку для каждого report ID. Флаг busy предотвращает наложение асинхронных опросов. Интервал и peer принадлежат активному состоянию; dropPeer удаляет таймер, а проверка идентичности объекта не позволяет позднему отчёту старой встречи изменить новую панель.

function startStats(pc){
 const previous=new Map();let busy=false;
 state.statsTimer=setInterval(async()=>{
  if(busy || pc!==state.pc)return;busy=true;
  try{const stats=await pc.getStats(),lines=[];
   for(const item of stats.values()){
    if(item.type==='outbound-rtp' && !item.isRemote && typeof item.bytesSent==='number'){
     const old=previous.get(item.id),dt=old?item.timestamp-old.timestamp:0;
     if(old && dt>0 && item.bytesSent>=old.bytes)lines.push(`${item.kind || item.mediaType}: ${((item.bytesSent-old.bytes)*8/dt).toFixed(1)} кбит/с`);
     previous.set(item.id,{timestamp:item.timestamp,bytes:item.bytesSent});
    }
    if(item.type==='transport' && item.selectedCandidatePairId){const pair=stats.get(item.selectedCandidatePairId),candidate=stats.get(pair?.localCandidateId);lines.push(`Путь: ${candidate?.candidateType || 'неизвестен'}`);}
   }
   if(pc===state.pc)$('stats').textContent=lines.join('\n') || 'Ожидаем следующую выборку';
  }catch(error){if(pc===state.pc)report(error);}finally{busy=false;}
 },2000);
}

Единицы вычисления

Отчёт timestamp выражает время в миллисекундах. Разность байтов умножается на восемь для перехода к битам. Деление на миллисекунды численно даёт килобиты в секунду при используемом десятичном обозначении. Так формула связывает счётчик и интервал; она не предполагает, что browser timer всегда выполняется ровно через две секунды. Фактическая разность timestamp важнее ожидаемой периодичности UI.

Если ID отчёта сменился, предыдущей точки нет и показатель снова начинается со следующего сравнения. Если счётчик уменьшился или интервал не положителен, код не выводит отрицательную скорость. Такое поведение полезно при новом sender, restart и смене объекта соединения. Простое вычитание «последнего общего байта» без идентификатора смешало бы независимые отчёты и дало бессмысленные значения.

После самостоятельного разрешённого запуска второй пригодный опрос должен вывести оценку скорости отправки для найденных медиаотчётов. Конкретных чисел здесь нет: камера, движение, codec, сеть и политика браузера влияют на результат. При авторстве соединение и stats не запускались. Поэтому нельзя ссылаться на эту панель как на проведённый сетевой замер или доказательство достаточной пропускной способности сервера.

Выбранный маршрут

Запись transport может содержать ID выбранной candidate pair. Мы находим пару по этому ID, затем её локального кандидата и выводим candidateType. Такой путь чтения точнее, чем выбирать первый попавшийся candidate из коллекции. В отчёте могут быть несколько известных, проверяемых или уже неиспользуемых кандидатов; наличие relay среди них не доказывает, что текущий поток идёт через TURN.

Значение relay у выбранного локального кандидата является полезным признаком ретрансляции в данной стороне соединения. Но отдельная строка панели не подтверждает надёжность всей TURN-инфраструктуры, срок credentials или пригодность других сетей. Также отчёты доступны в разной полноте. Если связанной записи нет, выводим «неизвестен», а не угадываем маршрут по конфигурации iceServers.

Для диагностики нет необходимости публиковать реальные IP-адреса или полный лог SDP. Вид кандидата и состояние могут объяснить сетевую ветвь, сохраняя меньший объём чувствительных сведений. Более подробный журнал требует собственной политики доступа и срока хранения. Тот факт, что API возвращает данные браузеру, не означает, что их следует сохранять во всех публичных отчётах приложения.

Чего не говорит bitrate

Высокая отправляемая скорость не равнозначна хорошему качеству разговора. Она может зависеть от активного изображения и выбранного формата. Низкая скорость также не доказывает плохую сеть: неподвижный экран иногда сжимается сильнее. Для оценки качества понадобятся другие показатели, например потерянные пакеты, jitter, частота кадров и ограничения отправителя, каждый со своей областью интерпретации.

Даже транспортные показатели не объясняют автоматически, слышит ли человек динамики или правильно ли направлена камера. Для пользовательского качества важны устройства и воспроизведение. Поэтому панель предназначена для исследования конкретной гипотезы, например действительно ли выбран relay или меняется ли счётчик отправки. Она не выдаёт общий рейтинг «качество 100%» из двух произвольных полей.

Стоимость наблюдения

Периодический сбор должен иметь ограниченный жизненный цикл. После завершения консультации старый interval не нужен и не должен обращаться к закрытому peer. Если оставить его активным, UI может выдавать поздние ошибки и удерживать ссылки. Флаг busy также не отменяет необходимость очистки: он лишь предотвращает параллельный опрос внутри одного действующего таймера.

Для более серьёзной панели можно сохранять небольшой ограниченный буфер выборок, показывать явно измеренный временной диапазон и помечать разрывы. При смене peer серия должна начинаться заново или иметь отдельный идентификатор. Нельзя соединять точки разных консультаций непрерывной линией без объяснения. Такие графики требуют фактических данных после разрешённого наблюдения, а не декоративных демонстрационных цифр.

Результат урока — корректная разность счётчиков и осторожное чтение выбранного пути. Для самостоятельного анализа объясните первую пустую выборку, смену ID и отсутствующую candidate pair. Все три состояния допустимы и не должны превращаться в выдуманные значения. Финальная глава завершит жизненный цикл комнаты и опишет границы подготовленной учебной поставки.

Оглавление серии · Исходные состояния проекта.

API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.