Pendant mon stage de 3e année chez Miggle One j’ai travaillé sur trois projets principaux : un modèle super basique, une application de suivi des courses et une application de suivi du football, ainsi qu’un projet de courte durée portant sur l’utilisation de MediaMTX pour développer un système de diffusion avec équilibrage de charge. Cette expérience m’a permis d’approfondir mes connaissances dans l’utilisation du framework React et de TailwindCSS pour la stylisation, ainsi que de découvrir Google Cloud, notamment à travers Firebase.
Le modèle super basique
Présentation du projet
Vue d'ensemble sur le modèle super basique
Afin de me familiariser avec l’organisation et le fonctionnement des projets déjà mis en place dans l’entreprise, j’ai repris le modèle super basique, qui est un modèle déjà existant. Ce modèle sert de base pour le développement de futurs projets au sein de l’entreprise Miggle. L’interface comprend une barre de navigation composée du logo et du titre de la page à gauche, ainsi que d’un menu à droite, permettant aux administrateurs de gérer les utilisateurs de leur club (équipe) sous forme de backoffice. J’ai premièrement utilisé le fichier readme divisé en plusieurs étapes pour lancer le projet sur ma machine en clonant le projet dans un répertoire m’appartenant. Vers la fin du stage, j’ai ajouté des fonctionnalités comme l’adaptation du projet en application mobile, afin d’améliorer sa compatibilité ainsi que son adaptabilité sur différents appareils.
Configuration du projet
J'ai premièrement suivi le fichier Readme fourni dans le projet pour le lancer localement. Il contient des étapes sur la création d'un fichier .env.local pour stocker les variables confidentielles comme la clé API, le domaine d'authentification, l'identifiant du projet, le stockage ainsi que l'identifiant de l'application. Il contient aussi des commandes à utiliser pour initialiser le projet, comme npm run setup:firestore et firebase deploy --only rules.
Le projet utilise une infrastructure de gestion des secrets étant déjà mise en place via Miggle Vault, un projet central externe dédié au stockage sécurisé des secrets, ainsi que via Google Secret Manager. Cette architecture évite le stockage local des clés sensibles comme la clé AWS (Services web d'Amazon) et les clés de chiffrement. Les fonctions Firebase accèdent aux secrets grâce à des permissions IAM (Gestionnaire d'identité et d'accès) spécifiques.
Une configuration existante d’AWS SES (Service de mail simple) est également utilisée pour l’envoi de mails d’inscription ou de connexion. Celles-ci utilisent un domaine vérifié, des identifiants IAM dédiés et une configuration dans la région eu-west-2 (Londres). Il est important de spécifier la région dans la configuration, Firebase utilisant par défaut la région us-central-1, même quand ce n’est pas la région la plus proche de l’utilisateur. Des procédures de vérification permettent de contrôler la validité des identifiants et le bon fonctionnement des services.
Extraits importants du fichier readme
Commandes pour la compilation
Extrait du fichier cors.json
Pour pouvoir stocker et récupérer des données comme des images, des vidéos ou des documents, il est possible d’utiliser Google Cloud Storage. Son utilisation nécessite la configuration des règles CORS (Cross-Origin Resource Sharing) afin d’autoriser les requêtes du frontend (local et production). Cela permet de sécuriser et de rendre possibles les échanges entre l’interface web et le stockage cloud. Enfin, les fonctions Firebase doivent être compilées puis redéployées après chaque modification afin d’appliquer les changements à l’environnement d’exécution. Cette phase de lancement en local du projet m'a permis de travailler sur la l'apprentissage critique AC34.05 : Maîtriser l'hébergement et le déploiement d'applications.
Fonctionnalité d'auto-traduction
Paramètre dans le menu pour changer la langue
J’ai développé une fonctionnalité d’auto-traduction en exploitant des cadres de réflexion français et internationaux afin d’adapter les contenus aux différentes conventions linguistiques et culturelles. Un super admin peut définir la langue globale du site, qui est ensuite appliquée à tous les utilisateurs. Pour éviter la duplication des constantes pour chaque langue et garder le projet facile à maintenir, j’ai utilisé Google Cloud Translation API : une API de traduction intégrée dans Google Cloud. Elle est intégrée du côté serveur dans le fichier translateBatch.ts du dossier callable, qui traduit les contenus par lots.
Fonction applyContextOverride du fichier translateBatch.ts
Fonction principale du fichier translateBatch.ts
La fonction applyContextOverlay() corrige certaines traductions automatiques selon leur contexte d’utilisation afin d’éviter les ambiguïtés liées à la traduction mot par mot. La fonction principale reçoit les textes à traduire depuis le client, les traite par lots afin d’optimiser les performances, puis renvoie les traductions obtenues.
Fonction collectTextNodes du fichier TranslationProvider.tsx
Extrait du fichier TranslationProvider.tsx
Fonction applyFrContextOverride du fichier TranslationProvider.tsx
Côté client, le provider TranslationProvider.tsx gère la traduction dynamique de l’interface en analysant le DOM (Document Object Model). Il contient une fonction collectTextNodes() qui récupère les nœuds de texte visibles en filtrant les éléments à ne pas traduire comme les balises HTML. Le mécanisme de contexte applyFrContextOverride() permet de corriger certaines traductions selon leur usage dans l’interface afin de limiter les ambiguïtés liées à la traduction mot par mot de l’API, comme le message de bienvenue et les éléments du menu.
Le composant principal met ensuite à jour l’interface sans rechargement de page, tout en synchronisant la langue enregistrée sur Firestore. Un MutationObserver* détecte les changements dans le DOM afin d’appliquer automatiquement les traductions aux nouveaux éléments, tandis qu’un mécanisme de contrôle empêche les appels simultanés à la fonction de traduction. Développer cette fonctionnalité m'a permis de travailler sur l'apprentissage critique AC34.04 : Concevoir et développer des composants logiciels, plugins ou extensions.
Adaptation du projet en application mobile
Application mobile
J’ai mis en place un système de déploiement automatique d’APK, un fichier d'installation d'une application Android contenant l'ensemble de ressources nécessaires à son exécution. Ce déploiement est déclenché à chaque nouvelle mise à jour publiée sur le dépôt GitHub du projet. Pour répondre à ce besoin, j’ai utilisé Capacitor, une technologie permettant de convertir une application web développée avec un framework en application mobile native multiplateforme. En produisant du code fonctionnel, sobre et réutilisable, cette solution, facile à intégrer en suivant sa documentation bien détaillée, permet de conserver une architecture React commune entre les versions web et mobile, sans avoir besoin de développer un projet mobile spécifique de zéro. Cette étape m'a permis de travailler les apprentissages critiques AC34.01 : Développer à l’aide d’un framework de développement côté client et AC34.04 : Concevoir et développer des composants logiciels, plugins ou extensions.
Release automatique de l'APK lors des mises à jour du projet
Le projet disposait déjà d’un workflow GitHub Actions permettant le déploiement automatique sur un domaine web. Pour déployer l’application mobile, j’ai repris ce principe en créant un nouveau workflow qui exécute les commandes nécessaires à la compilation et à la mise en release de l’APK.
Ce workflow gère de manière automatique l’ensemble du processus : installation des dépendances (npm run ci), compilation de l’application (npm run build), synchronisation du projet mobile (npm run cap sync android dans le cas d’une application Android, ou npm run cap sync ios dans le cas d’une application iOS), puis compilation des applications Android et iOS. Enfin, les fichiers générés sont automatiquement publiés sous forme de release sur GitHub. Le suivi du déploiement est accessible directement sur GitHub dans l’onglet Actions, permettant également de vérifier qu’il n’y ait pas d'erreurs dans la compilation. Pour tester l’application, un utilisateur ayant accès au projet peut se rendre sur le dépôt GitHub et télécharger directement l’APK depuis la section des releases. La mise en place de ce workflow m'a permis de valider l'apprentissage critique AC34.05 : Maîtriser l'hébergement et le déploiement d'applications tout en veillant à la sécurité des systèmes et des données, comme le fait de ne pas partager involontairement les données sensibles du fichier .env.
Extrait du workflow déployant un APK
Onglet Actions du répertoire du projet
Intégration d'une API Wordpress externe
Pages des actualités
Page d'une actualité
Pour aller plus loin dans l’évolution du projet, j’ai intégré une API WordPress externe permettant d’afficher dynamiquement des articles d’actualités. Ayant déjà travaillé avec des API WordPress lors de mon stage l’année dernière, cette étape a été relativement simple, même si elle avait été réalisée sur le framework Nuxt plutôt que sur React. Pour cela, j’ai repris l’API du site de Dorking Wanderers qui met à disposition des données publiques, tout en évitant de faire du scraping, une méthode consistant à directement récupérer des données sur un site web sans passer par sa source de données. Pour tester les endpoints, les points d'accès de l’API permettant de faire une requête, et identifier la correspondance entre les catégories et leurs identifiants, j’ai utilisé l’application Postman.
Vue d'ensemble de l'application Postman
Côté client, en produisant du code fonctionnel, sobre et réutilisable, j’ai créé un dossier news qui contient un fichier index.tsx qui permet d’afficher la liste des articles récupérés depuis l’API, et un fichier [postId].tsx qui gère l’affichage dynamique du contenu d’un article spécifique en fonction de son identifiant présent dans l’article que l’utilisateur clique sur la page index. Cette étape m'a permis de travailler sur l'apprentissage critique AC34.01 : Développer à l’aide d’un framework de développement côté client.
L’application envoie une requête HTTP vers les endpoints de l’API à l’aide d’un fetch. Les données sont reçues au format JSON, converties en objets TypeScript puis stockées dans des états React à l’aide d’un useState, avant d’être affichées dynamiquement dans l’interface. Cette approche permet d’afficher automatiquement les actualités à jour sans avoir à les stocker directement dans le projet React. La requête de la page index récupère la liste de tous les articles, tandis que celle de la page d’un article ([postId]) utilise l’identifiant du post cliqué en paramètre.
Une fois que les articles sont stockés dans l’état posts, ils sont affichés dynamiquement en utilisant un map(), qui génère un composant NewsCard pour chaque article. Des traitements sont également effectués pour formater les données, tels que la date, l’image de couverture et le contenu. Les cas où certaines informations sont manquantes sont également gérés afin de se rendre conforme aux standards du Web et aux normes d’accessibilité en ne laissant pas du vide incompréhensif sur la page.
Fonction de chargement des posts d'actualités
Composant d'affichage de la liste des actualités
Intégration des vidéos à la demande depuis un site externe
Aperçu d'une vidéo dans l'application
En utilisant le projet de vidéo à la demande de Miggle appelé ViewIt (dorking.miggle.video), j’ai créé une page séparée s’appelant MiggleTestPage.tsx englobant toutes les fonctionnalités liées à cette étape.
Fonction handlePlay de la page MiggletestPage.tsx
Côté client, l’application utilise des requêtes HTTP vers l’API de miggle.video afin de récupérer la liste des événements disponibles et de créer une session de lecture lorsqu’un utilisateur clique sur une vidéo. Cette logique est principalement implémentée dans la page MiggleTestPage.tsx, à travers la fonction handlePlay(), qui gère la sélection du type de flux selon qu’il s’agisse d’une vidéo à la demande (VOD) ou d’un direct (live), ainsi que la récupération de l’URL de streaming. Cette URL est ensuite transmise au lecteur basé sur hls.js, permettant la lecture des vidéos directement dans l’application.
En parallèle, la logique d’accès côté client est centralisée dans le hook useMiggleAccess.ts, qui interroge différentes APIs afin de vérifier si l’utilisateur dispose d’un abonnement actif au service ViewIt. Les fonctions serverless Firebase getMiggleEvents, createMiggleViewerSession et createMigglePlaybackSession complètent ce système en assurant la communication sécurisée avec l’API externe et le SDK (Software Development Kit) Miggle. getMiggleEvents permet notamment de récupérer la liste des événements vidéo disponibles via le SDK.
Extrait du fichier getMiggleEvents.ts
Extrait du fichier createMigglePlaybackSession.ts
Extrait du fichier createMiggleViewerSession.ts
Ces fonctions agissent comme une couche intermédiaire sécurisée entre l’application et les services externes, en protégeant les clés sensibles côté serveur et en garantissant un contrôle d’accès basé sur les tokens et l’authentification des utilisateurs. Ces fichiers proviennent d’un fichier readme déjà présent lors de mon arrivée, généré à l’aide de l’outil OpenAI Codex. J’ai par la suite complété ces fichiers pour les rendre compatibles avec le reste de la structure de ce projet.
Mise en place des abonnements mobiles via Stripe
Page des offres d'abonnement
Page Stripe
Pour permettre aux utilisateurs de l’application mobile d’acheter un abonnement sans passer par ViewIt, j’ai repris son intégration Stripe déjà présente et l’ai intégrée directement dans l’application. Capacitor fournit un navigateur intégré via le plugin @capacitor/browser, permettant d’ouvrir des pages web sans quitter l’application ni basculer vers le navigateur externe du téléphone. Cette approche permet une expérience utilisateur plus fluide en limitant les risques de confusion pendant le paiement.
La page PaywallScreen.tsx présente les différentes offres d’abonnement disponibles (audio ou vidéo, mensuel ou annuel). Lorsqu’un utilisateur sélectionne une offre, la fonction handleSubscribe() crée une session Stripe via une requête HTTP vers l’endpoint subscription-checkout du SDK Miggle. Une fois le paiement effectué, l’utilisateur est redirigé vers la page SubscriptionSuccessPage.tsx.
En parallèle, le composant SubscriptionSync.tsx, basé sur le hook useMiggleAccess.ts, vérifie côté serveur l’activation de l’abonnement afin de synchroniser les droits d’accès. L’ensemble s’inscrit dans l’architecture globale du SDK Miggle, qui centralise de manière sécurisée la gestion des échanges et l’accès aux services.
Extraits du fichier PaywallScreen.tsx
Mon retour sur ce projet
Ce projet m’a permis de me familiariser avec l’architecture utilisée dans les projets Miggle ainsi qu’avec leur organisation générale. En reprenant un projet déjà existant, j’ai appris à comprendre une base de code importante et à m’adapter à un environnement de développement professionnel en m’appuyant sur une veille technologique et des modèles de l’innovation.
Il m’a également permis de découvrir plusieurs technologies que je n’avais encore jamais utilisées, comme Firebase, Google Cloud et Capacitor. J’ai par la suite réutilisé Firebase et Google Cloud comme technologies principales dans les autres projets de mon stage, ce qui m’a permis d’approfondir progressivement mes connaissances. J’ai également eu l’occasion de reprendre une intégration existante de Stripe, ainsi que de réutiliser des notions déjà abordées en React et dans l’intégration d’APIs WordPress afin d’afficher du contenu dynamique en produisant du code fonctionnel, sobre et réutilisable.
Par ailleurs, j’ai appris à adapter une application web existante en application mobile sans repartir de zéro grâce à Capacitor, ce qui m’a permis de mieux comprendre les enjeux liés à la compilation et au déploiement sur mobile, ainsi que l’utilisation de plugins spécifiques pour accéder à certaines fonctionnalités natives, comme un navigateur interne. En favorisant la collaboration entre les parties prenantes du projet, l'abonnement et la diffusion des vidéos venant du SDK Miggle depuis le site Viewit ainsi que l'intégration permettent de réaliser des tests pour éventuellement collaborer avec un autre partenaire pour intégrer ses vidéos sur la plateforme Miggle et lui donner accès pour les diffuser sur son site. Cela m'a permis de travailler sur l'apprentissage critique AC35.04 | Défendre un projet de manière convaincante.
Enfin, j’ai gagné en autonomie dans la mise en place de nouvelles fonctionnalités, tout en prenant en compte les contraintes de maintenance, de sécurité et de déploiement propres à une application servant de base à de futurs projets de l’entreprise. Il m’a également sensibilisé à l’importance de garder une architecture simple, claire et réutilisable afin de faciliter les développements futurs. En m’appuyant sur des concepts théoriques issus de l’informatique et des sciences de l’information, j’ai également contribué à améliorer certains fichiers readme afin de les rendre plus compréhensibles pour des développeurs extérieurs à l’entreprise qui seraient amenés à rejoindre le projet ou à travailler dessus par la suite. J'ai travaillé sur l'apprentissage critique AC35.02 : Maîtriser la qualité en projet Web ou multimédia.
RC Racing
Présentation du projet
Page d'accueil de RC Racing
RC Racing est une interface web développée avec React permettant d’afficher en temps réel le classement d’une course de voitures télécommandées. L’application se connecte à un relais WebSocket servant d’intermédiaire entre le front-end et les flux de données LiveRC. Ce relais se connecte à une page LiveRC de type scoring. Comme pour les autres projets de Miggle, Firebase est utilisé pour gérer l’authentification ainsi que le stockage des données, notamment les photos de profil et l’historique des courses. Les informations reçues sont ensuite affichées dynamiquement sous forme de classement en temps réel. L’application inclut également un chronomètre de course ainsi que l’historique des tours.
Afin de s’adapter au fait que les courses réelles ont principalement lieu les week-ends, généralement en soirée en raison du décalage horaire avec les États-Unis, un relais de scénarios simulés a été mis en place afin de tester des comportements de course différents sans dépendre de données réelles. En produisant du code fonctionnel, sobre et réutilisable, cette architecture permet de dissocier la logique d’affichage du flux de données et d’offrir une plus grande flexibilité pour les tests et la visualisation en direct.
Relais des scénarios
Le relais simulé repose sur un serveur WebSocket, similaire au relais réel. Celui-ci diffuse en continu des paquets de données représentant l’état de la course aux clients connectés. Plusieurs scénarios de simulation ont été implémentés, tels que des dépassements fréquents, des phases sous drapeau jaune ou encore des fins de course, afin de reproduire des comportements réalistes.
Extrait du relais qui démarre le serveur
Mécanisme de heartbeat et gestion des messages clients
Initialisation des données
Extrait coeur de la fonction simulateRaceTrack
À chaque seconde, les positions des pilotes, les temps au tour, les écarts ainsi que l’historique des tours sont recalculés puis envoyés au client. Les données sont formatées de manière proche de la structure utilisée par LiveRC afin de faciliter une éventuelle intégration avec un flux de chronométrage réel. Le mock WebSocket permet ainsi de simuler localement le flux de données d’une course afin de développer et tester l’interface sans dépendre d’un relais ou d’un événement réel.
Il reproduit la communication attendue par l’application via des messages de type 2:: (heartbeat) et 5::: (données de course). La logique principale de simulation est implémentée dans la fonction simulateRaceTick(), qui constitue le cœur du moteur de course en mettant à jour l’état des pilotes à chaque tick. Plusieurs modes de fonctionnement sont proposés (steady, battle, caution, finish), permettant de reproduire différents comportements de course. L’ensemble assure une indépendance vis-à-vis des flux externes tout en conservant une structure de données identique à celle du système de production.
Exemple de commande pour déployer un relay
Le relais est ensuite déployé à l’aide de Google Cloud Run via la commande gcloud run deploy, permettant de déployer automatiquement le service sur l’infrastructure Google Cloud en veillant à la sécurité des systèmes et des données. Plusieurs paramètres peuvent être définis, comme la région d’hébergement, la mémoire allouée, le temps maximal d’exécution ainsi que les variables d’environnement nécessaires au fonctionnement de l’application, notamment l’URL de l’API Firebase utilisée par le relais. Une fois le déploiement terminé, l’URL publique associée au service est affichée dans le terminal, permettant aux clients de se connecter au relais WebSocket déployé. Ce relais constitue un moteur de simulation complet, reproduisant le flux temps réel attendu par l’application, tout en offrant un environnement contrôlé et reproductible pour le développement, les tests et la validation de l’interface. Cette partie du projet m'a permis de travailler les apprentissages critiques AC34.02 : Développer à l’aide d’un framework de développement côté serveur et AC34.05 : Maîtriser l'hébergement et le déploiement d'applications.
Relais final
Le relais final ne génère aucune donnée de course. Il agit comme une passerelle entre la source LiveRC et l’application. Il se connecte au flux externe, transforme les messages reçus afin de les adapter au format attendu par l’application, puis les redistribue en temps réel aux clients via WebSocket, sans stockage ni traitement métier supplémentaire.
Configuration permettant de se connecter à LiveRC
Fonction broadcastToClient qui diffuse des données au client
Fonction handleUpstreamDriverData qui permet la transformation des données
Connexion au flux LiveRC et gestion des événements des données en temps réel
La configuration runtimeConfig permet de définir dynamiquement les paramètres de connexion (serveur upstream, identifiants de course et mode d’accès), ce qui rend le relais adaptable à différentes courses ou environnements LiveRC. Une fois la connexion établie, le relais écoute les événements émis par le serveur upstream et reçoit en continu les mises à jour de course,notamment l’état du chronomètre, le classement et les données sur les pilotes.
Les données reçues sont ensuite traitées par des fonctions de transformation comme handleUpstreamDriverData() et handleUpstreamClockData(), qui assurent leur normalisation vers un format unique compatible avec l’application. Ces messages standardisés sont ensuite diffusés à l’ensemble des clients connectés via la fonction broadcastToClients(), garantissant une synchronisation en temps réel de l’interface. Ces fonctions forment une architecture en pipeline, allant de la source LiveRC jusqu’à l’affichage côté client, avec une étape intermédiaire de normalisation permettant à l’application d’utiliser les données de n’importe quelle course. En veillant à la sécurité des systèmes et des données, cette partie du projet m'a permis de travailler l'apprentissage critique AC34.03 : Développer des dispositifs interactifs sophistiqués.
Affichage des données côté client
Composant RaceGrid.tsx
Le composant RaceGrid.tsx affiche dynamiquement le classement des pilotes. Le tableau est généré automatiquement grâce à la méthode map(), qui parcourt les éléments d’un tableau et les transforme en éléments HTML/TSX. Dans ce cas, elle crée une ligne pour chaque pilote à partir des données reçues dans la page principale. L’utilisation des flèches permet de visualiser les changements de position, tandis que le meilleur tour de la session est affiché en vert pour le mettre en avant. Chaque ligne peut également être sélectionnée afin d’afficher des informations détaillées sur le pilote concerné sous forme d’overlay, comme sa photo ou son historique de tours.
Overlay d'un pilote
La page Home.tsx est le point central de l’application côté client. Elle gère à la fois la connexion au relais WebSocket, la réception des données temps réel et leur affichage dans l’interface utilisateur. Dès l’ouverture de la page, un WebSocket est créé vers le relais déployé sur Google Cloud Run afin de recevoir les informations de course en direct, en veillant à la sécurité des systèmes et des données.
En me conformant aux standards du Web et aux normes d’accessibilité, j'ai divisé l'interface en deux zones principales : le classement des pilotes et le panneau de détails. Les données de course sont d’abord traitées et enrichies côté front-end, puis injectées dans les composants d’affichage. Le composant RaceGrid.tsx gère le classement interactif, tandis que RacerOverlay.tsx affiche les informations détaillées du pilote sélectionné. J'ai travaillé sur les apprentissages critiques AC34.03 : Développer des dispositifs interactifs sophistiqués et AC34.04 : Concevoir et développer des composants logiciels, plugins ou extensions.
Fonctions de traitement côté client
Utilisation des composants d'affichage des données dans la page principale
Cœur de la fonction ParseData
Mon retour sur ce projet
Ce projet m’a permis de mettre en pratique mon expérience de l’utilisation des WebSockets dans une application, une technologie que j’avais déjà utilisée à l’IUT dans le cadre d’un projet de prise de notes collaborative. Au début, j’ai eu du mal à comprendre comment un WebSocket externe pouvait communiquer avec mon application, mais en utilisant des documents en ligne ainsi que Codex j'ai pu me débrouiller à créer deux relais pour la simulation et la source de données réelle.
La création, à l’aide de Codex, d’un relais fictif simulant les échanges de données m’a permis de mieux comprendre la structure des communications en temps réel avant d’intégrer un flux réel. Par la suite, j’ai pu adapter ce relais au vrai WebSocket en conservant la même logique. J’ai ainsi approfondi mes connaissances sur les communications en temps réel, ainsi que sur l’intégration et la transformation de données provenant de services externes. J'ai validé les apprentissages critiques AC34.01 : Développer à l’aide d’un framework de développement côté client et AC34.02 : Développer à l’aide d’un framework de développement côté serveur de manière globale en m’appuyant sur des concepts théoriques issus de l’informatique et des sciences de l’information, en utilisant les outils favorisant un développement itératif et collaboratif et en produisant du code fonctionnel, sobre et réutilisable.
Trackit
Présentation du projet
Page d'accueil de Trackit
Trackit est une application web permettant à ses utilisateurs de créer des équipes et des matchs afin de suivre les actions réalisées par chaque joueur (passes, tirs, buts, etc.). L’application permet également de publier du contenu multimédia, comme des photos ou des vidéos, automatiquement associées aux timestamps du match. À partir de ces contenus, il est ensuite possible de générer une diffusion regroupant automatiquement les différentes vidéos dans l’ordre chronologique.
Mes missions au sein de ce projet concernaient principalement l’implémentation de nouvelles fonctionnalités ainsi que la réalisation de tests de régression* afin de vérifier le bon fonctionnement de l’application après chaque ajout ou modification. En favorisant la collaboration entre les parties prenantes du projet, j’ai utilisé la méthode MoSCoW (Must Have = « Doit avoir », Should Have = « Devrait Avoir », Could Have = « Pourrait Avoir », Won’t Have = « N’aura pas ») pour classer les tickets du board Asana selon leur niveau d’importance afin de déterminer l'ordre de priorité à respecter. J’ai également veillé à limiter la création de dette technique (technical debt), en trouvant des solutions durables plutôt que d’utiliser des correctifs légers pouvant poser problème à long terme. ce découpage m'a permis de travailler sur les apprentissages critiques AC35.01 : Piloter un produit, un service ou une équipe et AC35.02 : Maîtriser la qualité en projet Web ou multimédia.
Le projet étant initialement développé entièrement en JavaScript, j’ai participé à sa migration progressive vers TypeScript, technologie utilisée dans les autres projets Miggle. Cette transition vise à réduire la dette technique tout en favorisant la sobriété numérique et en produisant du code fonctionnel, sobre et réutilisable.
Mise en ligne des médias volumineux
Déclenchement de la fonction Firebase
Vérification et validation du fichier
Détermination du temps réel de capture du média
Association du temps de la vidéo au temps de match
Création de l'événement média dans le Firestore
Au début, l’API ne permettait que l’ajout de fichiers de moins de 30 Mo de poids. Souhaitant représenter des matchs entiers d’au moins 90 minutes, il était nécessaire de créer une fonction permettant de gérer l’ajout des gros fichiers. Pour cela, j’ai créé la fonction ProcessLargeFanUploads (“Traiter les lourdes mises en lignes des fans”).
Cette fonction est déclenchée automatiquement lors de la mise à jour d’un document de traitement de mise en ligne dans le Firestore. Elle permet de gérer les vidéos volumineuses de manière asynchrone, sans impacter l’interface utilisateur. Le fichier est d’abord vérifié et validé selon sa taille afin de déterminer s’il doit être traité par le pipeline de mise en ligne volumineuse.
Une fois le média accepté, son timestamp de capture est converti en temps de match afin de le synchroniser avec le déroulement de la rencontre. Cette conversion permet de positionner précisément la vidéo dans le match et de l’associer aux événements correspondants, afin de garantir son affichage au bon moment dans la diffusion fan.
Enfin, un événement média est créé dans le Firestore, contenant les métadonnées du fichier ainsi que sa position temporelle dans le match. Ce processus assure l’intégration automatique des contenus vidéo dans le système d’événements. Cette première phase m'a permis de travailler l'apprentissage critique AC34.04 : Concevoir et développer des composants logiciels, plugins ou extensions.
Interface de l'API de mise en ligne
Analyse et modération du contenu
La fonction analyseMediaContent.ts analyse automatiquement les médias téléversés dans Firestore à l’aide de Vertex AI, intégrée à Google Cloud. Elle génère une description de l’événement contenu dans le média grâce à un prompt adapté selon qu’il s’agisse d’une image ou d’une vidéo. Lorsqu’aucun lien avec le football n’est détecté, elle le signale.
Afin de s’assurer qu’aucune vidéo à caractère violent ou contenant du contenu pour adultes soit mise en ligne, j’ai mis en place un système de modération par la fonction moderateVideoContent.ts. Pour mettre en place ce système, j’ai créé un diagramme permettant de visualiser clairement les différentes étapes du traitement et la logique suivie depuis l’upload jusqu’à la validation finale du média.
Extrait du prompt d’analyse des médias
Exemple d’analyse de média
En m’appuyant sur des concepts théoriques issus de l’informatique et des sciences de l’information, la procédure d'analyse des vidéos s’appuie sur Google Cloud Video Intelligence et la Vision API afin de détecter d’éventuels contenus sensibles ou inappropriés, tels que la nudité ou la violence. Elle évalue également la qualité du média à partir de différents critères comme la résolution, le débit, le nombre d’images par seconde ou encore la stabilité de l’exposition. Pour les images, elle utilise Vision SafeSearch, l’analyse des métadonnées EXIF (Exchangable Image File Format) ainsi que des critères d’estimation de flou afin d’estimer à la fois les risques liés au contenu et la qualité visuelle.
Pour bien structurer mes idées avant de développer la modération des médias, j’ai réalisé un schéma représentant la procédure à suivre, me permettant de travailler sur l'apprentissage critique AC35.04 : Défendre un projet de manière convaincante. Ce schéma a été validé par mon tuteur de stage, qui m’a également donné des retours afin de m’orienter vers les améliorations à apporter. Bien que l’idée de réaliser des schémas ne soit pas une des premières idées à me venir en tête pour développer des nouvelles fonctionnalités, j’ai appris grâce à cette mission que c’est un bon réflexe à prendre, surtout pour montrer son idée à une autre personne tout en favorisant la collaboration entre les parties prenantes du projet.
Schématisation de la procédure de modération des médias
Média ayant été marqué comme “flagged” dans la liste des évènements
Ce système attribue un score compris entre 0 et 1, avec des incréments de 0,1, évaluant les risques liés au contenu et la qualité du média. Le statut final de modération est défini comme flagged lorsqu’un score de contenu sensible atteint ou dépasse le seuil de 0,5, ou lorsque la qualité du média est jugée insuffisante (poor). Dans les autres cas, le média reçoit le statut approved. Ce score est enregistré dans le document du média.
Le statut approved met automatiquement à jour les informations de modération dans Firestore. Les médias marqués comme flagged restent visibles par les administrateurs qui peuvent les valider ou les supprimer, s’affichant avec un fond rouge pour mieux les différencier des médias approuvés. Des vérifications sont mises en place pour vérifier que l’utilisateur connecté soit bien un administrateur afin de masquer leur affichage pour les autres personnes. Le développement de ce système de modération m'a permis de travailler sur l'apprentissage critique AC35.02 : Maîtriser la qualité en projet Web ou multimédia.
Document de média dans le Firestore
Fonction d’analyse d’une image
Mise à jour du document Firestore
Finalisation de la modération
Application des points de pénalité
Diffusion des supporters
À partir d’événements d’un match stockés dans Firestore, j’ai créé deux fichiers : processFanBroadcast.ts et createFanBroadcast.ts. Lorsqu’un document de diffusion est créé via la fonction createFanBroadcast, stocké dans le chemin matches/{matchId}/fanBroadcasts/{broadcastId}, la fonction processFanBroadcast analyse les événements du match dont le statut est approved, puis construit une timeline (chronologie) basée sur les timestamps fanTime. Cette procédure exclut bien évidemment les médias n’ayant pas été approuvés, que ce soit pour des motifs de qualité ou pour de la potentielle violence.
La timeline est ensuite découpée en segments exploitables pour le rendu vidéo afin d’éviter les timeouts (délais d'attente) dans l’utilisation des fonctions. Les timeouts sont mis en place pour ne pas surcharger le serveur, contribuant à la favorisation de la sobriété numérique. L’utilisation d’un temps de référence différent du temps officiel du match est importante, car un match est structuré en périodes, alors que des médias peuvent être capturés avant le début du match ou pendant les pauses entre les périodes. Ce système permet donc de positionner plus précisément les médias dans la diffusion globale indépendamment des pauses et de la structure temporelle officielle du match.
Création du document broadcast
Coeur de la fonction processFanBroadcastSegmentBatch
Selon l’appareil utilisé pour l’enregistrement des médias, les timestamps sont gérés différemment entre Android et Apple. Apple utilise le timestamp correspondant au début de l’enregistrement alors qu’Android utilise celui de la fin. Afin de corriger ce décalage, une vérification des métadonnées EXIF est mise en place pour appliquer une soustraction de la durée de la vidéo au timestamp d’un média provenant d’un appareil Android.
Les segments sont ensuite transformés en jobs Firestore, stockés dans matches/{matchId}/fanBroadcasts/{broadcastId}/segmentJobs/{jobId}. Chaque job est traité par la fonction processFanBroadcastSegmentBatch(), qui télécharge les médias depuis Firebase Storage, applique un rendu vidéo avec FFmpeg, puis génère des clips MP4 organisés en batchs dans fan-broadcasts/{matchId}/{broadcastId}/batches/. Une fois tous les batchs terminés, la fonction finalizeFanBroadcast() assemble les fichiers dans le bon ordre afin de produire une vidéo finale unique, stockée dans fan-broadcasts/{matchId}/{broadcastId}.mp4.
Ce système gère les erreurs avec une gestion basée sur les jobs, la validation des fichiers générés et des contrôles de taille et de durée des médias afin d’éviter les contenus invalides ou trop lourds. Enfin, la fonction createFanBroadcast() expose une API sécurisée qui vérifie les droits de l’utilisateur et empêche l’exécution parallèle de plusieurs générations pour un même match. Pour pouvoir créer des diffusions, l’utilisateur doit avoir un abonnement. Le système d’abonnement étant déjà en place à mon arrivée, je n’ai pas eu à le toucher. Cette phase m'a permis de travailler l'apprentissages critiques AC35.02 : Maîtriser la qualité en projet Web ou multimédia.
Bouton de création de diffusion sur la page d’aperçu d’un match
Lorsque les batchs ne sont pas disponibles, le système effectue un fallback automatique en reconstruisant la vidéo directement à partir des segments individuels. Les segments sont regroupés et triés par index afin de garantir l’ordre chronologique de la timeline, puis validés sur leur durée pour exclure les fichiers invalides ou corrompus. Une fois validés, les clips sont assemblés avec FFmpeg afin de produire la vidéo finale, qui est ensuite enregistrée dans Firebase Storage.
Tri des batchs dans la fonction finalizeFanBroadcast
Tri par index, validation des clips et assemblage avec FFmpeg pour générer la vidéo complète.
Intégration d’un overlay pour les buts
En complément du système de diffusion, j’ai ajouté un overlay dynamique s’affichant lorsqu’un joueur marque un but. Pour cela, j’ai développé le fichier renderGoalOverlay.ts, une fonction déclenchée automatiquement à la création d’un événement de type Goal dans le Firestore, sous le chemin matches/{matchId}/events/{eventId}. Afin de favoriser la sobriété numérique en évitant de générer et stocker des vidéos qui ne sont pas utilisées, cette étape ne s’exécute que lorsqu’une diffusion est en cours de création pour le match concerné.
Arguments FFmpeg pour la mise en forme de l’overlay
Extrait de la fonction renderGoalOverlay
L’overlay est généré via un pipeline FFmpeg et regroupe les informations du but (joueur, score, équipe, logo et photo). Il est ensuite enregistré dans Firebase Storage afin d’être réutilisé dans la diffusion finale et synchronisé avec la timeline via le champ fanTime, avec une durée de 5 secondes. Lorsqu’aucune vidéo source n’est disponible, l’overlay est rendu avec un fond noir. Le développement de cette fonctionnalité m'a permis de travailler sur l'apprentissage critique AC34.03 : Développer des dispositifs interactifs sophistiqués.
Overlay d’un but dans une diffusion
Publication Instagram
En reprenant un principe identique à la création de l’overlay des buts, j’ai développé un visuel servant de maquette pour la publication de posts Instagram. N’ayant pas eu accès aux services Meta suite à des problèmes de droits, je me suis entièrement occupé de la partie rendu dans l’application. J’ai cependant réalisé un schéma décrivant comment j’aurais rendu la partie back-end de cette fonctionnalité fonctionnelle. La réalisation de ce schéma m'a de nouveau permis de travailler sur l'apprentissage critique AC35.04 : Défendre un projet de manière convaincante.
Schématisation du fonctionnement d'une publication Instagram d'un événement
Le visuel contient un fond aux couleurs du club sélectionné par l’administrateur, accompagné de son logo, du score du match en temps réel, du nom des équipes, de la période ainsi que du temps de jeu quand l’événement a eu lieu. L'intégration de la mise en forme des publications Instagram m'a également permis de travailler sur l'apprentissage critique AC34.03 : Développer des dispositifs interactifs sophistiqués. Les données sont directement récupérées à partir de l’événement de but, dans lequel l’utilisateur peut publier l’événement sur Instagram.
Un système de publication sur Twitter était également prévu, mais n’a pas pu être réalisé en raison du temps trop limité du stage. Son fonctionnement aurait toutefois été différent de celui d’Instagram car plutôt que de s’appuyer principalement sur un visuel à publier en tant qu’image, les publications auraient été plutôt centrées sur du texte et des hashtags, pour répondre aux usages les plus répandus sur cette plateforme.
Publication d'événement sur Instagram
Mon retour sur ce projet
Ce projet, qui a occupé une grande partie de mon stage, m’a permis de découvrir le traitement des vidéos et des images à l’aide de fonctions Firebase. J’ai appris à manipuler ces médias afin de les traiter par lots puis de les assembler automatiquement dans une vidéo finale. J’ai également appris à générer des éléments vidéo pouvant être superposés à la vidéo principale.
Comme le projet avait déjà été développé avant mon arrivée, j’ai dû en comprendre l’architecture et le fonctionnement avant de pouvoir y apporter des modifications, de manière identique au projet sur le modèle basique. Cette expérience m’a permis de renforcer ma capacité à analyser une base de code existante, à identifier les composants concernés par chaque évolution et à m’adapter aux choix techniques déjà mis en place. Chaque modification était accompagnée de tests de régression afin de vérifier que les nouvelles fonctionnalités ne génèrent pas de bugs ou de dysfonctionnements dans les parties déjà fonctionnelles de l’application.
J’ai particulièrement apprécié travailler sur ce projet malgré les difficultés rencontrées lors des phases de correction de bugs. L’exemple principal concernait la résolution de problèmes de timestamps lors de la création des diffusions. En prenant du recul et en m’appuyant sur des outils comme Codex pour m’aider dans la réflexion, j’ai pu trouver des solutions, comme l’utilisation d’un timestamp propre à la diffusion afin de positionner correctement les vidéos dans celle-ci.
Si je devais refaire ce projet, je commencerais par convertir la partie existant en TypeScript plutôt que de la conserver en JavaScript, afin d’éviter les incohérences de syntaxe entre les deux et d’avoir une base de code plus uniforme dès le départ. Le choix initial était de ne pas modifier des fonctionnalités déjà fonctionnelles, mais je pense qu’une conversion complète en TypeScript, accompagnée de tests de régression à chaque étape, aurait permis de gagner du temps sur le long terme.
Diffusion d'équilibrage
Ce court projet de streaming vidéo en direct intègre un mécanisme d’équilibrage de charge sur des serveurs régionaux (load balancing). En complément des autres missions que j’ai réalisées, j’ai mis en place une diffusion avec équilibrage pour la région France et une diffusion directe pour le Royaume-Uni.
La lecture de flux vidéo s’effectue directement depuis un navigateur web, via les protocoles WebRTC WHIP pour la publication du flux vers un serveur de streaming, et WHEP pour la lecture du flux depuis le serveur. L’architecture repose sur une infrastructure distribuée composée de serveurs MediaMTX déployés sur plusieurs régions afin d’optimiser la latence et la disponibilité du service.
Côté client le fichier streamRouting.ts détermine automatiquement le serveur le plus proche de l’utilisateur grâce à une mesure de latence dans la fonction pickBestStreamEndpoint(). Lors du lancement d’un flux, l’application tente successivement plusieurs endpoints pour assurer la plus fluide diffusion.
Pour la partie France, l’équilibrage a été testé à l’aide de deux machines virtuelles Google Cloud (mediamtx-fr-nlb et mediamtx-fr-nlb-udp), configurées pour simuler une répartition de charge. Ces machines virtuelles sont visibles dans l’interface de Google Cloud Console.
Fonction pickBestStreamEndpoint du fichier streamRouting.ts
Liste des machines virtuelles servant pour la diffusion équilibrée
Certification Google Cloud Digital Leader
Cette partie du stage ne correspond pas à une mission de l’entreprise, mais à une préparation de la certification Google Cloud Digital Leader que j’ai réalisée en parallèle. Cette certification vise à mettre en avant la maîtrise des bases du cloud computing. Elle permet également de se former à l’utilisation des outils de Google Cloud afin de répondre aux besoins d’une entreprise. Enfin, elle propose des modules de sensibilisation à l’intelligence artificielle ainsi que des cas d’usage permettant d’identifier le service cloud le plus adapté à chaque situation. Chaque course est accompagnée de vidéos avec des questionnaires de compréhension à la fin de chaque partie pour évaluer mon niveau de compréhension pour chaque sujet traité.
Cette formation se conclut par un examen programmé à l’avance, permettant d’obtenir une certification valable pendant trois ans. J’ai passé cet examen, mais n’ayant pas réussi la première tentative et n’ayant pas eu assez de temps à la fin de mon stage pour le repasser, je n’ai pas obtenu la certification. Cependant, j’ai pu acquérir des connaissances qui m’ont permis de mieux comprendre l’utilisation de Google Cloud et qui ont été bénéfiques pour la réalisation de mon stage, notamment la compréhension des différentes offres de services, ainsi que la signification et l’usage des principaux outils proposés par la plateforme. Je prévois de le repasser par moi-même prochainement afin de renforcer mon profil pendant ma recherche d’emploi.
Mon bilan sur mon stage
Ce stage m’a permis d’acquérir des compétences techniques que je ne n’avais pas auparavant, comme l’utilisation de Google Cloud et de Firebase comme back-end. J’ai également renforcé mes connaissances en React pour le développement d’interfaces web dynamiques, ainsi qu’en TailwindCSS pour la stylisation, tout en rendant possible la diffusion des projets sous forme d’application mobile grâce à Capacitor, un outil nouvellement découvert, sans nécessiter une refonte complète du projet.
J’ai également pu mettre en pratique les connaissances acquises lors de la préparation d’une certification. J’ai appris à structurer et optimiser la gestion des données dans une application web interactive tout en maîtrisant les coûts de fonctionnement, notamment grâce à Firebase et son modèle de facturation à l’usage, qui incite à ne développer que les fonctionnalités nécessaires et à prioriser les tâches, contribuant ainsi à la réduction de la dette technique.
D’un point de vue plus personnel, ce stage m’a permis de continuer à sortir de ma zone de confort et de gagner en autonomie, malgré les difficultés rencontrées, notamment lors des phases de correction de bugs. Le travail en équipe avec mon tuteur de stage m’a aidé à apprendre de mes erreurs et à adopter de nouvelles méthodes de travail, comme la création de diagrammes, facilitant la compréhension et la communication autour du projet au sein de l’équipe. J’ai apprécié travailler sur les projets de Miggle car ils ne sont pas exclusivement centrés sur le développement web, et ils permettent d’intégrer des fonctionnalités proches de celles d’applications classiques, comme le montage des média ou la création de posts et événements. En réalisant ce stage, j'ai validé les compétences AC35.01 : Piloter un produit, un service ou une équipe, AC35.02 : Maîtriser la qualité en projet Web ou multimédia, AC35.04 : Défendre un projet de manière convaincante, AC34.01 : Développer à l’aide d’un framework de développement côté client, AC34.02 : Développer à l’aide d’un framework de développement côté serveur, AC34.03 : Développer des dispositifs interactifs sophistiqués, AC34.04 : Concevoir et développer des composants logiciels, plugins ou extensions et AC34.05 : Maîtriser l'hébergement et le déploiement d'applications.