From 9b7ec7483198b74c8184efe7b7978a74f4f77d9b Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 22 Jul 2026 09:08:44 +0200 Subject: [PATCH] =?UTF-8?q?chore(tickets):=20cl=C3=B4ture=20du=20ticket=20?= =?UTF-8?q?#92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket #92 (ne pas couper la musique avec les chronos de step) validé GO par QA, passé en statut closed. Co-Authored-By: Claude Opus 4.8 --- .ideai/tickets/92/carnet.md | 130 +++++++++++++++++++++++++++++++++++- .ideai/tickets/92/issue.md | 8 +-- .ideai/tickets/index.json | 4 +- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/.ideai/tickets/92/carnet.md b/.ideai/tickets/92/carnet.md index 968c142..bfd8a86 100644 --- a/.ideai/tickets/92/carnet.md +++ b/.ideai/tickets/92/carnet.md @@ -1,6 +1,130 @@ --- issueRef: "#92" -version: 2 -updatedBy: {"kind":"user"} -updatedAt: 1784701920855 +version: 4 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784704108458 --- +# Architect — Cadrage technique #92 + +## Problème + +Le bip des chronos de step (`audio/step_countdown_short.wav`, `audio/step_completion_long.wav`) est joué par `AudioplayersExerciseStepAudioCuePlayer` dans `lib/presentation/exercise_step_audio.dart` via `audioplayers`. + +Avec `audioplayers 6.8.1`, le contexte audio par défaut côté Android utilise `AudioContextAndroid.audioFocus = AndroidAudioFocus.gain`. Ce focus exprime que l'app devient la source audio principale, ce qui peut mettre Spotify ou une autre app audio en pause au moment des bips. + +## Décision minimale + +Configurer explicitement le player des bips comme un son court de sonification qui se mélange avec l'audio déjà en cours, sans réclamer de focus exclusif. + +Contrat à appliquer : + +```dart +final stepCueAudioContext = AudioContext( + android: const AudioContextAndroid( + contentType: AndroidContentType.sonification, + usageType: AndroidUsageType.assistanceSonification, + audioFocus: AndroidAudioFocus.none, + ), + iOS: AudioContextIOS( + category: AVAudioSessionCategory.playback, + options: const {AVAudioSessionOptions.mixWithOthers}, + ), +); +``` + +Raisons : + +- Android `AndroidAudioFocus.none` ne demande pas de focus audio ; les bips se mixent avec la musique au lieu de la couper. `gainTransientMayDuck` est acceptable techniquement si l'on veut baisser temporairement la musique, mais ce n'est pas le contrat MVP : l'utilisateur demande de ne pas interrompre la musique, donc `none` est plus direct. +- Android `contentType.sonification` + `usageType.assistanceSonification` décrit mieux un bip UI/timer que les valeurs par défaut `music/media`. +- iOS `playback + mixWithOthers` conserve l'intention actuelle d'un son audible tout en autorisant le mix avec les autres apps. `ambient` mixerait aussi, mais changerait davantage le comportement car cette catégorie est silencée par le switch silencieux/verrouillage ; ne pas l'utiliser pour le MVP sans décision produit. + +## Où appliquer la configuration + +Fichier concerné : `lib/presentation/exercise_step_audio.dart`. + +Appliquer le contexte sur le `AudioPlayer` dédié aux bips, pas via `AudioPlayer.global.setAudioContext` côté Dart, afin de limiter le changement au player de `ExerciseStepAudioCuePlayer` sur Android. + +Forme recommandée : initialisation lazy et await avant la première lecture, car `setAudioContext` est async et le constructeur ne peut pas l'attendre. + +```dart +final class AudioplayersExerciseStepAudioCuePlayer + implements ExerciseStepAudioCuePlayer { + AudioplayersExerciseStepAudioCuePlayer(); + + final AudioPlayer _player = AudioPlayer(); + late final Future _audioContextReady = _player.setAudioContext( + AudioContext( + android: const AudioContextAndroid( + contentType: AndroidContentType.sonification, + usageType: AndroidUsageType.assistanceSonification, + audioFocus: AndroidAudioFocus.none, + ), + iOS: AudioContextIOS( + category: AVAudioSessionCategory.playback, + options: const {AVAudioSessionOptions.mixWithOthers}, + ), + ), + ); + + Future _play(String assetPath) async { + await _audioContextReady; + await _player.stop(); + await _player.play(AssetSource(assetPath)); + } +} +``` + +Note iOS importante : `audioplayers_darwin` indique que iOS ne permet pas réellement un contexte audio spécifique par player ; `player.setAudioContext` applique donc la session audio globalement comme `AudioPlayer.global.setAudioContext`. C'est une contrainte plateforme acceptable ici, mais elle doit être connue : si d'autres players audio sont ajoutés plus tard, ils hériteront potentiellement de cette session globale côté iOS. + +## Frontière architecture + +Cette correction reste en `presentation`, dans l'adapter concret qui joue les sons UI. Pas de modification domain/application : le domaine ne connaît ni Spotify, ni focus audio, ni `audioplayers`. + +Ne pas créer de port supplémentaire : `ExerciseStepAudioCuePlayer` existe déjà et suffit. Le contrat métier ne change pas (`playShortCountdownBeep`, `playLongCompletionBeep`) ; seule la politique d'interaction audio de l'implémentation concrète change. + +## Tests + +Un test automatisé Flutter standard ne peut pas prouver que Spotify/YouTube Music continue de jouer : le comportement dépend du focus audio natif Android/iOS et d'une app tierce en arrière-plan. + +Tests utiles : + +- Test manuel Android obligatoire : lancer Spotify en arrière-plan, démarrer une step avec chrono, vérifier que les bips des 3 dernières secondes ne mettent pas la musique en pause et que la musique continue après le bip de fin. +- Test manuel iOS si environnement disponible : même scénario avec Apple Music/Spotify. +- Test automatisé optionnel seulement si peu coûteux : vérifier que l'implémentation appelle `setAudioContext` avant `play`. Ne pas introduire une abstraction lourde uniquement pour ce test ; la valeur principale est le test manuel sur device. + +## Lot de travail + +### F1 — Correction player audio step + +- Modifier `lib/presentation/exercise_step_audio.dart` pour appliquer le `AudioContext` ci-dessus sur le player dédié. +- Garder les assets et les méthodes publiques inchangés. +- Vérifier que les bips fonctionnent toujours dans l'exécution de séance. +- Documenter dans le retour DevFrontend que la validation réelle est manuelle sur device avec une app musicale en arrière-plan. + +Aucun lot backend, aucune migration, aucun impact sync. + +# DevFrontend — Implémentation F1 + +## Réalisé + +- `AudioplayersExerciseStepAudioCuePlayer` configure maintenant un `AudioContext` sur son `AudioPlayer` dédié avant la première lecture. +- Android : `AndroidAudioFocus.none`, `AndroidContentType.sonification`, `AndroidUsageType.assistanceSonification`. +- iOS : `AVAudioSessionCategory.playback` avec `AVAudioSessionOptions.mixWithOthers`. +- Aucun appel à `AudioPlayer.global.setAudioContext` ajouté ; la recherche du code ne montre pas d'autre usage `AudioPlayer` dans l'app. +- Assets et méthodes publiques inchangés : `playShortCountdownBeep`, `playLongCompletionBeep`, `dispose`. + +## Fichier touché + +- `lib/presentation/exercise_step_audio.dart` + +## Vérifications DevFrontend + +- API vérifiée dans le package installé `audioplayers 6.8.1` / `audioplayers_platform_interface 7.2.0`. +- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze lib/presentation/exercise_step_audio.dart` : OK, aucune issue. +- `HOME=/tmp /opt/flutter/bin/flutter test --no-pub test/presentation/workout_execution_screen_test.dart` : non exécutable dans ce sandbox, le wrapper Flutter tente d'écrire dans `/opt/flutter/bin/cache` en lecture seule. + +## Écarts / limites + +- Le comportement "ne coupe pas Spotify/Apple Music" ne peut pas être prouvé par test widget standard ; validation manuelle requise sur device Android, et iOS si disponible. +- Côté iOS, `audioplayers_darwin` applique `player.setAudioContext` à la session audio globale par contrainte plateforme, comme cadré par Architect. +- L'outil `idea_ticket_update_carnet` n'était pas exposé dans cette session ; résumé ajouté directement au fichier carnet du ticket. diff --git a/.ideai/tickets/92/issue.md b/.ideai/tickets/92/issue.md index ac9e032..5ce0888 100644 --- a/.ideai/tickets/92/issue.md +++ b/.ideai/tickets/92/issue.md @@ -2,15 +2,15 @@ id: "048920d0-e0e0-4f5c-a0f9-d59b22a9c4c4" number: 92 title: "Ne pas couper la musique avec les chronos" -status: "open" +status: "closed" priority: "high" sprint: null links: [] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] createdBy: {"kind":"user"} -updatedBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784701912384 -updatedAt: 1784701920855 -version: 2 +updatedAt: 1784704108458 +version: 4 --- Lorsqu'un chrono de temps d'une step arrive sur les 3 dernières secondes, le son produit ne doit pas couper la musique de l'utilisateur \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index b8a4b81..9bbccd4 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -1019,13 +1019,13 @@ "issueRef": "#92", "path": "92", "title": "Ne pas couper la musique avec les chronos", - "status": "open", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [ "57695b92-24d0-4876-837c-76116e70a6ae" ], - "updatedAt": 1784701920855 + "updatedAt": 1784704108458 }, { "issueRef": "#93",