Watch together cinema
Build a private, self-hosted "watch a movie together in sync" site for two people in different countries — synced play/pause/seek over WebSocket, WebRTC camera+mic call with a coturn TURN relay, a shared laser pointer, ffmpeg smooth-bitrate transcoding so it never buffers, and a token-gated room link. Use when someone wants to watch local video files in sync with a partner/friend remotely, replace Kosmi/Teleparty/Twoseven with something they own, stream a personal movie library over the web with synced controls, or add a video-call overlay to a video player. Triggers: "watch movies together long distance", "sync video playback two browsers", "self-hosted watch party", "Kosmi alternative", "stream my movie to my girlfriend with subtitles and sync".From its SKILL.md
npx -y skills add bertbertov/claude-stack --skill watch-together-cinemaAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.8 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
Watch-Together Cinema
A private cinema you own: upload a movie, get an unguessable room link, send it to one other person, and you both watch in sync (either side controls play/pause/seek) with an optional camera+mic call overlaid and a shared laser pointer. No Kosmi/Teleparty subscription, no per-message limits, no sending big files around.
Reference implementation lives in reference/ — a ~180-line FastAPI backend, a YouTube-style player, an admin page, plus nginx + coturn examples. Copy them, fill placeholders, deploy.
Architecture (the important distinction)
There are two separable problems and only one is hard:
- Sync (mirror play/pause/seek between two browsers) — trivial: a WebSocket relay.
- Delivering the video — the real work. You serve the file over HTTP with byte-range (so seeking works) directly from the host.
browser A ─┐ ┌─ nginx :443 (TLS)
├── wss sync + signaling ──┤ ├── /media/* → static file, byte-range (video)
browser B ─┘ │ └── / → uvicorn :8088 (FastAPI: pages, upload, WS)
└─ coturn :3478 → WebRTC relay (camera/mic, NAT fallback)
- Movie stream: peer downloads the file from your server. One viewer = trivial bandwidth.
- Camera call: WebRTC peer-to-peer (cameras go directly between the two browsers). The WebSocket doubles as the signaling channel; coturn relays only when NAT/CGNAT blocks a direct path (common on mobile/home ISPs across countries).
Components (in reference/)
| File | What it is |
|---|---|
app.py | FastAPI: token-gated watch rooms, key-gated admin upload/list/delete, WebSocket that relays sync + WebRTC signaling + pointer, ffmpeg faststart-remux + .srt→.vtt on upload. Serves video via StaticFiles (byte-range capable). Admin key from CINEMA_ADMIN_KEY env — no hardcoded secrets. |
player.html | The watch room. Custom YouTube-style controls (auto-hide, scrub bar), keyboard (Space/K play, ←/→ 5s, J/L 10s, ↑/↓ volume, M mute, C subs, F fullscreen, P pointer), buffer-aware drift-correction sync, WebRTC call with draggable/resizable tiles, shared laser pointer. Has {{TURN_HOST}} / {{TURN_USER}} / {{TURN_PASS}} placeholders — fill before serving. |
admin.html | Upload (movie + optional subtitles) with progress, list, copy room link, delete. Key comes from ?key= in the URL. |
nginx.conf.example | Reverse proxy: serves /media/ statically (native range), proxies everything else + WebSocket upgrade to uvicorn, client_max_body_size 0 for multi-GB uploads. |
turnserver.conf.example | coturn config (long-term credential, relay port range, external-ip). |
Deploy (once)
- Server + Python:
mkdir -p /opt/cinema/media /opt/cinema/static python3 -m venv /opt/cinema/venv /opt/cinema/venv/bin/pip install fastapi "uvicorn[standard]" python-multipart websockets cp reference/app.py /opt/cinema/app.py cp reference/player.html reference/admin.html /opt/cinema/static/ apt-get install -y ffmpeg # for faststart remux + srt→vtt - Admin key (systemd unit):
# /etc/systemd/system/cinema.service [Service] Environment=CINEMA_ADMIN_KEY=%GENERATE_A_RANDOM_HEX% # openssl rand -hex 12 WorkingDirectory=/opt/cinema ExecStart=/opt/cinema/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8088 Restart=always [Install] WantedBy=multi-user.targetsystemctl enable --now cinema - Domain + TLS. Point a subdomain straight at the host (A record, DNS-only if behind Cloudflare — CF's free plan caps uploads at 100 MB and restricts video, so the media path MUST bypass the proxy). Then
certbot --nginx -d cinema.example.comwithnginx.conf.example. - TURN (coturn) for the camera call to survive cross-country NAT:
Put the same TURN host/user/password intoapt-get install -y coturn cp reference/turnserver.conf.example /etc/turnserver.conf # set your own user:password, realm, external-ip sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn systemctl enable --now coturnplayer.html's{{TURN_*}}placeholders. Verify the relay actually works:turnutils_uclient -y -u USER -w PASS -n 4 YOUR_HOSTshould reportTotal lost packets 0.
Use
- Admin:
https://cinema.example.com/admin?key=YOUR_ADMIN_KEY→ upload movie (+.srt) → wait for ready → copy the room link. - Send the room link to the other person. Both open it → Start watching together → synced. Press P and move the mouse for the shared pointer. Click the call icon to turn on cameras (earbuds recommended — speakers + open mic = echo).
The buffering fix (this is the #1 real-world problem)
Movies are usually VBR — a file that averages ~2 Mbit/s spikes to ~10 Mbit/s on busy scenes. While a camera call also runs, those spikes outrun a home/mobile connection and the movie stutters. Fix: transcode to a capped constant bitrate before uploading (no spikes, visually identical). On a machine with an NVIDIA GPU:
ffmpeg -i input.mp4 -map 0:v:0 -map 0:a:0 \
-c:v h264_nvenc -preset p5 -rc vbr -b:v 2500k -maxrate 3000k -bufsize 6000k \
-profile:v high -pix_fmt yuv420p -c:a aac -b:a 160k -movflags +faststart output.mp4
# CPU-only: swap -c:v h264_nvenc ... for -c:v libx264 -crf 21 -maxrate 3000k -bufsize 6000k
Extract embedded subtitles to upload alongside: ffmpeg -i input.mp4 -map 0:s:0 subs.srt
Do heavy transcodes on a GPU box (minutes), not a CPU-only VPS (hours). The app's on-upload step only does a fast stream-copy faststart remux (-c copy -movflags +faststart) — instant for already-H.264/AAC MP4s — plus .srt→.vtt.
Design notes worth keeping
- Sync is buffer-aware. Don't hard-seek on small drift (causes visible skipping). Only seek when drift > ~3s (someone scrubbed); for smaller drift the behind side nudges
playbackRateto 1.04 to glide back. Suppress state broadcasts whilebuffering, so a stalled peer doesn't drag the other backward. - Fullscreen the wrapper, not the
<video>. Native video-fullscreen hides sibling elements — so the camera tiles and pointer vanish. CallrequestFullscreen()on the container that holds video + tiles + pointer layer. - Cap the call bitrate (
sender.setParametersmaxBitrate ~400 kbps, 360p/20fps) so the call never starves the movie stream. - Privacy: room tokens and media filenames are unguessable random hex; admin is key-gated; pages carry
noindex. It's private-by-obscurity for two people — fine for personal use, not a public multi-tenant service. - Codec: browsers only reliably play MP4 / H.264 / AAC. MKV/HEVC/AC3 must be transcoded (the capped-bitrate command above also fixes codec).
When NOT to use
- More than a handful of simultaneous viewers, or a public audience → use a real streaming/CDN stack, not this.
- You don't control a server with a domain → use Syncplay (each person keeps their own file copy) or a hosted watch-party service.
What ships with it: 5 files
35.4 KB alongside SKILL.md, 1 of them executable
reference/
- admin.html5.8 KB
- app.pyruns6.8 KB
- nginx.conf.example1.3 KB
- player.html20.8 KB
- turnserver.conf.example787 B