fix(audio): keep local skips local, and stop a failed station blanking Auto
Three reported suspicions. Two confirmed by reading, one not.
1. CONFIRMED, self-inflicted. Playing a song from the phone and pressing
NEXT jumped to a radio station.
3398d02 taught skipToNext/skipToPrevious to fall back to station skipping
when there is no local queue, so the car's buttons would not be dead for
radio. But queue-less does not mean radio: tapping ONE track goes through
reproducirPistaLocal, which never builds a queue -- only folder playback
sets _colaLocal. That is exactly why the report said "at least the first
time".
emisoraActual cannot tell them apart either: _cambiarFuente fills it in for
every source, so a local MP3 arrives as an Emisora whose url is its
content:// document URI. The media id's scheme is the real discriminator,
the same test that already keeps the recorder off local files. A local
track now skips nowhere, which is the correct behaviour for a single item.
2. CONFIRMED mechanism. A failed station made the app disappear from the
Android Auto pane.
The error path published STATE_ERROR and then cleared everything:
`emisoraActual = null; mediaItem.add(null)`. That leaves the session in an
error state with no metadata at all, and Auto drops a session with nothing
to show -- reported as "if a station fails it seems to crash, and going to
1/3 it fails".
Both are kept now. Nothing outside servicio_audio.dart consumes mediaItem
(verified), so the phone is unaffected, and the car gains two things: the
screen can still name the station that failed instead of going blank, and
previous/next stay usable, so a driver can skip out of a dead station
instead of being stranded -- _saltarEmisora needs emisoraActual to know
where it is in the list. The error state itself is unchanged.
3. NOT CONFIRMED. A local track occasionally jumping to another one mid-play.
An advance requires a genuine `completed` from just_audio, so either the
player reports the end early -- plausible for a content:// SAF source,
whose duration is not always exact -- or something else moved the track.
Reading the code cannot separate those, so nothing was changed on a guess.
The advance now logs the decision with the processing state, position and
duration that caused it, so the next occurrence arrives with its reason
attached.
Tests: 1192 -> 1195.
This commit is contained in:
@@ -925,9 +925,22 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
errorMessage: mensaje,
|
errorMessage: mensaje,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
emisoraActual = null;
|
// The failed item and station are KEPT, deliberately.
|
||||||
mediaItem.add(null);
|
//
|
||||||
|
// This used to do `emisoraActual = null; mediaItem.add(null);`, which
|
||||||
|
// left the media session in STATE_ERROR with no metadata at all. Android
|
||||||
|
// Auto drops a session with nothing to show, which is what made PluriWave
|
||||||
|
// vanish from the car pane the moment a station failed -- reported as
|
||||||
|
// "if a station fails it seems to crash, and going to 1/3 it fails".
|
||||||
|
//
|
||||||
|
// Keeping them costs nothing on the phone (nothing outside this file
|
||||||
|
// consumes `mediaItem`, verified) and buys two things in the car: the
|
||||||
|
// screen can still name the station that failed instead of going blank,
|
||||||
|
// and previous/next stay usable, so the driver skips out of a dead
|
||||||
|
// station instead of being stranded -- `_saltarEmisora` needs
|
||||||
|
// `emisoraActual` to know where it is in the list.
|
||||||
|
//
|
||||||
|
// The error state itself is unchanged: STATE_ERROR with the message.
|
||||||
_player.stop().catchError((_) {});
|
_player.stop().catchError((_) {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1126,6 +1139,21 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
avanzandoCola: _avanzandoCola,
|
avanzandoCola: _avanzandoCola,
|
||||||
trackCompletado: proc == ProcessingState.completed,
|
trackCompletado: proc == ProcessingState.completed,
|
||||||
);
|
);
|
||||||
|
// Reported: a local track sometimes jumped to another one on its own,
|
||||||
|
// without reaching the end and without anyone pressing a thing. Reading
|
||||||
|
// the code cannot settle it -- an advance here requires a genuine
|
||||||
|
// `completed` from just_audio, so either the player reports the end
|
||||||
|
// early (plausible for a `content://` SAF source, whose duration is not
|
||||||
|
// always exact) or something else moved the track. Rather than guess,
|
||||||
|
// log the transition with the state that caused it, so the next report
|
||||||
|
// arrives with the reason attached instead of another hypothesis.
|
||||||
|
if (decision != DecisionAvanceCola.ninguna) {
|
||||||
|
debugPrint(
|
||||||
|
'[PluriWave][ServicioAudio] avance de cola decision=${decision.name} '
|
||||||
|
'proc=${proc.name} pos=${_player.position} dur=${_player.duration} '
|
||||||
|
'pista=${mediaItem.value?.title}',
|
||||||
|
);
|
||||||
|
}
|
||||||
switch (decision) {
|
switch (decision) {
|
||||||
case DecisionAvanceCola.ninguna:
|
case DecisionAvanceCola.ninguna:
|
||||||
return;
|
return;
|
||||||
@@ -1495,7 +1523,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
Future<void> skipToNext() async {
|
Future<void> skipToNext() async {
|
||||||
final cola = _colaLocal;
|
final cola = _colaLocal;
|
||||||
if (cola == null) {
|
if (cola == null) {
|
||||||
await _saltarEmisora(haciaAtras: false);
|
if (_reproduciendoRadio) await _saltarEmisora(haciaAtras: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final siguiente = cola.conSiguiente();
|
final siguiente = cola.conSiguiente();
|
||||||
@@ -1516,7 +1544,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
Future<void> skipToPrevious() async {
|
Future<void> skipToPrevious() async {
|
||||||
final cola = _colaLocal;
|
final cola = _colaLocal;
|
||||||
if (cola == null) {
|
if (cola == null) {
|
||||||
await _saltarEmisora(haciaAtras: true);
|
if (_reproduciendoRadio) await _saltarEmisora(haciaAtras: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final anterior = cola.conAnterior();
|
final anterior = cola.conAnterior();
|
||||||
@@ -1524,6 +1552,26 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
await _reproducirEntradaCola(anterior.actual);
|
await _reproducirEntradaCola(anterior.actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether what is playing right now is a RADIO STREAM, as opposed to a
|
||||||
|
/// local file.
|
||||||
|
///
|
||||||
|
/// Reported: playing a single local track and pressing next jumped to a
|
||||||
|
/// radio station. `emisoraActual` cannot answer this — `_cambiarFuente`
|
||||||
|
/// fills it in for every source, so a local MP3 arrives as an `Emisora`
|
||||||
|
/// whose `url` is its `content://` document URI. The scheme of the media
|
||||||
|
/// id is what actually distinguishes them, and it is the same test
|
||||||
|
/// `esEmisoraGrabable` uses to keep the recorder off local files.
|
||||||
|
///
|
||||||
|
/// Only a queue-less local track reaches this: folder playback sets
|
||||||
|
/// `_colaLocal` and skips within the queue, which is why the report said
|
||||||
|
/// "at least the first time" — tapping one track never builds a queue.
|
||||||
|
bool get _reproduciendoRadio {
|
||||||
|
final id = mediaItem.value?.id;
|
||||||
|
if (id == null) return false;
|
||||||
|
final esquema = Uri.tryParse(id)?.scheme.toLowerCase();
|
||||||
|
return esquema == 'http' || esquema == 'https';
|
||||||
|
}
|
||||||
|
|
||||||
/// Station-to-station skipping for the car's transport row.
|
/// Station-to-station skipping for the car's transport row.
|
||||||
///
|
///
|
||||||
/// The list to walk is resolved by [listaParaSaltoEmisora]: the narrowest
|
/// The list to walk is resolved by [listaParaSaltoEmisora]: the narrowest
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Reported: playing a song stored on the phone and pressing NEXT jumped to a
|
||||||
|
/// radio station — "at least the first time".
|
||||||
|
///
|
||||||
|
/// Self-inflicted, by 3398d02. That commit taught `skipToNext`/`skipToPrevious`
|
||||||
|
/// to fall back to station-to-station skipping when there is no local queue,
|
||||||
|
/// so the car's transport buttons would not be dead for radio. But a
|
||||||
|
/// queue-less state does NOT mean "radio is playing": tapping ONE local track
|
||||||
|
/// goes through `reproducirPistaLocal`, which never builds a queue. Only
|
||||||
|
/// folder playback sets `_colaLocal` — which is exactly why the report said
|
||||||
|
/// "at least the first time".
|
||||||
|
///
|
||||||
|
/// `emisoraActual` cannot tell them apart either: `_cambiarFuente` fills it in
|
||||||
|
/// for every source, so a local MP3 arrives as an `Emisora` whose `url` is its
|
||||||
|
/// `content://` document URI. The media id's SCHEME is the real
|
||||||
|
/// discriminator, and this asserts on that rule — the same one
|
||||||
|
/// `esEmisoraGrabable` uses to keep the recorder off local files.
|
||||||
|
///
|
||||||
|
/// `PluriWaveAudioHandler` needs platform MethodChannels and cannot be built
|
||||||
|
/// in a unit test, so this pins the predicate rather than the private getter.
|
||||||
|
void main() {
|
||||||
|
bool esRadio(String? id) {
|
||||||
|
if (id == null) return false;
|
||||||
|
final esquema = Uri.tryParse(id)?.scheme.toLowerCase();
|
||||||
|
return esquema == 'http' || esquema == 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
test('un stream de radio SÍ permite saltar de emisora', () {
|
||||||
|
expect(esRadio('http://stream.example.com/live'), isTrue);
|
||||||
|
expect(esRadio('https://stream.example.com/live'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('una pista local NO: es el caso exacto del reporte', () {
|
||||||
|
expect(
|
||||||
|
esRadio(
|
||||||
|
'content://com.android.externalstorage.documents/tree/'
|
||||||
|
'primary%3AMusic/document/primary%3AMusic%2FNew%20Limit%20-%20Smile.mp3',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
reason:
|
||||||
|
'con una pista suelta no hay cola, y antes de esto el boton '
|
||||||
|
'siguiente se iba a una emisora de radio',
|
||||||
|
);
|
||||||
|
expect(esRadio('file:///storage/emulated/0/musica/a.mp3'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sin nada sonando tampoco se salta', () {
|
||||||
|
expect(esRadio(null), isFalse);
|
||||||
|
expect(esRadio(''), isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user