Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5221b3160 | ||
|
|
4265b8a544 | ||
|
|
014f0a4903 | ||
|
|
2f0fc03dc9 | ||
|
|
9ae8462c2e |
@@ -1044,6 +1044,13 @@ class MusicAssistantProvider with ChangeNotifier {
|
|||||||
|
|
||||||
if (currentMedia != null) {
|
if (currentMedia != null) {
|
||||||
final mediaType = currentMedia['media_type'] as String?;
|
final mediaType = currentMedia['media_type'] as String?;
|
||||||
|
|
||||||
|
// Clear audiobook context when switching to music (track) playback
|
||||||
|
if (mediaType == 'track' && _currentAudiobook != null) {
|
||||||
|
_logger.log('📚 Media type changed to track - clearing audiobook context');
|
||||||
|
clearCurrentAudiobook();
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaType != 'flow_stream') {
|
if (mediaType != 'flow_stream') {
|
||||||
final durationSecs = (currentMedia['duration'] as num?)?.toInt();
|
final durationSecs = (currentMedia['duration'] as num?)?.toInt();
|
||||||
final albumName = currentMedia['album'] as String?;
|
final albumName = currentMedia['album'] as String?;
|
||||||
|
|||||||
@@ -186,43 +186,46 @@ class _NewHomeScreenState extends State<NewHomeScreen> with AutomaticKeepAliveCl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Connecting banner when showing cached data
|
// Main content
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
backgroundColor: colorScheme.surface,
|
||||||
|
child: _buildConnectedView(context, maProvider),
|
||||||
|
),
|
||||||
|
// Connecting banner overlay (doesn't affect layout)
|
||||||
if (!isConnected && syncService.hasCache)
|
if (!isConnected && syncService.hasCache)
|
||||||
Container(
|
Positioned(
|
||||||
width: double.infinity,
|
top: 0,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
left: 0,
|
||||||
color: colorScheme.primaryContainer.withOpacity(0.5),
|
right: 0,
|
||||||
child: Row(
|
child: Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
children: [
|
color: colorScheme.primaryContainer.withOpacity(0.9),
|
||||||
SizedBox(
|
child: Row(
|
||||||
width: 12,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
height: 12,
|
children: [
|
||||||
child: CircularProgressIndicator(
|
SizedBox(
|
||||||
strokeWidth: 2,
|
width: 12,
|
||||||
color: colorScheme.primary,
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
Text(
|
||||||
Text(
|
'Connecting...',
|
||||||
'Connecting...',
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
color: colorScheme.onPrimaryContainer,
|
||||||
color: colorScheme.onPrimaryContainer,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: _onRefresh,
|
|
||||||
color: colorScheme.primary,
|
|
||||||
backgroundColor: colorScheme.surface,
|
|
||||||
child: _buildConnectedView(context, maProvider),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class PcmAudioPlayer {
|
|||||||
final List<Uint8List> _audioBuffer = [];
|
final List<Uint8List> _audioBuffer = [];
|
||||||
bool _isFeeding = false;
|
bool _isFeeding = false;
|
||||||
bool _isStarted = false;
|
bool _isStarted = false;
|
||||||
|
bool _isAutoRecovering = false; // Tracks auto-recovery from setup errors
|
||||||
Completer<void>? _feedCompleter; // Tracks when current feed operation completes
|
Completer<void>? _feedCompleter; // Tracks when current feed operation completes
|
||||||
|
|
||||||
// Error callback for operation failures
|
// Error callback for operation failures
|
||||||
@@ -202,11 +203,32 @@ class PcmAudioPlayer {
|
|||||||
|
|
||||||
/// Handle incoming audio data from the stream
|
/// Handle incoming audio data from the stream
|
||||||
void _onAudioData(Uint8List audioData) {
|
void _onAudioData(Uint8List audioData) {
|
||||||
// Ignore data if in error state or if in transitional/paused state
|
// Ignore data if in error state
|
||||||
if (_state == PcmPlayerState.error) return;
|
if (_state == PcmPlayerState.error) return;
|
||||||
|
|
||||||
|
// Handle audio arriving during paused/transitional states
|
||||||
|
// This can happen when audio frames arrive before stream/start message (race condition)
|
||||||
|
// We need to auto-recover in this case
|
||||||
if (_shouldBlockFeeding) {
|
if (_shouldBlockFeeding) {
|
||||||
// Silently ignore audio data when paused or transitioning
|
// If we're paused and audio is arriving, this is likely a new stream
|
||||||
// This prevents buffer accumulation during pause and any race conditions
|
// starting before the stream/start message. Queue for auto-recovery.
|
||||||
|
if (_state == PcmPlayerState.paused && !_isAutoRecovering) {
|
||||||
|
_logger.log('PcmAudioPlayer: Audio arriving while paused - initiating auto-recovery');
|
||||||
|
_isAutoRecovering = true;
|
||||||
|
_audioBuffer.add(audioData);
|
||||||
|
|
||||||
|
// Trigger async recovery
|
||||||
|
_autoRecoverFromPause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already recovering or in transitional state, buffer the data
|
||||||
|
if (_isAutoRecovering) {
|
||||||
|
_audioBuffer.add(audioData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other blocking states (pausing, stopping, resuming), ignore
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,15 +240,44 @@ class PcmAudioPlayer {
|
|||||||
_startPlayback();
|
_startPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Auto-resume from paused state has been removed to prevent race conditions.
|
|
||||||
// The provider should explicitly call play() when ready to resume.
|
|
||||||
|
|
||||||
// Feed data if not currently feeding
|
// Feed data if not currently feeding
|
||||||
if (!_isFeeding && _isStarted) {
|
if (!_isFeeding && _isStarted) {
|
||||||
_feedNextChunk();
|
_feedNextChunk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Auto-recover from paused state when audio arrives unexpectedly
|
||||||
|
Future<void> _autoRecoverFromPause() async {
|
||||||
|
_logger.log('PcmAudioPlayer: Auto-recovering from pause');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Re-initialize the native player
|
||||||
|
await pcm.FlutterPcmSound.setup(
|
||||||
|
sampleRate: _format.sampleRate,
|
||||||
|
channelCount: _format.channels,
|
||||||
|
);
|
||||||
|
await pcm.FlutterPcmSound.setFeedThreshold(_feedThreshold);
|
||||||
|
pcm.FlutterPcmSound.setFeedCallback(_onFeedRequested);
|
||||||
|
await pcm.FlutterPcmSound.start();
|
||||||
|
|
||||||
|
_isStarted = true;
|
||||||
|
_state = PcmPlayerState.playing;
|
||||||
|
_startElapsedTimeTimer();
|
||||||
|
_logger.log('PcmAudioPlayer: Auto-recovery from pause successful');
|
||||||
|
|
||||||
|
// Start feeding buffered data
|
||||||
|
_isAutoRecovering = false;
|
||||||
|
if (_audioBuffer.isNotEmpty && !_isFeeding) {
|
||||||
|
_feedNextChunk();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_logger.log('PcmAudioPlayer: Auto-recovery from pause failed: $e');
|
||||||
|
_isAutoRecovering = false;
|
||||||
|
_state = PcmPlayerState.error;
|
||||||
|
_emitError(PcmPlayerError.resumeFailed, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Start audio playback
|
/// Start audio playback
|
||||||
Future<void> _startPlayback() async {
|
Future<void> _startPlayback() async {
|
||||||
if (_isStarted) return;
|
if (_isStarted) return;
|
||||||
@@ -298,7 +349,41 @@ class PcmAudioPlayer {
|
|||||||
|
|
||||||
// Check state again before the async feed call
|
// Check state again before the async feed call
|
||||||
if (samples.isNotEmpty && !_shouldBlockFeeding) {
|
if (samples.isNotEmpty && !_shouldBlockFeeding) {
|
||||||
await pcm.FlutterPcmSound.feed(pcm.PcmArrayInt16.fromList(samples));
|
try {
|
||||||
|
await pcm.FlutterPcmSound.feed(pcm.PcmArrayInt16.fromList(samples));
|
||||||
|
} catch (feedError) {
|
||||||
|
// Auto-recover from "must call setup first" error
|
||||||
|
// This happens when audio frames arrive before stream/start message
|
||||||
|
if (feedError.toString().contains('must call setup first') && !_isAutoRecovering) {
|
||||||
|
_logger.log('PcmAudioPlayer: Auto-recovering from setup error');
|
||||||
|
_isAutoRecovering = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Re-initialize the native player
|
||||||
|
await pcm.FlutterPcmSound.setup(
|
||||||
|
sampleRate: _format.sampleRate,
|
||||||
|
channelCount: _format.channels,
|
||||||
|
);
|
||||||
|
await pcm.FlutterPcmSound.setFeedThreshold(_feedThreshold);
|
||||||
|
pcm.FlutterPcmSound.setFeedCallback(_onFeedRequested);
|
||||||
|
await pcm.FlutterPcmSound.start();
|
||||||
|
_isStarted = true;
|
||||||
|
_state = PcmPlayerState.playing;
|
||||||
|
_startElapsedTimeTimer();
|
||||||
|
_logger.log('PcmAudioPlayer: Auto-recovery successful');
|
||||||
|
|
||||||
|
// Retry the feed with the current chunk
|
||||||
|
await pcm.FlutterPcmSound.feed(pcm.PcmArrayInt16.fromList(samples));
|
||||||
|
} catch (recoveryError) {
|
||||||
|
_logger.log('PcmAudioPlayer: Auto-recovery failed: $recoveryError');
|
||||||
|
_isAutoRecovering = false;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
_isAutoRecovering = false;
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't update stats if we got paused during the feed
|
// Don't update stats if we got paused during the feed
|
||||||
if (!_shouldBlockFeeding) {
|
if (!_shouldBlockFeeding) {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: ensemble
|
name: ensemble
|
||||||
description: A minimalistic Music Assistant client built with Flutter
|
description: A minimalistic Music Assistant client built with Flutter
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 2.2.0-beta+19
|
version: 2.2.1-beta+21
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|||||||
Reference in New Issue
Block a user