cd ../projects
2026-07-20·8 min de lecture

telOS : Transformer mon PC en Console de Cloud Gaming Local (Moonlight/Sunshine + Launcher Rust Maison)

Construction de telOS, une console de cloud gaming auto-hébergée basée sur Moonlight/Sunshine et un launcher maison en Tauri/Rust, avec un vieux Raspberry Pi comme client portable.

Cloud Gaming

🎮 telOS : Transformer mon PC en Console de Cloud Gaming Local

Quand on parle de cloud gaming, tout le monde pense à GeForce Now ou Xbox Cloud Gaming : le serveur de quelqu'un d'autre, un abonnement mensuel, et une bibliothèque qui ne vous appartient pas vraiment. Il existe une façon bien moins connue d'obtenir exactement la même sensation, streamer un jeu d'un écran à un autre avec une latence quasi imperceptible, en utilisant son propre PC comme serveur. La stack derrière ça s'appelle Moonlight/Sunshine, et elle mérite clairement plus d'attention qu'elle n'en a.

J'avais un vieux Raspberry Pi qui ne servait à rien sur une étagère, et un PC gaming qui ne sert à rien pendant que je suis au boulot ou dans une autre pièce. C'est toute la genèse de telOS : transformer le PC en hôte de streaming de jeux, et transformer le Pi en client portable bête mais capable. Par-dessus cette stack, je construis mon propre launcher en Rust pour qu'un simple appui sur « A » suffise à lancer n'importe lequel de mes jeux, peu importe la plateforme d'origine.


🧠 L'idée : tout héberger, ne streamer qu'un écran

Le principe de base est simple, et c'est l'inverse de la plupart des projets « boîtier de streaming » qu'on croise : rien de la logique du jeu ne quitte jamais le PC hôte.

  • Sunshine tourne sur le PC gaming et streame son affichage et son audio.
  • Moonlight tourne sur le client (le Raspberry Pi, un téléphone, plus tard un Fire TV Stick) et se contente d'afficher ce flux et de renvoyer les entrées manette.
  • telOS est le shell qui tourne sur l'hôte, déclaré comme application Sunshine à part entière (pas juste « Bureau ») pour que Moonlight tombe directement dessus.
┌────────── PC hôte (Windows) ──────────┐      ┌─── Client ───┐
│  telOS ──lance──> Steam / .exe        │      │              │
│     └── affichage ──> Sunshine ───────┼─────>│  Moonlight   │
│                    <──── manette ─────┼──────┤              │
└────────────────────────────────────────┘      └──────────────┘

Comme l'UI n'est pas une appli installée sur le téléphone ou le Pi, lancer un jeu est une action locale et native (spawn de process ou URI steam:// directement sur l'hôte) au lieu de dépendre de deep-links Android fragiles ou d'une API distante. Le client reste bête et interchangeable : aujourd'hui c'est un Raspberry Pi, demain ça pourrait être un Fire TV Stick, et rien dans l'architecture n'a besoin de changer.

L'objectif n'est pas de bien streamer un jeu. C'est de pouvoir jouer à tous mes jeux, toutes plateformes confondues, depuis un handheld, y compris via un partage de connexion mobile quand je ne suis pas chez moi, sans qu'un launcher générique et lourd vienne s'interposer.


🕶️ Pas un dashboard, une console

La plupart des launchers de jeux auto-hébergés finissent par ressembler à un clone de Plex : une grille de posters, une barre de recherche, une roue crantée pour les réglages. Ça fonctionne, mais on a l'impression de naviguer dans un serveur média qui contient des jeux, pas d'allumer une console.

telOS suit une petite liste de règles empruntées à l'analyse de vraies UI de console (PS4, PS5, Xbox, Switch, Wii, DS) :

  • Le jeu sélectionné est toujours le plus gros objet à l'écran.
  • Système et contenu ne se mélangent jamais sur la même couche visuelle.
  • Un overlay permet de vérifier la connexion ou de changer de jeu sans tout quitter.
  • La récence bat le rangement : un rail « continuer » doit résoudre la plupart des lancements tout seul.

Le démarrage du shell joue une courte séquence de boot glitchée, en monospace vert sur noir (INITIALIZING telOS KERNEL..., BYPASSING SECURE BOOT..., MOUNTING VIRTUAL FILESYSTEM..., ESTABLISHING HANDSHAKE..., DECRYPTING PAYLOAD..., ACCESS GRANTED.), avant de se résoudre sur le logo propre. C'est un clin d'œil à toute la prémisse : le PC hôte est traité comme un système qu'on « pénètre », pas juste une appli qu'on lance.

Séquence de boot de telOS : un log terminal glitché qui se résout sur le logo telOS

Il y a une contrainte technique dure qui a façonné tout le langage visuel bien plus qu'un moodboard : cette UI est elle-même compressée en H.265 sur un petit écran, parfois à 10 Mbps. Ça exclut le texte fin, les dégradés subtils (banding horrible avec la compression vidéo) et les détails d'1 px. Ça impose des aplats francs, un contraste élevé, une typo grasse, des gros objets, une contrainte devenue un vrai parti pris esthétique plutôt qu'une limitation.


🕹️ Ce qui tourne réellement aujourd'hui

C'est la partie qui m'excite le plus : telOS a arrêté d'être une simple maquette depuis un moment. Le shell lit la vraie bibliothèque de jeux de ma machine, affiche les vraies jaquettes, et lance réellement des jeux, confirmé sur Elden Ring, entre autres.

Écran principal de telOS : hero plein cadre sur Baldur's Gate 3 avec le rail de récence en dessous

L'écran principal est un hero plein cadre : l'artwork du jeu sélectionné remplit le fond, une barre de statut dans le coin affiche HÔTE LOCAL, le nombre de jeux, et une horloge, et le titre trône en gros au-dessus d'une ligne « continuer » tirée directement de Steam (STEAM · CONTINUER, poids sur le disque, date de dernière session). En dessous, un rail horizontal montre le reste de la bibliothèque, chaque jaquette portant un petit badge de plateforme posé de travers, façon autocollant, pour qu'un jeu Steam et un jeu hors Steam ne soient jamais ambigus.

Fiche jeu de telOS pour Baldur's Gate 3, avec plateforme, développeur, date de sortie, note et boutons d'action

Appuyer sur le bouton « fiche » ouvre une fiche détaillée sur un fond flouté du même artwork : plateforme, développeur, date de sortie, une note agrégée, des tags de genre, un court synopsis, et deux actions (jouer, ou éditer la fiche). C'est volontairement plus proche de la fiche jeu d'une console que d'une ligne de tableur.

Sous le capot, ça repose sur :

  • Un vrai scan Steam : un parser VDF maison (sans dépendance externe) retrouve Steam via le registre Windows, lit libraryfolders.vdf et chaque appmanifest_*.acf, trie par récence, et récupère les jaquettes dans le cache Steam local. Les redistribuables, runtimes et couches de compatibilité Proton sont filtrés pour ne jamais apparaître comme des « jeux ».
  • Une architecture multi-plateforme posée dès le départ : chaque jeu porte sa plateforme d'origine et son propre URI de lancement. Ajouter Epic, EA, GOG ou Ubisoft plus tard revient à déposer un nouveau fichier de plateforme, pas à réécrire le cœur.
  • Des lancements réels, vérifiés : le handler steam:// est enregistré sur ma machine, et le shell spawn effectivement de vrais jeux à travers lui, pas un écran placeholder.

Ma bibliothèque actuelle : 8 jeux sur Steam (fonctionnels), 1 sur Ubisoft Connect, l'app EA installée, et une app Xbox sans le moindre jeu Game Pass. Steam couvre donc déjà l'essentiel de mon usage réel, bon signe que le problème de la « bibliothèque universelle » est plus petit en pratique qu'il n'y paraît.


🗂️ D'où viennent les infos des jeux

Le scan lui-même ne touche jamais le réseau. Le boot n'attend jamais une réponse HTTP, c'est une des règles dures sur lesquelles tout le shell est construit. Les infos de la fiche détaillée (développeur, éditeur, date de sortie, genres, note) sont une autre histoire : des données d'enrichissement, récupérées paresseusement seulement quand on ouvre vraiment la fiche d'un jeu, puis mises en cache sur disque pour ne jamais redemander deux fois.

Deux sources, en cascade :

  • L'API publique appdetails de Steam pour tout ce qui a un appid Steam. Gratuite, sans inscription, sans clé.
  • RAWG comme filet universel pour le reste (jeux hors Steam, ROMs émulées) : une recherche floue par nom qui remplit aussi quelques champs absents côté Steam, comme le temps de jeu moyen ou la classification d'âge. Ça demande une clé API gratuite, inscription sur rawg.io/apidocs.

Les jaquettes suivent la même logique « source gratuite d'abord, filet avec clé en dernier recours » : le CDN de Steam pour les titres Steam, le dépôt communautaire libretro-thumbnails pour les ROMs bien nommées, et SteamGridDB comme dernier recours qui rattrape le reste par recherche floue. C'est la seule brique qui demande un compte, inscription sur steamgriddb.com.

Les deux clés vivent dans un fichier de config local, gitignoré, et ne quittent jamais la machine ni ne sont commitées dans le repo, même règle que tout ce que telOS touche en local.


⚙️ Choix du moteur : Tauri, pas Electron

Deux contraintes ont dirigé cette décision : l'empreinte et la posture de sécurité.

  • Empreinte : Tauri utilise le WebView système (WebView2, déjà présent sur Windows 11) au lieu d'embarquer un Chromium complet. Environ 50 Mo contre 150+ Mo pour la même UI en Electron.
  • Sécurité par défaut : le renderer est sandboxé et n'atteint le système que via des commandes exposées explicitement une par une, derrière une CSP stricte et un cœur en Rust. Avec Electron, obtenir la même posture demande un durcissement manuel où chaque oubli est une faille.
  • La contrepartie assumée : ça nécessite le toolchain Rust, et le prototype actuel en Node pour le scan et le lancement devra à terme migrer vers des commandes Rust natives.

L'UI elle-même est du web standard, donc elle reste portable et ne dépend pas du shell natif pour avancer, exactement comme je l'ai construite jusqu'ici : concevoir et itérer dans le navigateur contre un petit serveur de dev local, puis brancher sur Tauri une fois l'interface stabilisée.

Quelques règles auxquelles je me tiens en le faisant : aucune commande native exposée au renderer sans nécessité stricte, aucun chemin d'exécutable ni argument construit à partir d'une entrée non validée, et la bibliothèque scannée plus le cache de jaquettes restent des données strictement locales, jamais commitées dans le repo.


🗺️ La suite

Globalement dans cet ordre :

  1. Cœur natif : scaffolder le shell Tauri et faire migrer le scanner de bibliothèque du prototype Node vers des commandes Rust, avec une persistance locale pour les réglages et le cache de bibliothèque.
  2. Sunshine/Moonlight, pour de vrai : installer et configurer Sunshine sur l'hôte, déclarer telOS comme application Sunshine dédiée, valider un aller-retour LAN propre avec de vraies valeurs de latence et de débit affichées dans la barre de statut, plus des placeholders.
  3. Accès distant : mon partage de connexion mobile est derrière du CGNAT, donc la redirection de port classique est hors jeu. Tailscale est le plan pour traverser ça proprement, avec un bitrate adaptatif pour garder une conso data raisonnable (le streaming avale environ 9 Go/h à 20 Mbps).
  4. Petits extras, évalués honnêtement : une intégration Spotify Web API pour la pochette et le contrôle de lecture est simple et bien documentée. Embarquer le vocal Discord, en revanche, n'a aucune API tierce officielle ; la seule voie non officielle (automatiser le client avec un token utilisateur) viole les CGU de Discord et met le compte en danger, donc exclue d'office. Comme telOS tourne sur le même PC que tout le reste, la réponse la plus simple et honnête est d'afficher le vrai client Discord en overlay, plus le SDK officiel Rich Presence pour diffuser « en train de jouer à Elden Ring » depuis telOS lui-même.
  5. D'autres clients : la séparation hôte/client fait qu'un Fire TV Stick n'est qu'un client Moonlight de plus, sans nouvelle architecture nécessaire. Le hic : la télécommande Fire TV n'a qu'un D-pad et deux boutons, donc la navigation doit fonctionner avec ce minimum, même si jouer nécessite toujours une vraie manette.

🧩 Le nouveau rôle du Raspberry Pi

Le Pi qui traînait sans rien faire sur une étagère devient le client portable : Moonlight installé, pointé vers l'instance Sunshine de l'hôte, et rien d'autre qui tourne dessus. Il n'a pas besoin d'être puissant. Décoder un flux H.265 coûte peu comparé à réellement faire tourner le jeu, ce qui est exactement l'intérêt du cloud gaming fait en local : toute la charge lourde reste sur le PC qui a déjà le GPU.

Le shell tourne déjà et lance de vrais jeux ; le cœur natif Tauri et le câblage Sunshine/Moonlight sont les prochains vrais jalons. Je referai un point dès que le Raspberry Pi streamera son premier jeu de bout en bout.