VK Звонки:
выходя за лимиты браузера

@bmsdave

Вадим Горбачев

команда VK calls

Статистика

звонков в день

400к

групповых звонков

13 лет

в день

Цели

  • Предоставить пользователям возможность звонить
  • Технически сильное решение в вебе
  • Групповые звонки
  • P.S. сохранить скролл

Оглавление

  • Теория
  • Архитектура
  • UX
  • Оптимизации на большое число участников

Введение

  • WebRTC
  • Топологии
  • Видео
  • Функциональность

WebRTC

  • захват видео/аудио
  • установка соединения
  • передача данных
Signalingdatachannel / video / audio

WebRTC


/*** HTML */



/*** JS */
const video = document.getElementById('video');
const button = document.getElementById('button');

function init() {
  navigator.mediaDevices
    .getUserMedia({audio: false, video: true})
    .then((stream) => { video.srcObject = stream; })
    .catch(console.error);
}

button.addEventListener('click', init);
								

Топологии
MESH

Топологии
SFU

Топологии
MCU

Видео

keyframedifftime

Видео

keyframedifftime

Видео

keyframedifftime

Функциональность. Общение

HUSuserAudioVideoScreen sharing

Функциональность. Общение

HUSuserAudioVideoScreen sharing

Функциональность. Общение

HUSuserAudioVideoScreen sharingWeb ApplicationGoogle

Функциональность
Нотификация

Входящий

DeclineAcceept

Исходящий

Call

Звонок по ссылке

https://vk.com/join/call/...

Функциональность
Удобство

Grid

Режим оратора

HUSuser

Свернутый звонок

Функциональность. Управления

Admin panelKickMuteAdminHraise a hand
  • Режим админа
  • Понятие руки
  • Бан
  • Mute

Функциональность
Быстро и качественно

  • Ноут не превращается в сковородку
  • Качественное аудио и видео

Архитектура звонка

ReactReduxSDKSignalingbackendHWebRTCAudio/VideoSignalingDatachannelOtherUIAvatarsRolesIncoming callPainting

Архитектура звонка

ReactReduxSDKSignalingbackendHWebRTCAudio/VideoSignalingDatachannelOtherUIAvatarsRolesIncoming callPainting

Архитектура звонка

ReactReduxSDKSignalingbackendHWebRTCAudio/VideoSignalingDatachannelOtherUIAvatarsRolesIncoming callPainting

Сложности WebRTC в групповых

  • Крэш при видео > 50
  • Нужно ли столько видео?
  • Может просто выводить постранично?
  • А что делать с аудио если нет стрима?

Too many WebMediaPlayers

[Intervention] Blocked attempt to create a WebMediaPlayer as there are too many WebMediaPlayers already in existence. See crbug.com/1144736#c27

  auto* delegate = GetWebMediaPlayerDelegate();

  // Prevent a frame from creating too many media players, as they are extremely
  // heavy objects and a common cause of browser memory leaks. See
  // crbug.com/1144736
  if (delegate->web_media_player_count() >= GetMaxWebMediaPlayers()) {
    blink::WebString message =
        "Blocked attempt to create a WebMediaPlayer as there are too many "
        "WebMediaPlayers already in existence. See crbug.com/1144736#c27";
    web_frame->GenerateInterventionReport("TooManyWebMediaPlayers", message);
    return nullptr;
  }
								

Too many WebMediaPlayers


// example 1
function onEnded(){
  this.currentSrc = null;
  this.src = "";
  this.srcObject = null;
  this.remove();
};

// example 2
mediaEl.remove();
mediaEl.srcObject = null;
								

Неизвестно когда объект WebMediaPlayer будет удален

audio-mix

  • Запретить включать больше N видео? 😞
  • Постраничный вывод? 😒
  • Слать не все потоки пользователей? 😟
  • А как услышать того, чей стрим не транслируется? 🤔
  • 🤔
  • Видео не масштабируется
  • Аудио? Масштабируется 💡
  • 🤔
  • Видео не масштабируется
  • Аудио? Масштабируется 💡

MESH

SFU

MCU

  • 🤔
  • Видео не масштабируется
  • Аудио? Масштабируется 💡

MCU

MCU audio mix

MCU audio mix

SFU video

Track on demand

video-on-demand-1

  React.useEffect(() => {
    if (onShow && onHide) {
      onShow(id);
      const observer = new IntersectionObserver(
        ([entry]) => {
          if (entry.isIntersecting) {
            onShow(id)
          } else {
            onHide(id)
          }
        },
        {
          root: null,
          rootMargin: '0px',
          threshold: 0,
        },
      );
      if (ref.current) { observer.observe(ref.current); }
      return () => observer.disconnect();
    }
  }, [ref]);
								

Track on demand

video-on-demand-1

  React.useEffect(() => {
    if (onShow && onHide) {
      onShow(id);
      const observer = new IntersectionObserver(
        ([entry]) => {
          if (entry.isIntersecting) {
            onShow(id)
          } else {
            onHide(id)
          }
        },
        {
          root: null,
          rootMargin: '0px',
          threshold: 0,
        },
      );
      if (ref.current) { observer.observe(ref.current); }
      return () => observer.disconnect();
    }
  }, [ref]);
								

quality on demand

video-on-demand-2

function getOratorLayout(
  participant: Participant,
  cardSize: number,
  cardPriority: number,
  mediaType: MediaType | null = null,
): ParticipantLayout {
  return {
    uid: participant.id,
    mediaType,
    fit: Fit.COVER,
    width: cardSize,
    height: cardSize,
  };
}
								

quality on demand

video-on-demand-2

function getOratorLayout(
  participant: Participant,
  cardSize: number,
  cardPriority: number,
  mediaType: MediaType | null = null,
): ParticipantLayout {
  return {
    uid: participant.id,
    mediaType,
    fit: Fit.COVER,
    width: cardSize,
    height: cardSize,
  };
}
								

quality on demand

video-on-demand-2

Придется ждать keyframe

quality on demand

video-on-demand-2

function getOratorLayout(
  participant: Participant,
  cardSize: number,
  cardPriority: number,
  mediaType: MediaType | null = null,
): ParticipantLayout {
  return {
    uid: participant.id,
    mediaType,
    fit: Fit.COVER,
    width: cardSize,
    height: cardSize,
    keyFrame: true,
  };
}
								

Архитектура
Промежуточные выводы

MCU audio mix

SFU track/quality/keyframe on demand

UX

  • Работа в плохой сети
  • Синхронизация сигналинга и стримов
  • Дороговизна перерисовки
  • Пошаговая анимация

Работа в плохой сети

  • Фриз видео
  • Лаги аудио
  • Реконнекты

Аудиопайплайн звонка ВКонтакте

  • Packet loss concealment
  • Forward error correction
  • Time stretching

🤔

packet loss
round trip

синхронизация

  • Индикация кто говорит

синхронизация


const dc = pc
  .createDataChannel("dc");
dc.send("some string");

otherPc.addEventListener(
  'datachannel',
  e => {
    const channel = e.channel;
    channel
      .onmessage = ev => {
        console.log(
          'received',
          ev.data
        );
    };
  }
);
								

Дорогие перерисовки
filter: blur(40px);

  • 😞 Вынести в отдельный composite layout? статья Сергея Чикуенка
  • Считать на клиенте в canvas? 😒
  • 😟 Считать на бэке и передавать уже с blur?
  • Уболтать дизайнеров и просто заменить на градиент 💡

Before

After

Дорогие перерисовки
Основной вид

Дорогие перерисовки
Основной вид

Дорогие перерисовки
Основной вид

Дорогие перерисовки Aнимация

Дорогие перерисовки Aнимация

UX

Выводы

  • Индикация плохой сети
  • Datachannel для синхронизации
  • Облегчение отрисовки
  • Минимизация анимаций

Безлимит участников

  • Intersection Observer
    • Видео можно запросить только после отрисовки
    • Пропускает элементы при быстром скролле
    • Срабатывает после отрисовки UI
    • Возникают утечки треков
  • Пересоздания <video>
  • Запрос keyframe может "не попасть" в готовый video тег
  • рисовать Nk карточек становится дорого
  • Дорого пробегать по массиву в Nk
  • Дорого загружать данные всех пользователей на клиент
  • Слишком много событий от пользователей

Безлимит участников

  • Отказаться от отрисовки всех участников
  • Ленивая подгрузка данных
  • Оптимизация работы с видео
  • Кэширование, предрасчеты
  • Группировка событий

video slots

video slots

  • WebMediaPlayers константа
  • Не тратится время на инициализацию
  • Есть "последний кадр" не затертых стримов

<video> slots

События

  • Допустим 1 участник = 1 событие в минуту
  • Вкл. видео/аудио, реконнект, плохая сеть, поднятая рука
  • Nk участников = 33 события в секунду
  • Или 1 событие в 30ms

Группировка событий

Группировка событий

Группировка событий

Группировка событий

Infinite Scroll

заменяем IntersectionObserver на расчет по пикселям

Data

  • список видимых карточек
  • список имен кто говорит
  • серверный поиск участников

Выводы

  • создание звонков больше напоминает gamedev нежели веб-разработку
  • экспериментируйте
  • webrtc расчитан на p2p, но с групповыми нужно будет повозиться
  • берите на клиенте только те данные которые показываете именно сейчас

Вопросы =)