Как организовать интерактивное игровое шоу для стриминга, где участники соревнуются в выдержке и пытаются не рассмеяться при просмотре забавных роликов? Первая очевидная мысль любого разработчика — созвониться в 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), либо на игрока, чей индекс улыбки приблизился к критическому порогу и вот-вот сорвется в смех.
Практический вердикт
Этот кейс — блестящая иллюстрация того, как архитектурная смекалка побеждает грубую силу облачных ресурсов. Перенос инференса на клиентские устройства и покадровая синхронизация по легковесному протоколу превратили потенциально дорогой проект в доступную систему с минимальной стоимостью владения.
