Дайджесты новостей
Иллюстрация бессерверной архитектуры стрима с детекцией смеха: покадровая синхронизация локального видео по WebSocket и анализ мимики MediaPipe в браузере.

Детектор смеха для Twitch: архитектура интерактивного стрима на MediaPipe и LiveKit

Как организовать интерактивное игровое шоу для стриминга, где участники соревнуются в выдержке и пытаются не рассмеяться при просмотре забавных роликов? Первая очевидная мысль любого разработчика — созвониться в Discord или Zoom, запустить демонстрацию экрана, захватить окно в OBS и посадить ведущего следить за улыбками игроков.

Однако на практике такой «лобовой» подход мгновенно разбивается о суровую реальность сетевых задержек. В стандартных сервисах видеоконференций джиттер и буферизация потоков составляют от сотен миллисекунд до пары секунд: один участник уже заливается хохотом, а второй еще даже не увидел кульминацию шутки. Если же попытаться пересылать потоки веб-камер на центральный сервер для распознавания эмоций нейросетью, аренда GPU-инстансов для параллельного декодирования и инференса нескольких потоков 1080p быстро пробьет брешь в любом бюджете.

Разработчик Егор спроектировал изящную распределенную бессерверную архитектуру (Edge Compute), где тяжелая обработка видео перенесена в браузеры игроков, а сетевой обмен сведен к микросекундным синхроимпульсам.

Магия без видеопотока: синхронизация видео до кадра

Главный архитектурный прорыв проекта — отказ от трансляции видеороликов через WebRTC. Зачем гонять тяжелый медиапоток по сети с неизбежными потерями качества и задержками, если файл можно скачать заранее?

Все ролики раунда предварительно кэшируются на клиентских устройствах игроков (pre-download). В момент старта сервер отправляет по протоколу WebSocket только временную метку «нулевой секунды». Чтобы видео воспроизводилось строго синхронно у игроков из разных городов и стран, клиенты непрерывно сверяют свои часы с сервером по модифицированному алгоритму NTP:

// client-sync.js: Расчет смещения часов и подстройка скорости видео
class StreamClockSync {
  constructor(socket) {
    this.socket = socket;
    this.clockOffset = 0;
  }

  pingLoop() {
    setInterval(() => {
      const sendTime = performance.now();
      this.socket.send(JSON.stringify({ type: "ping", sendTime }));
    }, 2500);

    this.socket.on("message", (event) => {
      const data = JSON.parse(event);
      if (data.type === "pong") {
        const rtt = performance.now() - data.sendTime;
        // Расчет смещения локальных часов относительно сервера с поправкой на половину RTT
        const estimatedServerTime = data.serverTime + rtt / 2;
        this.clockOffset = estimatedServerTime - Date.now();
      }
    });
  }

  syncVideoFrame(videoEl, startTimestamp) {
    const currentServerTime = Date.now() + this.clockOffset;
    const targetPlaybackSec = Math.max(0, (currentServerTime - startTimestamp) / 1000);
    const drift = videoEl.currentTime - targetPlaybackSec;

    // Мягкая микро-подстройка скорости без рывков звука при расхождении больше 1 кадра (16 мс)
    if (Math.abs(drift) > 0.016 && Math.abs(drift) < 0.3) {
      videoEl.playbackRate = drift > 0 ? 0.98 : 1.02;
    } else if (Math.abs(drift) >= 0.3) {
      videoEl.currentTime = targetPlaybackSec;
      videoEl.playbackRate = 1.0;
    } else {
      videoEl.playbackRate = 1.0;
    }
  }
}

Благодаря адаптивной коррекции скорости (playbackRate 0.98x–1.02x) звук не искажается, а расхождение кадров между игроками держится в пределах менее 16 миллисекунд (один кадр при 60 FPS).

52 мышцы лица в браузере: клиентский инференс MediaPipe

Вторая проблема — детекция смеха. Передавать видеокамеры участников на сервер для обработки — непозволительная роскошь. Автор перенес инференс нейросети непосредственно в браузер каждого игрока с помощью библиотеки Google MediaPipe Tasks Vision.

Модель FaceLandmarker скомпилирована в WebAssembly и использует аппаратное ускорение WebGL на видеокарте пользователя. Она отслеживает не просто контур лица, а вычисляет 52 коэффициента активности мимических мышц (стандарт blendshapes от Apple ARKit):

// face-detector.js: Локальное извлечение признаков улыбки
import { FaceLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";

export async function createFaceAnalyzer(videoElement, onScoreUpdate) {
  const resolver = await FilesetResolver.forVisionTasks(
    "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
  );
  
  const landmarker = await FaceLandmarker.createFromOptions(resolver, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
      delegate: "GPU",
    },
    outputFaceBlendshapes: true,
    runningMode: "VIDEO",
    numFaces: 1,
  });

  function processFrame() {
    if (videoElement.readyState >= 2) {
      const results = landmarker.detectForVideo(videoElement, performance.now());
      if (results.faceBlendshapes && results.faceBlendshapes.length > 0) {
        const shapes = results.faceBlendshapes[0].categories;
        const smileL = shapes.find((s) => s.categoryName === "mouthSmileLeft")?.score || 0;
        const smileR = shapes.find((s) => s.categoryName === "mouthSmileRight")?.score || 0;
        const jawOpen = shapes.find((s) => s.categoryName === "jawOpen")?.score || 0;

        // Расчет интенсивности с фильтрацией открывания рта при разговоре
        const smileScore = (smileL + smileR) / 2;
        onScoreUpdate({ smileScore, jawOpen });
      }
    }
    requestAnimationFrame(processFrame);
  }

  processFrame();
}

Клиент отправляет на центральный сервер не видеопоток, а крошечные JSON-сообщения с числовыми коэффициентами несколько раз в секунду. Обычный одноядерный сервер на Node.js легко обслуживает десятки таких сессий одновременно.

Автоматический режиссер и OBS

Для живого общения между игроками используется открытый WebRTC SFU сервер LiveKit, обеспечивающий сверхнизкую задержку аудио и видео. При этом вся сцена шоу (веб-камеры, счетчики штрафных очков, шкалы улыбок и чат) собрана как веб-приложение на React.

В OBS сцена подключается одной ссылкой через Browser Source. Алгоритм «виртуального режиссера» автоматически монтирует кадр для вертикальных трансляций (TikTok, Reels): камера переключается либо на говорящего участника (по детектору активности микрофона VAD), либо на игрока, чей индекс улыбки приблизился к критическому порогу и вот-вот сорвется в смех.

Практический вердикт

Этот кейс — блестящая иллюстрация того, как архитектурная смекалка побеждает грубую силу облачных ресурсов. Перенос инференса на клиентские устройства и покадровая синхронизация по легковесному протоколу превратили потенциально дорогой проект в доступную систему с минимальной стоимостью владения.