Telegram Mini App для совместного просмотра видео: комнаты, общий плеер, который держит всех на одном таймкоде, и чат в реальном времени. Владелец комнаты управляет воспроизведением, остальные смотрят синхронно.
Telegram WebApp → FastAPI (REST + WebSocket) → Redis (player state)
↓ ↓
React 19 + Vite yt-dlp + ffmpeg (прокси потока)
Watch-party Telegram Mini App. The room owner controls playback (play/pause/seek) and loads a video by URL or YouTube search; every participant's player follows the shared state. Rooms are private or invite-only, and each one has a real-time chat.
Stack: FastAPI, SQLAlchemy 2 (async), SQLite + Redis, WebSockets, yt-dlp/ffmpeg, React 19, Vite, nginx.
- Комнаты — создание, пароль, приватность, список в лобби.
- Синхронизация — владелец играет/ставит на паузу/перематывает, остальные повторяют. Расхождение больше 2 секунд автоматически выравнивается.
- Загрузка видео — поиск по YouTube (
ytsearch10) или вставка прямой ссылки. Поток проксируется через backend:yt-dlpдостаёт прямые URL видео и аудио,/api/player/streamпроксирует их с поддержкойRangeи переписывает.m3u8-плейлисты. - Раздельные аудио и видео — если источник отдаёт их разными потоками, аудио становится мастер-клоком, а видео подгоняется под него (допуск 0.3 с).
- Чат — сообщения через WebSocket, хранятся в БД, поддерживается удаление своих.
- Участники — роли
owner/guest, вход по инвайт-коду. - Проверка рассинхрона — сервер умеет считать дельту между таймкодом клиента и
состоянием комнаты (WS-действие
check_desync). - Полноэкранный режим, тактильный отклик и кнопка «назад» через Telegram API.
- HLS — воспроизведение
.m3u8черезhls.js.
| Слой | Технологии |
|---|---|
| Backend | Python 3.12 / 3.13, FastAPI, uvicorn |
| ORM / БД | SQLAlchemy 2 (async), aiosqlite (SQLite) |
| Состояние плеера | Redis (redis.asyncio) |
| Real-time | WebSockets (websockets, uvicorn) |
| Видео | yt-dlp, ffmpeg, httpx (прокси потока) |
| Auth | HMAC-SHA256 валидация initData из Telegram |
| Frontend | React 19, Vite, React Router 7, motion, hls.js |
| Reverse proxy | nginx |
| Тесты | pytest, pytest-asyncio, httpx |
git clone https://github.com/Yarfer800/sync-player.git
cd sync-player
cp .env.example .env # подставьте свой токен бота
docker compose up --build- Фронтенд — http://localhost
- API — http://localhost:8000 (Swagger: http://localhost:8000/docs)
- Redis — localhost:6379
Backend:
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # или: uv sync
cp .env.example .env # заполнить token / database_url / admin_id
python main.py # uvicorn с reload на :8000Frontend:
cd frontend
npm install
npm run dev # http://localhost:5173Vite проксирует /api и /api/ws на http://localhost:8000, так что CORS в dev не мешает.
Переменные читаются из .env в корне (app/core/config.py).
| Переменная | Обязательна | По умолчанию | Назначение |
|---|---|---|---|
TOKEN |
да | — | токен Telegram-бота; ключ для проверки подписи initData |
DATABASE_URL |
да | — | SQLAlchemy async URL, например sqlite+aiosqlite:///app.db |
ADMIN_ID |
да | — | Telegram ID администратора |
REDIS_URL |
нет | redis://localhost:6379 |
Redis для состояния плеера |
Переменные фронтенда (.env в frontend/):
| Переменная | Назначение |
|---|---|
VITE_API_URL |
база API, по умолчанию /api (nginx/Vite-прокси) |
VITE_MOCK_INIT_DATA |
поддельный initData для локальной разработки без Telegram |
При добавлении своего домена в
app/app.pyне забудьте внести его вallow_originsуCORSMiddleware— там захардкожен список (localhost:5173,localhost:8000, Vercel-домен).
app/
api/
deps.py зависимости FastAPI, валидация initData, провайдеры репозиториев
routes/ users, rooms, messages, player, search, ws
core/config.py pydantic-settings
db/ engine, модели (users, rooms, room_participants, messages), redis
repositories/ доступ к данным, один класс на сущность
schemas/ pydantic-схемы запросов/ответов
services/ player (yt-dlp), player_state, websocket, web_search
frontend/src/
api/ HTTP-клиент и WebSocket-обёртка
components/ layout, lobby, room, ui
context/ AuthContext
hooks/ useRoom, useMessages, usePlayerState, useWebSocket
pages/ LobbyPage, RoomPage, ProfilePage
utils/telegram.js initData, haptic, back button
tests/ pytest: API, репозитории, плеер, поиск
Слои идут строго сверху вниз: routes → services → repositories → db.
Репозитории не знают про HTTP, бизнес-логика живёт в services и роутах.
Никаких паролей и JWT — только Telegram.
- Фронтенд читает
window.Telegram.WebApp.initData(в dev —VITE_MOCK_INIT_DATA). - Отправляет его в заголовке
X-Init-Data(для WebSocket — в query-параметреinit_data). validate_init_data()пересобирает data-check-строку, считаетHMAC-SHA256(HMAC-SHA256("WebAppData", token), data_check_string)и сверяет сhashчерезhmac.compare_digest.- Пользователь создаётся в БД при первом запросе,
usernameсинхронизируется на каждом входе.
Все маршруты под префиксом /api. Фронтенд шлёт X-Init-Data с каждым запросом;
на мутациях (POST / PUT / PATCH / DELETE) подпись проверяется обязательно, а
GET-ручки комнат, участников и сообщений сейчас открыты.
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/users/me |
текущий пользователь |
PATCH |
/api/users/me |
обновить username |
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/rooms |
список комнат с количеством участников |
POST |
/api/rooms |
создать комнату (создатель становится owner) |
GET |
/api/rooms/{room_id} |
комната со списком участников |
DELETE |
/api/rooms/{room_id} |
удалить комнату (только owner) |
POST |
/api/rooms/{room_id}/join |
войти по паролю |
POST |
/api/rooms/{room_id}/leave |
выйти |
POST |
/api/rooms/{room_id}/invite |
выпустить инвайт-код (только owner) |
POST |
/api/rooms/join/invite |
войти по инвайт-коду |
GET |
/api/rooms/{room_id}/participants |
участники комнаты |
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/rooms/{room_id}/player/state |
текущее состояние плеера |
POST |
/api/rooms/{room_id}/player/state |
создать состояние (только owner) |
PUT |
/api/rooms/{room_id}/player/state |
обновить состояние + рассылка участникам (только owner) |
{
"room_id": 1,
"current_timecode": 42.5,
"video_source_link": "https://www.youtube.com/watch?v=...",
"is_paused": true
}| Метод | Путь | Описание |
|---|---|---|
POST |
/api/player/info |
метаданные + прямые URL видео/аудио (yt-dlp) |
POST |
/api/player/download |
скачать фрагмент по таймкодам, отдаётся файлом |
GET |
/api/player/stream?url=... |
прокси потока с Range и переписыванием HLS-плейлиста |
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/rooms/{room_id}/messages |
история сообщений (limit 1–500, offset) |
POST |
/api/rooms/{room_id}/messages |
отправить сообщение (+ рассылка) |
DELETE |
/api/rooms/{room_id}/messages/{message_id} |
удалить своё сообщение |
GET |
/api/search?query=... |
поиск видео на YouTube |
Подключение: ws(s)://<host>/api/ws/rooms/{room_id}?init_data=<initData>
Клиент шлёт {"action": "...", "payload": {...}}:
| Действие | Payload | Ответ сервера |
|---|---|---|
send_message |
{text, image?} |
рассылка new_message всей комнате |
check_desync |
{timecode} |
лично клиенту desync_result с desync_seconds и room_timecode |
Сервер шлёт:
| Событие | Когда |
|---|---|
new_message |
новое сообщение в комнате (WS или REST) |
player_state_updated |
владелец обновил состояние плеера |
desync_result |
ответ на check_desync |
Фронтенд переподключается через 3 секунды после нештатного закрытия. Действие
check_desync реализовано на сервере, но текущий клиент его не вызывает — задел
на будущую кнопку «синхронизироваться со мной».
Владелец — источник истины, его клиент не слушает рассылку player_state_updated,
чтобы не зациклиться на своих же событиях.
- Владелец жмёт play/pause или двигает скраббер →
PUT /player/state→ запись в Redis → рассылкаplayer_state_updatedпо комнате. - Гость получает состояние: расхождение больше 2 с → жёсткий
seek. - Если видео и аудио идут разными потоками, мастер-клок — аудио; если
video.currentTimeотстаёт больше чем на 0.3 с — видео догоняет аудио. player_state_updatedв клиенте применяется только у не-владельцев.
pip install -r requirements.txt
pytestПокрыты API-роуты (test_api_*), репозитории (test_*_repo.py), плеер и поиск.
Тесты test_player.py и test_web_search.py ходят в сеть (YouTube) и качают реальный
фрагмент — им нужен установленный ffmpeg и стабильное соединение.
Render — render.yaml из корня, два сервиса:
sync-player-api—pip install -r requirements.txt+uvicorn app.app:app, переменныеPYTHON_VERSION,CORS_ORIGINS,BOT_TOKEN;sync-player-frontend— static site,npm run buildизfrontend/, публикацияdist.
В
render.yamlвstartCommandуказанapp.main:app, а приложение лежит вapp/app.py(app.app:app) — поправьте при деплое.
Docker / nginx — frontend/Dockerfile собирает Vite и раздаёт статику через nginx,
который проксирует /api/ на контейнер api:8000 с поддержкой WebSocket (Upgrade).
Vercel — фронтенд статикой, frontend/vercel.json проксирует /api/* на удалённый
backend; поправьте адрес в destination под свой хост.
.envне должен попадать в git: в нём лежит токен бота, а.gitignoreв корне репозитория вообще нет. Если файл уже в истории — смените токен через @BotFather, добавьте.env,app.dbи__pycache__/в.gitignoreи почистите историю.- Эндпоинты
/api/player/streamи/api/player/downloadвыполняют произвольные URL от имени сервера (SSRF-риск). Для публичного инстанса добавьте allowlist доменов вapp/api/routes/player.py. app.dbтоже лежит в репозитории — в проде смонтируйте его на диск или переходите на PostgreSQL.