tg-streaming-bot: киновечера в голосовом чате Telegram
DraftСобственный Telegram-бот, который стримит фильмы, музыку, интернет-радио и 50 000+ каналов живого ТВ прямо в групповой голосовой чат — чтобы друзья из разных городов смотрели одно и то же, синхронно, хоть с телефона. Плюс боевые истории: почему бот в принципе не может сделать это в одиночку, стриминг YouTube по принципу «скачай или ничего» и пауза, которая сама себя снимала.
Часть моих друзей теперь живёт в других городах. Старый ритуал — собраться на одном диване, включить фильм, комментировать его — не переживает расстояния. Инструменты демонстрации экрана хотят, чтобы все были в одном приложении с хорошей скоростью отдачи; они захлёбываются на 1080p и умирают, как только кто-то сидит с мобильного интернета. А что у всех, кого я знаю, и так открыто весь день на каждом устройстве — это Telegram.
Групповые звонки в Telegram умеют передавать видео. Не только веб-камеру, а настоящую трансляцию в звонок, которую все участники видят одновременно. Так что идея была маленькой и упрямой: направить бота в групповой голосовой чат, сказать ему включить фильм — и пусть четыре человека из трёх городов смотрят одни и те же кадры в один и тот же момент с того устройства, что под рукой.
Результат — tg-streaming-bot, self-hosted бот, который стримит музыку и видео в групповые голосовые чаты Telegram. Фильмы, YouTube, файл, который кто-то кинул в чат, интернет-радио или живое ТВ из каталога на 50 000 каналов — всё это попадает в звонок, а полное управление (пауза / перемотка / громкость / пропуск) висит inline-кнопками, общими для всех в комнате.
Если вы видели мой более ранний tg-mpv-bot — это та же идея, направленная на другой экран. tg-mpv-bot — это Telegram-пульт для телевизора в моей гостиной: он управляет mpv, подключённым по HDMI, так что воспроизведение попадает на один физический экран в одной комнате. tg-streaming-bot сохраняет ощущение «Telegram как пульт», но меняет вывод: вместо стриминга через mpv на мой телевизор он стримит в групповой видеочат Telegram — так что «экраном» становится телефон каждого сразу, где бы человек ни был. Та же идея — управлять медиа из чата, — только ради просмотра вместе, а не дома.
Что он умеет
Повседневных действий немного, а вот список источников — нет:
/vplay <запрос | URL>— стрим фильма или клипа в звонок. YouTube (поиск или ссылка), видеофайл из чата, Rutube / Vimeo / любой сайт, который знает yt-dlp, или живой.m3u8. Добавьте720/480/360, чтобы ограничить качество,mute— чтобы начать без звука./play,/search— то же для музыки: включить первый результат или выбрать из списка выдачи YouTube (🎵 аудио / 🎬 видео)./radio— десятки станций интернет-радио, с текущим треком на карточке./iptv <название>— живое ТВ. Ищет по каталогу iptv-org (50 000+ каналов) по названию и стримит, вместе с логотипом канала. Удобно поставить всем футбольный матч./library,/lplay— просмотр и воспроизведение собственных папок с медиа, с выбором аудиодорожки и субтитров.- Общее управление — одна панель «сейчас играет» с паузой, продолжением, пропуском, перемоткой, перемоткой по %, общей громкостью и выключением звука. Громкость именно общая: она меняет сам поток, так что изменение слышат все в звонке, а не только тот, кто нажал.
- Дополнительно — запись стрима в файл, скриншот текущего кадра,
/autoplay(авто-диджей похожих треков, когда очередь пустеет), SponsorBlock для вырезки спонсорских вставок YouTube, loop / shuffle / clear и/topic lock, чтобы привязать бота к одной теме форума.
Под капотом — Python + Pyrogram, стриминг через
py-tgcalls,
yt-dlp для загрузки и ffmpeg (опционально
с VAAPI-ускорением) для перекодирования в то, что примут звонки Telegram.
Главное, что определяет всё: бот не может зайти в звонок
Это факт, вокруг которого построен весь проект, и его стоит сказать прямо, потому что меня он удивил: бот-аккаунт Telegram не может зайти в групповой голосовой чат. В bot API такого понятия просто нет. Воспроизведение в звонок доступно только пользовательскому аккаунту.
Так что tg-streaming-bot — это на самом деле две личности Telegram, работающие вместе:
- Бот — командный интерфейс. Живёт в группе админом (ему нужно право Управление видеочатами, чтобы начать звонок, и Удаление сообщений, чтобы держать панель в порядке). С ним вы и общаетесь.
- Аккаунт-ассистент — настоящий пользовательский аккаунт, вошедший через строку сессии Pyrogram и управляемый py-tgcalls. Именно он заходит в голосовой чат и передаёт настоящие байты аудио и видео.
Бот командует, ассистент стримит. Из этого вытекает пара следствий, о которых узнаёшь, только наступив на них: ассистент должен быть участником каждой группы, где играет (команды воспроизведения приглашают его автоматически), и Telegram разрешает ровно одно голосовое подключение на аккаунт — так что ассистент обязан быть отдельным аккаунтом, а не тем, с которого слушает живой человек. Как только это уложилось в голове, всё остальное свелось к тому, чтобы подключать источники к потоку ассистента.
Боевые истории
1. YouTube: сначала скачай — или ничего
Очевидное решение — отдать стримеру ссылку, как mpv, и пусть тянет поток сам.
YouTube это убил: прямые ссылки на потоки заблокированы, а путь «просто дай URL»
возвращает 403, неотличимые от любых других ошибок. Работает неказистый вариант —
сначала скачать файл (yt-dlp с мобильным клиентом плеера, H.264 + AAC в mp4), а
потом стримить в звонок локальный файл. Даже клиент приходится менять:
android_vr работал, пока тоже не начал получать 403, теперь это android.
Перед стартом приходится подождать несколько секунд «Searching…», но это разница
между надёжным киновечером и бесконечным спиннером. Ссылки не с YouTube (Rutube,
Vimeo, IPTV) разбираются и стримятся сразу, без загрузки, но их HLS-токены
истекают примерно через 30 минут, так что трёхчасовой фильм оттуда иногда
приходится подтолкнуть.
2. Радио, которое работало везде, кроме контейнера
Интернет-радио казалось тривиальным — отдай ffmpeg ссылку на поток. На хосте всё
работало, а в контейнере для нескольких станций ffmpeg падал с «Failed to resolve
hostname». Сами DNS-ответы были в порядке. Встроенный DNS-резолвер Docker
(127.0.0.11) неправильно обрабатывает ответы с цепочками CNAME для некоторых
хостов, и getaddrinfo() из glibc возвращал EAI_NODATA для имён, которые
снаружи прекрасно резолвились. Исправление — две строчки dns: в
docker-compose.yml: контейнер ходит прямо в 1.1.1.1 / 8.8.8.8, минуя этот слой.
Вдобавок /radio теперь один раз повторяет play(), потому что вход в звонок
иногда сам по себе падает с временной ошибкой Telegram INTERDC_X_CALL_ERROR.
«У меня работает» подозрительно часто оказывается историей про DNS.
3. Мастер-плейлисты IPTV и придирчивые CDN
Живое ТВ оказалось двумя багами в одном плаще. Во-первых, записи iptv-org часто
оказываются мастер-плейлистами .m3u8 — меню вариантов по битрейту, а не
потоком. Если отдать плееру такой плейлист напрямую, он может не выбрать ничего. Теперь бот
перед стримом сам выбирает из мастер-плейлиста один конкретный вариант. Во-вторых, многие
из этих CDN отклоняют запросы, не похожие на браузер, так что запрос должен
нести настоящие User-Agent и Referer, иначе получишь вежливый 403. По
отдельности несложно; и то и другое невидимо, пока не смотришь на чёрный
прямоугольник, гадая, почему канал, работающий в VLC, не играет.
4. Пауза, которая снимала сама себя
Самый противный баг был не падением: /pause снимал паузу сам примерно через
пятнадцать секунд, да ещё и отматывал немного назад. У бота есть сторож, который
следит за зависаниями: если позиция воспроизведения не меняется 15 секунд, он
считает, что поток умер, и перезапускает его с последней сохранённой позиции.
Поставленные на паузу потоки он должен был пропускать, проверяя
call.status == PAUSED, — но у объекта звонка в py-tgcalls нет .status (есть
.playback), так что проверка молча никогда не срабатывала, и любая пауза
выглядела как зависание.
Исправить имя атрибута оказалось мало. py-tgcalls сообщает о потоке на паузе как
об IDLE, а не PAUSED — тот же статус, что и у оборвавшегося, — так что никакая
проверка атрибутов их не различит. Настоящее исправление — перестать спрашивать
драйвер и хранить состояние паузы явно: /pause и /resume ставят флаг, и
сторож такие потоки не трогает. Два релиза, чтобы загнать его в угол, — из тех
багов, где каждое исправление вскрывает следующий слой.
5. Хуже всего — обработчик, который падает молча
Поначалу команда, упавшая посреди стрима, просто… останавливалась, оставляя
замороженную карточку «Searching…» на превью и ни намёка почему. Поэтому теперь
каждый обработчик команд и кнопок обёрнут в общий перехватчик ошибок: сбой сам
сообщает о себе обратно в чат (или всплывающим уведомлением — для кнопок), а не растворяется в
docker logs. Отлаживать общий киновечер по SSH, пока четыре человека ждут, —
сомнительное удовольствие; ошибка должна быть просто в чате.
Как я им пользуюсь на самом деле
В основном по пятницам. Кто-то начинает видеочат в группе, я запускаю фильм через
/vplay — и мы смотрим вместе, ставим на паузу на перекусы (и это ставит паузу
всем), спорим о дырах в сюжете в том же чате. В будни чаще /radio фоном, пока
люди приходят и уходят, или /iptv, чтобы включить матч, когда идёт трансляция.
Общая громкость оказалась важнее, чем я ожидал: возможность убавить
фильм для всех, когда кто-то заговорил, с телефона — та мелочь, из-за которой
это ощущается как одна комната, а не четыре.
Попробовать
git clone https://github.com/antlis/tg-streaming-bot && cd tg-streaming-botcp example.env .env # BOT_TOKEN от @BotFather, API_ID/HASH с my.telegram.org# сгенерировать строку сессии ассистента (войдя под отдельным аккаунтом):docker run -it --rm --env-file .env \ -v "$(pwd)/gen_session.py:/gen.py" python:3.11-slim \ sh -c "pip install -q kurigram tgcrypto && python3 /gen.py"docker compose up -d --buildДальше добавьте бота в группу админом, начните голосовой чат и запустите
что-нибудь через /vplay. README
описывает все переменные окружения, структуру локальной медиатеки,
и VAAPI-перекодирование. Issue и PR приветствуются — особенно если найдёте источник,
который yt-dlp умеет, а удобной команды для него у меня ещё нет.