DOCS/v2/vr-quest-unity-plan.md
2026-09-03 14:00:51 +02:00

23 KiB
Raw Blame History

VR Meta Quest — app Unity + onglet XR dans manager-app — 📦 V2

Relevé le 2026-08-31. Toutes les affirmations « existe / n'existe pas » ci-dessous ont été vérifiées dans le code, pas dans la doc. Ce fichier remplace la section VR de ../roadmap.md comme référence technique — la roadmap garde le volet commercial.

⚠️ Rien de ce document n'est V1. La bascule Postgres et le backlog V1 passent avant.


0. Correction d'une affirmation de STATUS.md

STATUS.md §5 disait jusqu'ici : VR Meta Quest = « idée, zéro ligne de code dans tous les repos ». C'est faux depuis au moins juillet 2026. Le canal VR est déjà un citoyen de première classe du modèle de données et de la navigation du back-office. Ce qui manque, c'est l'app et l'écran de flotte — pas les fondations.


1. Ce qui existe déjà

1.1 Backend (manager-service)

Brique Emplacement État
AppType.VR Data/ApplicationInstance.csenum AppType { Mobile, Tablet, Web, VR, Voice } Valeur 3, persistée en base
Instance.IsVR Data/Instance.cs:35, DTOs/InstanceDTO.cs:16, InstanceController.cs:212 Activable par instance, exposé au DTO, modifiable par l'API
Migration 20250709151627_AddedApplicationInstanceAndMisc Colonne IsVR en base depuis juillet 2025
ApplicationInstance VR Controllers/ApplicationInstanceController.cs — CRUD complet + application-link Une instance VR se crée déjà par l'API, sans une ligne de code neuve
AppConfigurationLink Data/AppConfigurationLink.cs Porte déjà DeviceId (nullable) — c'est exactement le mécanisme « une config par appareil » du kiosk
Entité Device Data/Device.cs Identifier, Name, Connected, BatteryLevel, ConnectionLevel, AppVersion, LastSeen, InstanceId, ConfigurationId
DeviceController 7 endpoints (list, detail, create, update, mainInfos, delete) ⚠️ Existe, mais hardcodé sur AppType.Tablet (DeviceController.cs:155)
Push MQTT « config changée » DeviceController.cs:303 → topic player/{device.Id} Le mécanisme de rafraîchissement à distance est déjà là
Télémétrie Data/VisitEvent.cs:27public AppType AppType Un event VR se stocke sans aucun changement de schéma
Export de contenu GET /api/configuration/{id}/export (ConfigurationController.cs:388) Configuration + sections + ressources en un seul appel — le contrat idéal pour un renderer tiers
Auth app cliente GET /api/instance/appKeyByPin (InstanceController.cs:347) ⚠️ Existe, mais ApiKeyAppType ne connaît que { VisitApp, TabletApp, Other }

1.2 manager-app

Brique Emplacement État
AppType.VR côté client manager_api_new/lib/model/app_type.dart + app_type_extensions.dart Valeur 3 présente et nommée « VR »
Sous-menu « VR » lib/Screens/Main/main_screen.dart:65if (instance.isVR == true) L'entrée de menu apparaît déjà si l'instance a le canal activé
Écran VR main_screen.dart:698-705case 'vr' Text("TODO vr") — c'est littéralement le seul trou de la navigation
Écran de flotte à cloner lib/Screens/Kiosk_devices/ — 4 fichiers, 769 lignes kiosk_screen.dart (grille d'appareils), device_element.dart, dropDown_configuration.dart (assignation de config), change_device_info_modal.dart
Filtre stats par canal lib/Screens/Statistics/statistics_screen.dart:81, 177 Générique sur AppType.values, et statsChannelVR existe déjà en i18n — le canal VR apparaîtra tout seul dès le premier VisitEvent

1.3 Ce qui n'existe nulle part

  • Aucun projet Unity, aucun .unity, aucun package Meta XR dans les repos.
  • Aucun rendu 360° : pas de ResourceType.Panorama360 (planifié en V2, voir ../todo-features.md § Ressource 360°), pas de flag « vidéo equirectangulaire ».
  • Aucun Device non-tablette : la table est peuplée exclusivement par tablet-app.

2. Ce qu'il reste à faire

Lot V-1 — Le Device devient multi-canal (backend) — 2-3 j

Aujourd'hui un appareil est une tablette : DeviceController.Create va chercher ApplicationInstances.FirstOrDefault(ai => ai.AppType == AppType.Tablet) en dur. Un casque enregistré par ce chemin serait rattaché à l'instance kiosk et apparaîtrait dans l'onglet Kiosk.

  • Device.AppType (colonne int, défaut Tablet = 1 pour que les lignes existantes ne bougent pas) + migration EF
  • DeviceDTO.appType / DeviceDetailDTO, mappé dans ToDTO / ToDetailDTO / FromDTO
  • DeviceController.Create : résoudre l'ApplicationInstance sur newDevice.appType au lieu de AppType.Tablet
  • DeviceController.GetAll : paramètre de requête appType optionnel (sans lui, comportement actuel)
  • ApiKeyAppType : ajouter VrApp à la fin de l'enum ({ VisitApp, TabletApp, Other, VrApp }) — l'enum est persisté en int, ne jamais insérer au milieu
  • DeviceControllerTests : un cas « création d'un device VR » qui vérifie le rattachement à la bonne ApplicationInstance

⚠️ Le champ Device.ConfigurationId est marqué [Required] et le commentaire du code le dit « OLD WAY → AppConfigurationLink ». Le casque doit passer par AppConfigurationLink, pas par ce champ ; on le remplit quand même pour ne pas casser la contrainte.

Lot V-2 — Onglet XR dans manager-app — 4-5 j

C'est le lot le moins cher : l'écran kiosk est un patron directement réutilisable, la seule différence de fond est l'AppType filtré.

  • lib/Screens/Vr_devices/vr_screen.dart — clone de kiosk_screen.dart : grille de casques, pincode d'appairage en tête, état connecté/déconnecté
  • Réutiliser tel quel device_element.dart, dropDown_configuration.dart (assignation d'une configuration par casque), change_device_info_modal.dart
  • Brancher main_screen.dart:704 : remplacer Text("TODO vr") par VrScreen(applicationInstanceDTO: applicationInstanceVR)
  • Carte casque : ajouter niveau de batterie + AppVersion + LastSeen (colonnes déjà en base, jamais affichées pour le kiosk)
  • manager_api_new : ajouter appType à device_dto.dart / device_detail_dto.dart et VrApp à api_key_app_type.dartà la main, la génération OpenAPI ne se relance pas sur ce repo
  • i18n FR/EN/NL/DE : titre d'onglet, « casques », « aucun casque appairé », libellés batterie/version

Rien à faire pour le sous-menu ni pour les stats : les deux sont déjà branchés sur isVR / AppType.VR.

Lot V-3 — Contrat de contenu pour un client non-Flutter — 1-2 j

Unity ne peut pas consommer le client Dart généré. Deux options :

Option Coût Verdict
Unity appelle les endpoints REST un par un, DTO C# écrits à la main Élevé et récurrent — chaque évolution de section se répercute
Unity appelle GET /api/configuration/{id}/export (un appel, tout le contenu) et désérialise ExportConfigurationDTO Faible — le DTO d'export est déjà stable, c'est celui de l'import/export back-office retenu
  • Vérifier que Export répond bien à une clé d'API d'app (aujourd'hui utilisé depuis le back-office authentifié)
  • Documenter le JSON d'export comme contrat public versionné (c'est aussi ce qui servira au mode offline du casque)
  • Décider si l'export multilingue (aujourd'hui ?language=) doit renvoyer toutes les langues d'un coup pour la VR — un casque en borne change de langue à chaud

Lot V-4 — L'app Unity + Meta XR SDK — 6-10 semaines (POC utile : 3-4 semaines)

  • Socle : Unity 6 LTS, URP, XR Plugin Management → OpenXR + Meta XR feature group, Meta XR Core SDK + Interaction SDK (raycast contrôleur et hand tracking)
  • Couche réseau C# : appairage par pincode (clone du flux tablet-app/lib/Screens/Configuration/config_view.dart:274), récupération de la clé d'API, POST /api/device avec appType = VR et l'identifiant matériel du casque
  • Cache disque + offline-first : une borne d'accueil perd le wifi ; l'export JSON et les médias se mettent en cache, l'app démarre sur le cache et se rafraîchit en tâche de fond
  • Renderer par type de section — arbitrage tranché, voir §8
  • Lecture 360° : dépend du lot « Ressource 360° » de todo-features.md — sans un type de ressource 360°, il n'y a rien à afficher en immersif
  • Mode borne : verrouiller le casque sur l'app (Meta Horizon Managed Solutions ou un MDM tiers), relance auto au réveil, timeout de session, retour au menu à la repose du casque
  • Télémétrie : POST de VisitEvent avec AppType.VR → les stats manager-app fonctionnent sans une ligne de plus
  • Distribution : Meta Horizon Store (review) ou canal privé / sideload via MDM

Lot V-5 — Supervision de la flotte — 1-2 semaines

  • Client MQTT dans Unity (MQTTnet) : heartbeat, batterie, abonnement à player/{deviceId} — le publish côté serveur existe déjà (DeviceController.cs:303)
  • Alternative pour le POC : polling de l'export toutes les N minutes. Moins élégant, 10× moins cher, suffisant pour 1-3 casques
  • Remontée batterie/version/dernier vu (colonnes déjà présentes sur Device)

3. Estimation

Lot Charge Dépendances
V-1 — Device multi-canal (backend) 2-3 j
V-2 — Onglet XR manager-app 4-5 j V-1
V-3 — Contrat de contenu 1-2 j
Sous-total « back-office XR prêt » 8-12 j
V-4 — App Unity (POC : menu + vidéo 360 + une section) 3-4 sem. V-1, V-3, Ressource 360°
V-4 — App Unity (couverture complète) 6-10 sem. idem
V-5 — Supervision flotte 1-2 sem. V-4
Total avec app complète ~3-4 mois d'un dev

Le découpage est volontaire : les 8-12 jours du back-office XR se tiennent debout tout seuls. Ils rendent le canal VR administrable et démontrable (flotte, assignation de config, stats) avant d'engager le vrai coût, qui est l'app Unity.


4. Risques

  1. Le contenu du CMS est 2D. C'est le risque produit majeur, pas un risque technique. Un Article affiché sur un panneau flottant en VR est moins bon qu'une tablette. La valeur du canal vient du contenu 360°, que très peu de clients possèdent. Sans un client pilote qui a déjà des vidéos 360°, le POC démontrera une régression.
  2. Nouvelle stack, zéro mutualisation. Unity/C# XR ne réutilise ni Flutter, ni le client API généré, ni myinfomate_layout. C'est un quatrième front à maintenir, pas une déclinaison.
  3. Économie du MDM. Rétrogradé le 2026-08-31. Un MDM XR (~15-20 €/casque/mois) n'est nécessaire qu'à l'échelle d'une flotte. À 1-3 casques, sideload adb + le mode appareil intégré suffisent. Ne redevient un sujet qu'au-delà d'une dizaine de casques. ⚠️ Les noms et l'état des programmes business de Meta bougent vite — à revérifier avant de l'écrire dans une offre.
  4. Exploitation en borne publique : hygiène des casques, sessions courtes, présence de personnel. C'est un frein opérationnel réel côté client, indépendant de la qualité de l'app.
  5. La landing promet déjà « Meta Quest — Bientôt » en 4 langues (myinfomate-landing, translations.ts, clé mode4Desc). L'écart annonce/réalité existe depuis un moment ; il se creuse tant que rien n'est livré.

5. Recommandation de séquencement

  1. V-1 + V-2 + V-3 (~2 semaines) — peu cher, sans risque, rend le canal administrable, ferme le TODO vr.
  2. Ressource 360° (déjà en V2, le plus petit des trois chantiers médias) — prérequis dur du POC.
  3. Décision go/no-go conditionnée à un client pilote avec du contenu 360° existant. Sans lui, ne pas démarrer V-4.
  4. POC Unity (3-4 semaines) — menu immersif + vidéo 360 + une section riche.
  5. V-4 complet et V-5 seulement après validation du POC en conditions réelles.

§6 à §10 ajoutés le 2026-08-31, seconde passe. Tout ce qui suit est vérifié dans le code — c'est de l'analyse déjà faite, à ne pas refaire.


6. Pricing — add-on « Contenu immersif », à partir de 70 €/mois

Arbitré par Thomas le 2026-08-31. Les chiffres encore inscrits dans ../roadmap.md+30-50€/mois (L113) et +40€/mois dans le bundle (L148-149) — sont périmés et à réécrire.

Historique de la décision : l'assistant avait proposé un palier « Immersif » à 299 €/mois en remplacement de l'add-on. Arbitrage retenu : on garde un add-on, revalorisé de 40 à 70 €/mois et plus. Le raisonnement du palier est conservé ci-dessous parce que son principe structurant, lui, est validé — c'est le montant et le véhicule qui changent.

Le principe qui reste vrai : décorréler du casque

Un add-on « VR » ne se vend que le jour où l'app Unity existe et où le client achète un casque. Un add-on « Contenu immersif » vend le contenu — image 360, vidéo 360, modèles GLB — qui s'affiche déjà dans le web, le mobile et le kiosk. Le casque devient le haut de gamme de l'add-on, pas sa condition d'existence. Un client souscrit et en a pour son argent sans casque.

Pourquoi l'add-on bat le palier — le point que l'assistant avait manqué

Un add-on s'accroche à n'importe quel palier. Le palier à 299 € forçait tout le monde à passer par Premium ; l'add-on permet à un client Pro à 99 € de prendre du 360 sans payer l'assistant IA dont il ne veut pas → 169 € au lieu de 299 €. Marché adressable nettement plus large.

Sur le total, les deux modèles convergeaient de toute façon : Premium 179 + 70 = 249 €, soit exactement le plancher de négociation cité pour le palier. Le désaccord portait sur la structure, pas sur le montant.

Paliers de référence (source de vérité : myinfomate-landing, HomeClient.tsx:534,583 + segments.ts) : Essentiel 39 / Pro 99 / Premium 179 / Enterprise sur devis.

Ce que l'add-on débloque

Ressources image 360, vidéo 360, modèles GLB avec POI cliquables texte/audio — et l'app Quest quand elle sortira. L'assistant IA n'est pas dedans : il reste attaché à son propre add-on / au palier Premium.

⚠️ Le stockage — plus aigu en add-on qu'en palier

C'est le seul point de vigilance qui s'aggrave avec ce modèle. Un client Pro à 99 € a un quota dimensionné pour du Pro, et une vidéo 360 de 5 min en qualité correcte pèse des Go.

➡️ L'add-on doit porter sa propre enveloppe de stockage, explicite et finie, avec facturation du dépassement. Sans ça, le premier client 360 sur un petit palier fait sauter la marge.

Ce qui reste HORS de l'add-on

Exclusion Pourquoi
Modélisation 3D Coût studio, 3 à 15 k€, variable — reste sur devis
Hardware casque Achat client
Stockage au-delà de l'enveloppe Voir ci-dessus — plafond dur + facturation du dépassement

Onboarding one-shot : 1200-1500 € quand il y a des casques (provisioning, config borne, formation, journée sur site). Sans casque, l'add-on ne justifie pas de frais de mise en place particuliers.

Implémentation — une journée de backend

Vérifié dans le code, et c'est la bonne surprise. L'Instance recopie localement les valeurs du plan au lieu de les lire à travers : StorageQuotaBytes, AiTokensPerMonth, HasStats, StatsHistoryDays, HasAdvancedStats sont tous des colonnes de Instance (Data/Instance.cs:87-98). Le SubscriptionPlan n'est qu'un gabarit.

Et IsAssistant (Data/Instance.cs:37) est déjà exactement ce patron : un add-on = un bool sur l'Instance.

  • Instance.HasImmersiveContent (bool) + migration
  • Relèvement de StorageQuotaBytes à l'activation
  • Exposition dans InstanceDTO + l'écran SuperAdmin

➡️ Pas de nouvelle ligne de plan, pas de table de jointure, pas de refonte de la facturation.

Et ça tombe sur une carte déjà planifiée : « Écran SuperAdmin — add-on IA & canaux d'une instance » (kanban, colonne Planifié), qui dit explicitement « à faire d'un bloc avec l'endpoint de mise à jour d'ApplicationInstance : c'est le même écran ». L'add-on immersif y prend sa place sans rien ajouter au backlog.

⚠️ Réserve Stripe : le webhook ne gère que checkout.session.completed et invoice.payment_failed (Controllers/StripeWebhookController.cs:62-65), rien sur les lignes d'abonnement. Facturer l'add-on automatiquement demandera du travail. Pour 1-3 clients pilotes, une ligne ajoutée à la main dans Stripe suffit.


7. Assistant IA dans le casque — possible, et quasi gratuit

Vérifié dans le code. POST /api/Ai/chat (Controllers/AiController.cs:367) prend déjà un AppType dans sa requête (DTOs/AiChatDTO.cs:10) et se contente de vérifier que l'ApplicationInstance de cet AppType a IsAssistant = true (AiController.cs:387).

➡️ Créer l'ApplicationInstance VR avec le flag suffit. Zéro ligne de backend.

Deux nuances :

  • Pas de fallback pour VR. Le fallback sur Mobile n'existe que pour AppType.Voice (commentaire AiController.cs:387). Sans ApplicationInstance VR, c'est un 403.
  • Le TTS est côté client. L'instance ne stocke qu'un nom de voix Gemini (Data/Instance.cs:54), la synthèse est faite par l'app. Unity devra appeler Gemini TTS lui-même et gérer le micro pour le STT. L'architecture est prouvée par le POC Ray-Ban (11 implémentations de moteurs, branche Meta-Rayban-Test) mais c'est du Dart : réimplémentation en C#, pas un portage.

8. Types de section en VR — arbitrage tranché

Sur les 13 types existants :

Verdict Types Pourquoi
Nécessaire Menu C'est le hub flottant lui-même
Gagne vraiment Video (360 en skybox), Slider (galerie immersive), Map (le plan posé devant soi comme une maquette — c'est là qu'arrive le GLB), Parcours (chaque étape = un lieu autour de soi) La VR ajoute une dimension que l'écran n'a pas
Utile en borne Event / Agenda « Ce qui se passe aujourd'hui » en écran d'attente d'office de tourisme — c'est même l'usage principal
Correct Quiz Jouable au regard ou au contrôleur, bon pour l'engagement
À ne pas porter Article, PDF, Web, Weather, Game Un mur de texte flottant est moins bon qu'une tablette. Une WebView en VR est frustrante

Scope du POC : Menu + Video 360 + Slider + Map/GLB, avec Event en écran d'attente. Quatre types, et c'est exactement la démo.


9. POI sur modèle GLB — le modèle de données existe déjà à 80 %

La plus grosse économie du chantier, repérée le 2026-08-31.

GeoPoint (Data/SubSection/SectionMap.cs:85) porte déjà Title, Description, ImageResource, et surtout Contents — une List<Content> où chaque Content (Data/SubSection/Content.cs) est titre + description + Resource, donc audio. Le tout multilingue via TranslationDTO.

➡️ Un POI sur GLB, c'est le même objet avec une position locale (x,y,z) au lieu d'une lat/lon.

  • GeoPoint.Model3DPosition nullable (x,y,z + rotation) à côté du Geometry PostGIS existant
  • SectionModel3D référençant une ressource GLB
  • ResourceType.Model3Den fin d'enum, il est persisté en int
  • Éditeur de placement de POI en 3D dans manager-app — le seul morceau non trivial

Le back-office réutilise l'éditeur de POI existant ; le multilingue et l'audio suivent gratuitement.


10. Unity ou WebXR — la question tranchée

Unity n'est PAS une nécessité technique

La distinction qui compte : le mode kiosk ne vient pas du moteur, il vient de la gestion d'appareil. Un MDM ou le mode appareil de Meta verrouille le casque sur n'importe quelle app installée — build Unity, APK Godot, ou navigateur épinglé sur une URL. Le choix du moteur est orthogonal au verrouillage kiosk.

Routes possibles vers un APK installable sur Quest : Unity + Meta XR SDK, Unreal, Godot 4 + OpenXR, ou du WebXR empaqueté (Meta supporte l'installation de PWA sur Horizon OS). ⚠️ Sur ce dernier point : hors date de connaissance de l'assistant, à revérifier avant tout engagement.

Ce qui plaide pour Unity — l'exploitation, pas la technique

  • Média offline : plusieurs Go de vidéo 360 en cache navigateur se heurtent aux quotas de stockage. En natif on écrit sur le disque.
  • Décodage vidéo : une 360 en 8K veut le décodeur matériel et le contrôle dessus.
  • Prévisibilitél'argument le plus fort : une borne tourne 8 h/jour sans personne. Un build Unity est figé ; le navigateur du casque se met à jour tout seul et peut casser la borne un mardi matin.
  • Accessoirement : MQTT en tâche de fond, hand tracking riche, porte du Horizon Store.

Ce qui plaide pour WebXR — à ne pas balayer

visitapp-web rend déjà les 13 types de section en React 19 / Next 16 (src/components/sections/, 13 fichiers, aucune dépendance 3D pour l'instant). Une couche WebXR par-dessus attaque frontalement le risque n°2 (« nouvelle stack, zéro mutualisation ») : c'est potentiellement un quatrième front en moins à maintenir.

Position retenue

WebXR pour la maquette et la spec visuelle, Unity pour la borne en production. Pas parce que le web ne peut pas, mais parce qu'une borne publique non surveillée pardonne mal. ➡️ À rouvrir si l'usage devient « le visiteur met le casque 5 minutes avec un agent à côté » — là, le web redevient le bon choix.

Ce que l'assistant peut et ne peut pas produire

Peut : le projet Unity complet — scripts C#, client API, script d'éditeur qui construit la scène par code (plutôt que du YAML .unity écrit à la main, illisible et cassant), manifest.json avec les packages Meta XR, menu flottant, swap de skybox 360, chargement GLB, raycast sur POI avec audio.

Ne peut pas : ⚠️ Unity n'est pas installé sur cette machine — vérifié le 2026-08-31, ni Program Files/Unity, ni Unity Hub. Donc pas de compilation, pas d'APK, pas de rendu, pas de casque de test. Le livrable est un projet plausible : il faut l'ouvrir et itérer sur les erreurs de compilation. Compter 2 à 4 allers-retours, surtout sur le SDK Meta dont les API bougent entre versions.

Pour voir le rendu, la maquette WebXR/three.js est le chemin court : visible sur écran ou dans le navigateur du Quest en immersif, itérable en secondes. Précédent dans le repo : la démo villa d'Echternach (visitapp-web/src/app/demo/villa-echternach/scene.ts), rendu procédural fait exactement pour montrer au client.