chore(tickets): clôture du ticket #92
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 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,130 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#92"
|
issueRef: "#92"
|
||||||
version: 2
|
version: 4
|
||||||
updatedBy: {"kind":"user"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784701920855
|
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<void> _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<void> _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.
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
id: "048920d0-e0e0-4f5c-a0f9-d59b22a9c4c4"
|
id: "048920d0-e0e0-4f5c-a0f9-d59b22a9c4c4"
|
||||||
number: 92
|
number: 92
|
||||||
title: "Ne pas couper la musique avec les chronos"
|
title: "Ne pas couper la musique avec les chronos"
|
||||||
status: "open"
|
status: "closed"
|
||||||
priority: "high"
|
priority: "high"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: []
|
links: []
|
||||||
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
createdBy: {"kind":"user"}
|
createdBy: {"kind":"user"}
|
||||||
updatedBy: {"kind":"user"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784701912384
|
createdAt: 1784701912384
|
||||||
updatedAt: 1784701920855
|
updatedAt: 1784704108458
|
||||||
version: 2
|
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
|
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
|
||||||
@ -1019,13 +1019,13 @@
|
|||||||
"issueRef": "#92",
|
"issueRef": "#92",
|
||||||
"path": "92",
|
"path": "92",
|
||||||
"title": "Ne pas couper la musique avec les chronos",
|
"title": "Ne pas couper la musique avec les chronos",
|
||||||
"status": "open",
|
"status": "closed",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784701920855
|
"updatedAt": 1784704108458
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#93",
|
"issueRef": "#93",
|
||||||
|
|||||||
Reference in New Issue
Block a user