
В октябре прошлого года на Хабре вышла статья «Как я писал свою звонилку для видеозвонков» — автор под ником CEOCTO за несколько вечеров написал P2P-видеозвонилку на WebRTC, потому что его достали обрывающиеся созвоны в мессенджерах. Я прочитал, улыбнулся, полез смотреть исходники на GitHub — и на этом всё. Как обычно бывает с чужими pet-проектами: оценил, закрыл вкладку, забыл.
Я живу в Сербии четвёртый год, здесь у меня свой бизнес. Про ту статью вспомнил, когда понял, что не могу нормально дозвониться маме в Россию. Дело не в моём интернете и не в деньгах — часть привычных мессенджеров там просто заблокирована, а держать впн ради звонка сыну для человека, который не по этой части, задача не из простых: то отвалится в середине разговора, то придётся заново вспоминать, как его включить. Технически звонок возможен. Практически — каждый раз квест.
Jopa Call как учебный проект — и почему этого мало для звонка маме
CEOCTO в своей статье сразу оговаривается: JOPA Call — учебный проект, а не продакшен-сервис. Это видно по архитектуре: P2P на WebRTC, сигнальный сервер на Go, STUN/TURN для обхода NAT — ровно тот набор, которого хватает, чтобы за несколько вечеров с ChatGPT и Gemini получить рабочий видеозвонок между двумя Android-телефонами. Уже само по себе достижение для проекта такого масштаба.
Чтобы звонок так же просто работал у обычного, нетехнического человека на другом конце, этого мало по трём причинам. Android-only — а у мамы, как и у половины моих контактов, может быть iPhone. Сквозное шифрование — сам автор пишет, что оно в планах, а не в релизе. И главное — «за несколько вечеров» означает отсутствие тестов, обработки деградации сети и всего того скучного, что отличает pet-проект от сервиса, которому должен без объяснений поверить нетехнический человек за тысячи километров.
Что я знал такого, чего не было у автора Jopa Call
Опыт из телекома тоже не прошёл бесследно. Довольно долго я писал iOS-часть похожего сервиса изнутри коммерческой компании — видел, какие грабли обычно всплывают не в демо на два устройства, а в проде: обрыв сети посреди звонка, VoIP-push, который обязан синхронно поднять экран входящего звонка до истечения таймаута (иначе Apple банит приложение), деградация звука на слабом канале. Это и определило выбор технологии: Kotlin Multiplatform — весь продуктовый код общий для Android и iOS, платформенные модули — только тонкие адаптеры системных API (CallKit/PushKit на iOS, ConnectionService на Android). Не два отдельных приложения, а один продукт с двумя витринами.
Из этого же бэкграунда выросло решение, которое действительно нравится: групповые аудиозвонки на mesh-топологии — до 8 участников без выделенного медиасервера (SFU). Обычно групповые звонки такого масштаба тянут за собой инфраструктуру транскодирования и маршрутизации потоков; у нас каждый узел рассылает трафик сам, а нетривиальная часть — договориться, кто кому шлёт офер при подключении нового участника, и не перепутать это с таймаутами приглашений. Этой механике тоже достанется отдельная статья в цикле.

Коммерческие рельсы вместо вечера с ChatGPT
Тут стоит сказать честно: весь код в проекте написан ИИ. Но не в духе «вечер с ChatGPT», а по процессу, который я выстраивал как для коммерческой разработки. Каждая фича начинается с discovery-пакета: спецификация, критерии приёмки, разбивка на тикеты — и только потом код. TDD в буквальном смысле: сначала падающий тест отдельным коммитом, потом реализация, потом рефакторинг. Без критерия приёмки агент не имеет права придумывать поведение сам.
Ревью тоже не человеческое в привычном смысле — я использовал Claude Code и Codex как независимых ревьюеров друг для друга: то, что написал один harness, критикует и проверяет другой, с другой моделью под капотом. Процент автономности высокий — процентов 80 работы прошло через Ralph Loop, агентный цикл, который сам берёт следующую задачу из очереди, пишет тест, чинит, коммитит и переходит к следующей. Но не обошлось и без бебиситтинга: где-то агент упирался в архитектурную развилку, где-то зацикливался на неверной гипотезе, и приходилось вмешиваться руками. На всё это ушёл месяц и, если честно, я не берусь точно посчитать, сколько токенов — счёт шёл на миллионы.

Приватность как архитектурное ограничение, а не обещание
Приватность в проекте — это не строчка в политике, а следствие схемы данных. Сервер физически не может отдать то, чего не хранит: только identity (12-значный ID и Ed25519-ключ, сгенерированные на устройстве), связи между контактами по факту обмена QR-кодом и токены для пуш-уведомлений. Имена контактов, история звонков, содержимое медиа — только на устройстве, сам звонок P2P идёт по DTLS-SRTP напрямую. Удаления — физические, никаких soft-delete с меткой «удалено», которая формально скрыта, а по факту жива в бэкапе. Сигнальные данные, по которым в принципе можно восстановить детали звонка (token, sdp, candidate), не попадают ни в логи, ни в Sentry — это отдельно проверяется тестами на редакцию.
Для меня это не абстрактный принцип «мы за приватность», а прямое следствие изначальной задачи: сервис не должен становиться точкой, из которой можно узнать, кто кому и когда звонил. Так остаётся меньше того, что вообще можно спросить — ни у меня, ни у пользователя сервиса. Да и теоретически не должно быть вопросов по пользовательским данным у “товарищей”, их просто нет.

Что дальше
Сейчас в проекте не хватает одной вещи — устойчивого TURN-релея для российских сетей, где прямое P2P-соединение не проходит: провайдерский NAT, корпоративные файрволы, ограничения операторов. В ближайших планах — TURN-серверы в белых списках специально для звонков в Россию и вообще на ее территории, чтобы соединение устанавливалось даже в сетях, где всё остальное — квест. Пока это roadmap, не готовая фича. TURN конечно есть в проекте, но пока он не привязан к гео и находится в Германии (нормально доступен из РФ).
Сам сервис можно посмотреть на id-caller.online/ru — не буду превращать статью в рекламный лендинг, ссылка просто для тех, кому интересно попробовать или взглянуть на техническую сторону вблизи.
О чём будет этот цикл
Список тем, которые я планирую разобрать, длинный — вот основные блоки.
Расследования и баги — самый читаемый жанр малой формы: почему видео на iOS зависало на протяжении месяца (спойлер: use-after-scope в чужой библиотеке), как сторож медиастатистики сам гасил камеру на 8-й секунде звонка, и как юнит-тесты остаются зелёными, пока фича не работает вовсе — потому что транспортный слой физически не был подключён к потребителю.
Тестирование — livelock из-за бесконечных delay-циклов в корутинах, зачем писать тесты для того, что не исполняется (CI-конфиги, docker-compose, метаданные сторов), и как автоматизировать полевой стенд из четырёх Android и iPhone без единого касания экрана.
Архитектура — уже упомянутый mesh для групповых звонков без SFU, протокол как source of truth между Kotlin- и Go-частями, звонки без номеров телефона и аккаунтов.
Платформенная специфика — правила iOS, за нарушение которых бывает больно (VoIP-push обязан синхронно поднять экран звонка до истечения таймаута), и почему Android-вендоры воюют с фоновыми звонками.
Инфраструктура и релиз — как получить TLS-сертификат для coturn без отдельного certbot, и как не сойти с ума, отправляя звонилку в Google Play.
Процесс — что должно лежать в репозитории, чтобы AI-агент разрабатывал фичи по спецификации, а не просто генерировал код.
Начну с истории про зависание видео на iOS — самого упорного бага за весь проект, из-за которого пришлось форкать чужую библиотеку.
Если у вас есть свои грабли на похожую тему или вопросы, на которые хотелось бы отдельного разбора — пишите в комментариях: соберу самое интересное и учту при выборе порядка публикаций.

