Outil web / Synchronisation

SelixyWatch

Vidéos et musique regardées ensemble, à la même seconde

JavaScriptPythonPostgreSQLSSEPodman

SelixyWatch permet de regarder des vidéos et d’écouter de la musique à plusieurs, à la même seconde, chacun chez soi. Le dépôt commence le 14 octobre 2026 avec une écoute musicale synchronisée, puis s’est élargi en une quinzaine de jours : playlists, comptes, partage, vidéo, chat. Il est en cours, et plusieurs choix décrits plus bas sont encore à l’essai.

Playlists et rooms

Le site a deux pages. Les playlists rangent des médias dans des dossiers imbriqués, avec un rangement par glisser-déposer. Un même média peut être dans plusieurs playlists, et il est supprimé quand il n’est plus dans aucune. On y importe des fichiers, on cherche sur YouTube, ou on colle un lien YouTube ou Spotify : le serveur lit les titres et les durées, et retrouve chaque morceau Spotify sur YouTube en prenant la durée la plus proche.

La page Playlists : dossiers imbriqués à gauche, contenu de la playlist ouverte à droite
Une room : vidéo partagée, file « À suivre » et chat

La room joue la playlist dans laquelle on est entré. Tous ceux qui y sont voient et entendent la même chose. Elle a un lecteur façon YouTube avec ses raccourcis clavier, un mode cinéma, une file « À suivre » avec boucle et aléatoire partagés par toute la room, un chat et des réactions emoji qui flottent sur la vidéo avec l’avatar de celui qui réagit. Le son reste local : couper le sien ne coupe celui de personne.

Comment la synchro marche

Le serveur garde l’état de chaque room (média, position, lecture ou pause) et le diffuse en SSE. Chaque navigateur mesure son écart avec l’horloge du serveur, calcule la position cible et corrige toute dérive de plus de 0,4 s, vérifiée chaque seconde. La position affichée vient toujours de l’état du serveur, jamais du lecteur local.

À la fin d’un média, le premier lecteur qui le signale fait passer au suivant ; sinon le serveur enchaîne seul une seconde plus tard. Les navigateurs exigent un geste avant de jouer du son, donc un bouton « Activer le son » apparaît quand il le faut.

J’ai pris SSE plutôt que des WebSockets parce que le flux ne va que dans un sens : le serveur diffuse, et les commandes (lecture, pause, saut) repartent par de simples requêtes POST.

YouTube et ses limites

Les vidéos YouTube sont jouées par le lecteur intégré de YouTube, synchronisé comme le reste. Rien n’est téléchargé. Ça impose des limites que le site ne peut pas lever, parce que le lecteur vit dans un cadre d’un autre site. Il ne peut pas retirer les pubs (chacun garde son bloqueur), mais quelqu’un qui en subit une est remis à la bonne seconde quand elle finit. YouTube affiche aussi ses étiquettes pendant environ cinq secondes après chaque démarrage ou saut, sans réglage possible. Une règle de filtre pour bloqueur de contenu les retire, et le site en publie la liste.

J’avais d’abord essayé de ne plus dépendre du lecteur de YouTube. Un petit service dans le pod résout les pistes avec yt-dlp et les relaie, et le navigateur les assemble avec les Media Source Extensions. Ça marche techniquement, mais YouTube refuse les requêtes venant de l’IP du VPS (« Sign in to confirm you’re not a bot »), même avec des cookies d’un compte secondaire. Le lecteur est donc revenu à l’iframe. Le service et son code restent dans le dépôt, non branchés, en attendant une IP de sortie acceptée.

Chat vocal et vidéo

La room a aussi un chat vocal et vidéo en WebRTC, avec un menu profil pour le micro, la caméra et la sourdine, et une rangée des comptes présents. Le serveur ne fait que relayer les signaux d’établissement de la connexion. Pour que le micro ne renvoie pas le son de la vidéo, la référence d’écho passe par le partage de l’onglet, ce qui retire aussi le son de l’iframe YouTube. Je n’ai pas de recul sur la tenue de ce vocal à plus de quelques personnes.

Comptes et partage

Les comptes se créent avec un pseudo et un mot de passe, ou en anonyme : le compte « Invité » est éphémère et supprimé à la déconnexion avec tout ce qui n’est lié qu’à lui. Les pseudos sont uniques sans tenir compte de la casse ni des sosies (« Intel » et « Іntel » en cyrillique sont le même). Les mots de passe sont hachés en scrypt, et seul le hachage du jeton de session est en base.

Un dossier ou une playlist se partage par lien d’invitation, avec le rôle d’éditeur. Le partage vaut pour tout le sous-arbre, et chacun peut ensuite ranger et renommer à sa façon sans toucher à la bibliothèque du propriétaire. Il y a une adresse e-mail à valider, une récupération de compte par courriel, et deux offres : l’envoi de fichiers est réservé aux comptes Pro, avec 2 Go par compte et 4 Go par média, et un compte administrateur n’a pas de limite.

Technique

L’interface est un site statique en modules JavaScript, sans compilation ni bibliothèque. Une coque garde les trois vues chargées (room, playlists, profil), chacune dans son cadre, si bien que la lecture continue pendant qu’on parcourt les playlists. Les vues se parlent par postMessage.

L’API est un petit serveur Python avec PostgreSQL, dans un pod Podman derrière nginx avec le service de flux. Seul l’état de lecture des rooms reste en mémoire, parce qu’il change chaque seconde et se reconstruit au redémarrage. Le déploiement se fait par un git push : un hook installe l’interface, applique les migrations et redémarre l’API. Une sauvegarde de la base tourne chaque jour.

Je m’impose des règles strictes : 200 lignes et cinq fonctions au plus par fichier, une responsabilité par fichier, aucun commentaire dans le code (le pourquoi va dans le README du module). Un script d’audit les mesure et bloque le commit. Le code hérité des premières versions, écrit avant ces règles, est listé écart par écart dans un fichier d’exceptions qui ne peut que diminuer.

Où j'en suis

La synchro, les playlists, les comptes, le partage et le chat fonctionnent. Je teste sur un banc local avec une base PostgreSQL jetable, et les tests couvrent surtout le serveur et quelques modules de l’interface. Le vocal et la vidéo sont récents et peu éprouvés, et la lecture YouTube dépend de ce que YouTube tolère. Il reste du code hérité à remettre aux règles.

Liens

Web tool / Synchronization

SelixyWatch

Videos and music watched together, to the same second

JavaScriptPythonPostgreSQLSSEPodman

SelixyWatch lets several people watch videos and listen to music at the same second, each from their own place. The repository starts on 14 October 2026 with synchronized music playback, then grew over about two weeks: playlists, accounts, sharing, video, chat. It is still in progress, and several choices described below are still being tried out.

Playlists and rooms

The site has two pages. Playlists organize media in nested folders, with drag-and-drop sorting. The same media can be in several playlists, and it is deleted once it is in none. You can import files, search YouTube, or paste a YouTube or Spotify link: the server reads titles and durations, and finds each Spotify track on YouTube by picking the closest duration.

The Playlists page: nested folders on the left, the open playlist on the right
A room: shared video, "Up next" queue and chat

The room plays the playlist you entered. Everyone in it sees and hears the same thing. It has a YouTube-style player with keyboard shortcuts, a cinema mode, an “Up next” queue with loop and shuffle shared by the whole room, a chat, and emoji reactions that float over the video with the avatar of whoever reacts. Sound stays local: muting yours mutes nobody else’s.

How the sync works

The server keeps each room’s state (media, position, playing or paused) and broadcasts it over SSE. Each browser measures its offset from the server clock, computes the target position and corrects any drift above 0.4 s, checked every second. The displayed position always comes from the server state, never from the local player.

When a media ends, the first player to report it moves the room on to the next one; otherwise the server does it alone one second later. Browsers require a gesture before playing sound, so an “Enable sound” button appears when needed.

I took SSE rather than WebSockets because the stream only goes one way: the server broadcasts, and commands (play, pause, seek) go back as plain POST requests.

YouTube and its limits

YouTube videos are played by YouTube’s embedded player, synchronized like the rest. Nothing is downloaded. This imposes limits the site cannot lift, because the player lives in a frame from another site. It cannot remove ads (everyone keeps their own blocker), but someone who gets an ad is put back on the right second when it ends. YouTube also shows its labels for about five seconds after every start or seek, with no setting to turn them off. A filter rule for a content blocker removes them, and the site publishes the list.

I first tried not to depend on YouTube’s player. A small service in the pod resolves the tracks with yt-dlp and relays them, and the browser assembles them with Media Source Extensions. It works technically, but YouTube refuses requests coming from the VPS IP (“Sign in to confirm you’re not a bot”), even with cookies from a secondary account. The player went back to the iframe. The service and its code stay in the repository, not wired in, until an exit IP that YouTube accepts is available.

Voice and video chat

The room also has a WebRTC voice and video chat, with a profile menu for microphone, camera and mute, and a row of the accounts present. The server only relays the signals that set up the connection. To keep the microphone from sending back the video’s sound, the echo reference goes through tab sharing, which also removes the sound of the YouTube iframe. I have no data yet on how this voice chat holds up beyond a few people.

Accounts and sharing

Accounts are created with a username and password, or anonymously: the “Guest” account is ephemeral and deleted at logout along with everything tied only to it. Usernames are unique regardless of case and lookalike characters (“Intel” and “Іntel” in Cyrillic are the same). Passwords are hashed with scrypt, and only the hash of the session token is stored in the database.

A folder or playlist is shared through an invitation link, with the editor role. Sharing covers the whole subtree, and each person can then sort and rename in their own way without touching the owner’s library. There is an e-mail address to validate, account recovery by e-mail, and two plans: file upload is reserved for Pro accounts, with 2 GB per account and 4 GB per media, and an administrator account has no limit.

Technical side

The interface is a static site in JavaScript modules, with no build step and no library. A shell keeps the three views loaded (room, playlists, profile), each in its own frame, so playback continues while you browse playlists. The views talk to each other through postMessage.

The API is a small Python server with PostgreSQL, in a Podman pod behind nginx along with the stream service. Only the rooms’ playback state stays in memory, because it changes every second and is rebuilt on restart. Deployment is a git push: a hook installs the interface, applies the migrations and restarts the API. A database backup runs every day.

I hold myself to strict rules: at most 200 lines and five functions per file, one responsibility per file, no comments in the code (the why goes in the module’s README). An audit script measures them and blocks the commit. Code inherited from the first versions, written before these rules, is listed gap by gap in an exceptions file that can only shrink.

Where I am

Sync, playlists, accounts, sharing and chat work. I test on a local bench with a disposable PostgreSQL database, and the tests mostly cover the server and a few interface modules. Voice and video are recent and little tested, and YouTube playback depends on what YouTube tolerates. Some inherited code still has to be brought up to the rules.

Links