feat(audio): log AudioService.asyncError instead of swallowing it
`AudioService.asyncError` had ZERO subscribers app-wide. The plugin funnels
every asynchronous failure of its own observers into that stream and nowhere
else — `_observePlaybackState`, `_observeMediaItem` and `_observeQueue` each
wrap their whole body in `catch (e) { _asyncError.add(e); }`, and the artwork
path uses `.catchError(_asyncError.add)` — and a `PublishSubject` with no
listeners simply drops what it is given. The platform-side exception behind
"the media playback notification disappeared" was therefore being discarded
without a single log line, which is why that report arrives with no evidence
attached.
`observarErroresAudio` is a pure, injectable seam in `arranque_audio.dart`
(stream in, logger callback out), matching the seam convention this codebase
already uses for `esperarArranqueAudio`, `decidirAvanceCola` and
`debeReaplicarEcualizador`: the unit tests exercise the wiring with a plain
`StreamController`, never the real plugin. The default logger emits one
`[PluriWave]`-prefixed `developer.log` line at `level: 900`, the same level
and prefix `servicio_audio.dart` already uses, so one logcat filter catches
both.
Wired from `lib/main.dart`, not from `arranque_audio.dart`: main.dart is the
module that genuinely owns handler lifecycle — it is the only caller of
`AudioService.init`, `registrarHandler` and `ServicioAudioSession`, and both
the on-time and the degraded/timeout startup branches converge on its
`conectarHandler` closure. `arranque_audio.dart` owns only the timeout race
and the degraded loading shell; it never creates or registers a handler
(`alListo` is injected into it from main.dart), so it has no lifecycle to
hang a subscription on. Subscribing happens before `AudioService.init` — the
getter only touches a static subject — so nothing reported during the
MediaBrowser handshake is missed, and one subscription covers both paths.
The subscription is cancellable and its `cancel` is registered into the
handler via `registrarLimpiezaArranque`, mirroring the existing
`registrarHandler` / `registrarFuenteNavegacion` / `registrarFuenteMusicaLocal`
registration convention. `onTaskRemoved` — the only handler teardown in this
app — runs it, so the subscription cannot outlive what it instruments. The
dependency points bootstrap -> service, so `servicio_audio.dart` never has to
import the bootstrap module or the plugin's static stream.
Zero behaviour change: nothing but log output is added.
This commit is contained in:
@@ -55,6 +55,21 @@ Future<void> main() async {
|
||||
// permanently hidden (`hayCarpetaConfigurada()` has no fuente to ask).
|
||||
registrarFuenteMusicaLocal(FuenteMusicaLocalAutoImpl(prefs: prefs));
|
||||
|
||||
// Silent-error channel (fix/notificacion-media): `AudioService.asyncError`
|
||||
// had ZERO subscribers app-wide, and a `PublishSubject` with no listeners
|
||||
// drops what it is given — so every exception `audio_service` catches
|
||||
// internally was discarded without a trace, which is exactly why the
|
||||
// "media notification disappeared" report came with no evidence attached.
|
||||
// Subscribed BEFORE `AudioService.init` below (the getter only touches a
|
||||
// static subject, so it needs no initialisation) so nothing reported
|
||||
// during the MediaBrowser handshake is missed, and placed here rather than
|
||||
// in `conectarHandler` so ONE subscription covers both the on-time and the
|
||||
// degraded/timeout startup paths.
|
||||
final subErroresAudio = observarErroresAudio(
|
||||
AudioService.asyncError,
|
||||
registrar: registrarErrorAudioService,
|
||||
);
|
||||
|
||||
// Design "Timeout without re-init": AudioService.init is started exactly
|
||||
// ONCE here and `handlerFuturo` is the only future ever awaited for it —
|
||||
// the plugin caches state internally, so a double-configure call is
|
||||
@@ -69,6 +84,11 @@ Future<void> main() async {
|
||||
// degraded/late-completion paths below.
|
||||
void conectarHandler(PluriWaveAudioHandler handler) {
|
||||
registrarHandler(handler);
|
||||
// The handler is the only thing this app ever tears down
|
||||
// (`onTaskRemoved`), so the asyncError subscription's `cancel` travels
|
||||
// with it and can never leak — same "register from main.dart" convention
|
||||
// as `registrarHandler` itself.
|
||||
registrarLimpiezaArranque(subErroresAudio.cancel);
|
||||
final sesionAudio = ServicioAudioSession(objetivo: handler);
|
||||
unawaited(sesionAudio.configurar());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user