
Есть программирование, о котором пишут книги, снимают курсы и спорят в комментариях. А есть программирование, о котором молчат, потому что его почти никто не делал. Потоковая обработка данных в реальном времени из второй категории: видео из браузера в браузер, живой голос по телефону, поток с датчиков, который нельзя поставить на паузу. Здесь не бывает «обновите страницу и попробуйте ещё раз»: человек уже сказал фразу, кадр уже улетел, пульс уже ударил.
В этом кейсе мы расскажем, что такое потоковая обработка простыми словами, почему это одна из самых сложных и непопулярных задач в разработке, как устроен пиринговый интернет с его NAT, STUN и TURN, и как мы всё это прошли на реальных проектах. Спойлер: местами было больно, но проекты сделаны и работают =)
Бесплатная консультация с архитектором и расчёт цены после неё. Если у вас задача со стримингом, видеосвязью, голосом или датчиками, разберём её на созвоне и пришлём расчёт сроков и стоимости по этапам. Записаться на консультацию
Что такое потоковая обработка: река, а не озеро
Большинство бизнес-программ работает с данными как с озером. Заказы, платежи и отчёты сначала накапливаются в базе, а потом их обрабатывают: раз в час, раз в сутки, по кнопке. Это называется пакетной (батчевой) обработкой. Если ночной отчёт посчитался на пять минут дольше, никто даже не заметит.
Потоковая обработка работает с данными как с рекой. Данные текут непрерывно, мелкими порциями: кадр видео, пакет звука длиной 20 миллисекунд, отсчёт датчика. Каждую порцию нужно принять, проверить, обработать и отдать дальше, пока не пришла следующая. Опоздала обработка, и порция уже никому не нужна: кадр, показанный через секунду после своего времени, это не видео, а слайд-шоу, а ответ голосового агента через пять секунд воспринимается как зависание.
По дороге поток подстерегают пять врагов, и с каждым мы знакомы лично:
- Задержка. На весь путь от микрофона до уха собеседника есть бюджет в несколько сотен миллисекунд. Его делят между захватом, сетью, обработкой и воспроизведением, и каждый участок норовит откусить побольше.
- Джиттер. Пакеты приходят неравномерно: то пачкой, то с паузой. Если проигрывать их как пришли, звук булькает и заикается. Нужен джиттер-буфер, который чуть придерживает поток и выдаёт его ровно.
- Потери. В живом видео и голосе используется UDP, а он потерянные пакеты не переспрашивает. И правильно: потерянный кадр через секунду уже не нужен. Значит, система должна уметь жить с дырками.
- Порядок. Пакеты обгоняют друг друга в сети, и их нужно сложить обратно по номерам и меткам времени. Иначе собеседник скажет «нет, да» вместо «да, нет».
- Backpressure (обратное давление). Если обработка не успевает за потоком, данные копятся в очередях, задержка растёт, и через минуту разговор идёт с отставанием в десять секунд. Правильный поток умеет вовремя выбросить лишнее, а не копить его.
Почему это сложнейшая и непопулярная задача
Обычный веб живёт по принципу «запрос и ответ». Браузер спросил, сервер ответил, всё. Ошибку можно воспроизвести, поставить точку останова, посмотреть переменные. В потоке так не получится: пока вы стоите на точке останова, поток утёк, соединение отвалилось по таймауту, а собеседник положил трубку. Отлаживать приходится на живом потоке, по логам и метрикам, иногда покадрово.
- Сеть ведёт себя непредсказуемо. У одного пользователя домашний Wi-Fi, у другого мобильный интернет в лифте, у третьего офисный файрвол, который пропускает только веб-страницы. И всё это должно работать одинаково.
- Ошибки плавающие. «У меня всё работает» здесь звучит особенно жалко: баг проявляется только у конкретного оператора, только на конкретном телефоне, только при плохом сигнале.
- Стек огромный. Кодеки, частоты дискретизации, протоколы RTP и SIP, WebRTC, WebSocket, NAT, сертификаты, браузерные аудио-API. Каждая тема тянет на отдельную книгу, и книги эти в основном на английском и в основном для телеком-инженеров.
- Специалистов почти нет. Людей, которые реально запускали стриминг в продакшене, единицы, и они обычно сидят в крупных компаниях. На рынке услуг их практически не найти даже за хорошие деньги.
Сегодня часть ответов можно спросить у языковой модели, и она даже что-то подскажет. Но большая часть нашего опыта в реальном времени набиралась тогда, когда спросить было не у кого. Документация WebRTC описывала, как должно быть, форумы описывали, как у кого-то не работает, а между этим была пропасть, которую приходилось перепрыгивать самим. Мы искали консультанта по WebRTC за деньги и честно не нашли: те, кто умел, были заняты, а те, кто был свободен, знали тему по той же документации, что и мы. Поэтому всё, о чём дальше пойдёт речь, мы прошли руками: экспериментами, логами и бессонными ночами с консолью браузера.
Пиринговый интернет: почему два компьютера не могут просто поговорить
Кажется, что интернет это сеть, где любой компьютер может связаться с любым другим. В теории так и задумывалось. На практике адресов IPv4 на всех не хватило, и почти каждое устройство сегодня сидит за роутером, который прячет его за одним общим адресом. Это называется NAT (трансляция сетевых адресов).

NAT: охранник на входе
Роутер работает как охранник в бизнес-центре. Изнутри наружу вы выходите свободно, он запоминает, кто куда пошёл, и пропускает ответ. А вот снаружи к вам без приглашения не зайти: незнакомый входящий пакет охранник просто выбрасывает. Для сайтов это отлично, вы всегда «звоните первым». Для видеозвонка из браузера в браузер это катастрофа: оба собеседника сидят за своими охранниками, и каждый ждёт, что первым позвонит другой.
UDP против TCP
TCP это заказное письмо: доставка гарантирована, порядок гарантирован, потерянное пересылается. Для страниц и файлов идеально. Для живого звука плохо: пока TCP переспрашивает потерянный пакет, вся очередь за ним стоит, и звук замирает. UDP это открытки: отправил и забыл. Что-то потеряется, зато ничего не ждёт. Поэтому видео и голос в реальном времени живут на UDP, а с потерями разбираются кодеки и джиттер-буфер.
STUN, hole punching и ICE
Чтобы два браузера соединились напрямую, им нужно три вещи. Первая: сигнальный сервер, обычно на WebSocket, через который они обмениваются «визитками» (offer, answer и кандидатами адресов). Сам видеопоток через него не идёт. Вторая: STUN-сервер, который отвечает на простой вопрос «как меня видно снаружи», то есть сообщает внешний адрес и порт. Третья: hole punching, «пробивание дыры». Оба браузера одновременно отправляют друг другу UDP-пакеты, каждый охранник думает, что его жилец «позвонил первым», и пропускает ответ.
Всем этим дирижирует протокол ICE. Он собирает кандидатов (локальный адрес, внешний адрес от STUN, адрес ретранслятора) и перебирает их, пока какой-нибудь не заработает.
TURN, симметричный NAT и мобильные сети
Теперь плохие новости. Есть симметричный NAT, который для каждого нового адресата выдаёт новый внешний порт, и подсказка STUN становится бесполезной. Есть CGNAT у мобильных операторов: NAT за NAT, когда тысячи абонентов сидят за одним адресом. Есть корпоративные сети, где UDP запрещён совсем. Во всех этих случаях напрямую не пробиться, и на сцену выходит TURN: ретранслятор, который принимает поток от одного собеседника и пересылает другому. Работает почти всегда, в том числе по TLS на 443 порту, который выглядит для файрвола как обычный сайт. Но весь трафик идёт через ваш сервер, и вы за него платите.
Хорошая новость: всё это можно настроить так, что пользователь ничего не заметит. Плохая: чтобы настроить, нужно пройти через каждый из этих случаев на живых людях.
Опыт 1: стриминг в браузере для премиальной социальной платформы
Нашим первым большим проектом в реальном времени стала премиальная социальная платформа с видеочатом и стримингом прямо в браузере. Пользователь включает камеру и ведёт трансляцию, зрители смотрят её без установки чего-либо, а время в чате и на стриме оплачивается внутренними кредитами. То есть каждая секунда видео здесь ещё и деньги: биллинг должен точно знать, когда поток начался и когда оборвался.
Схема «каждый с каждым» (mesh) для стрима не годится: если у стримера сто зрителей, его браузеру пришлось бы отправлять сто копий видео, и ноутбук взлетел бы. Поэтому мы написали собственный SFU (Selective Forwarding Unit, сервер выборочной пересылки) на Node.js. Стример отправляет один поток на сервер, сервер раздаёт его неограниченному числу зрителей, а браузер стримера держит ровно одно соединение.
- Сигнализация на WebSocket. Обмен offer, answer и ICE-кандидатами между браузером и сервером, роли стримера и зрителя, автоматическое переподключение при обрыве.
- Свой STUN/TURN на coturn. С TLS-сертификатом, отдельными портами и логами. Без него часть зрителей с мобильного интернета просто видела чёрный экран.
- Жизненный цикл соединений. Стример ушёл, и все его зрители корректно отключаются; зритель пропал из сети, и сервер освобождает его ресурсы, не дожидаясь вежливого прощания.
- Чат на WebSocket рядом с видео, под присмотром супервизора, который поднимает демона после падения.
- Мобильные оболочки для Android и iOS и запись сториз прямо с камеры устройства.
- Покадровая диагностика. Мы логировали каждый кадр (номер, размер, время) и состояние ICE-соединения раз в секунду. Звучит как паранойя, но именно так ловятся баги, которые случаются только у одного зрителя из двадцати.
Отдельная история: смена камеры на лету. Пользователь подключил внешнюю камеру или выдернул её посреди эфира, браузер шлёт событие, и поток нужно пересобрать, не обрывая трансляцию. В документации это одна строчка, в жизни это неделя экспериментов. Позже в других проектах мы пробовали и готовый медиасервер Janus, но собственный SFU дал главное: мы понимаем каждую строчку пути пакета и можем починить его сами.
Опыт 2: голос и телефония в реальном времени
Второе большое направление это голос. Наш голосовой ИИ-секретарь отвечает на обычные телефонные звонки, понимает собеседника и отвечает живым голосом. Снаружи это просто звонок. Внутри это целый набор модулей и сборок, которые работают с потоком звука в реальном времени. Подробности самого Секретаря у нас в отдельном кейсе, а здесь расскажем, какие потоковые задачи пришлось решить.

- Телефония. Звонок приходит по SIP в АТС Asterisk, звук идёт по RTP кадрами по 20 миллисекунд в телефонном кодеке 8 кГц. Сервер на Node.js забирает поток, декодирует, пересэмплирует под нужды распознавания и отдаёт обратно синтезированную речь с тем же ритмом, кадр за кадром.
- Задержка ответа. Первые версии отвечали через несколько секунд, а в телефоне это вечность: человек успевает сказать «алло, вы тут?». Мы разобрали бюджет задержки по участкам, перевели распознавание, генерацию и озвучку в потоковый режим (озвучиваем первую фразу, пока пишется вторая), уменьшили буферы и довели паузу на живом звонке до уровня меньше секунды.
- Эхо. На мобильной линии секретарь слышал сам себя и вежливо отвечал сам себе. Пришлось учиться отличать собственное эхо от настоящей речи собеседника.
- Перебивание (barge-in). Живой человек перебивает, и агент должен замолчать за доли секунды, а потом понять, это было «угу» или новая мысль.
- Конец реплики. Как понять, что человек договорил, а не просто задумался? Мы сравнили несколько подходов на полутора часах реальных записей и выбрали лучший баланс между «перебивает» и «тормозит».
- Капризы RTP. Например, адрес для отправки звука обратно в звонок становится известен только из первого входящего пакета. Если не начать сразу проигрывать тишину, абонент секретаря просто не услышит. Такие вещи не пишут в учебниках, их находят в логах.
- Порядок и зависания. Один зависший кусок синтеза речи мог остановить весь разговор. Теперь куски идут строго по порядку, медленные запросы дублируются запасными, а обрыв WebSocket к телефонии восстанавливается автоматически.
- Переключение без разрыва. Звонок можно перенести между версиями голосового контура прямо во время разговора, и звонящий этого не заметит.
Есть и браузерные сборки: микрофон через getUserMedia с шумоподавлением и эхоподавлением, воспроизведение через AudioWorklet с собственным джиттер-буфером, полнодуплексный режим на WebRTC. А про синтез и распознавание речи как отдельный сервис можно почитать в кейсе NaturalTTS.
Опыт 3: датчики, биометрия и мониторинг
Поток это не только видео и голос. Первый график температуры и влажности в реальном времени мы собрали ещё в 2019 году: датчик на Arduino, последовательный порт, Python и поток событий прямо в браузер. Позже это выросло в IoT-контур на дешёвом железе с автономной записью в память датчика, когда компьютер выключен, и досылкой без дублей при подключении.
Самый плотный поток у нас в проекте ЩИТ: носимое устройство на ESP32 непрерывно снимает биометрию и отправляет её на сервер пачками раз в секунду. Связь пропала, и прибор копит данные на карту памяти, а потом досылает с того места, где остановился. Формат записи мы сделали примерно в десять раз компактнее JSON, а сервер проверили нагрузочным тестом на сотне виртуальных устройств. Панель в браузере рисует графики вживую и перерисовывает только то, что изменилось.

Что это даёт вам
Мы не продаём «стриминг из коробки». Мы приносим опыт, который трудно купить: знаем, где поток ломается, и закладываем это в архитектуру с первого дня, а не находим на запуске.
- Видеосвязь и трансляции в браузере без установки приложений: консультации, обучение, продажи по видео, платные эфиры с биллингом по времени.
- Голосовые агенты на обычной телефонии: приём звонков, обзвоны, запись и расшифровка разговоров, интеграция с CRM.
- Мониторинг датчиков и оборудования в реальном времени: от климата на складе до носимых устройств, с буферизацией при обрывах связи.
- Своя инфраструктура: STUN/TURN, медиасерверы, WebSocket-шлюзы на ваших серверах, без зависимости от чужого сервиса и его тарифа за минуту.
- Честная оценка: где хватит готового сервиса, а где нужна своя разработка. Иногда правильный ответ «не пишите свой SFU», и мы так и скажем.
Обсудим ваш поток
Если у вас есть данные, которые текут, а не лежат, будь то видео, голос, звонки или датчики, давайте поговорим. Разберём задачу, покажем риски и пришлём расчёт цены и сроков по этапам после бесплатной консультации. Оставить заявку на systems.ingello.com