Compare commits

..
Author SHA1 Message Date
Your NameandClaude Opus 4.5 f5221b3160 chore: bump version to 2.2.1-beta+21 for release
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 17:45:21 +00:00
Your NameandClaude Opus 4.5 4265b8a544 fix: Clear audiobook context when switching to music playback
When switching from audiobook to music track playback, the audiobook
context (_currentAudiobook) was not being cleared. This caused the
player UI to remain in audiobook mode (30-second skip buttons, author
instead of artist, chapters in queue) even when playing regular music.

The fix detects when media_type changes to 'track' in player_updated
events and clears the audiobook context, allowing the UI to correctly
switch to music mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 17:20:25 +00:00
Your NameandClaude Opus 4.5 014f0a4903 Fix PCM audio race condition with auto-recovery
When audio frames arrive before the stream/start message (or after
pause/release), the native PCM player isn't setup yet, causing
"must call setup first" errors and silent playback.

This fix adds auto-recovery in two places:
1. In _onAudioData: If audio arrives while paused, automatically
   re-initialize and resume playback
2. In _feedNextChunk: If feed() fails with setup error, re-initialize
   the native player and retry

This ensures audio playback starts even when binary frames race
ahead of JSON control messages on the WebSocket.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 16:55:13 +00:00
Your NameandClaude Opus 4.5 2f0fc03dc9 chore: bump version to 2.2.1-beta+20
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 16:41:21 +00:00
Your NameandClaude Opus 4.5 9ae8462c2e fix(home): use overlay for connecting banner to prevent layout shift
Changed from Column to Stack so the "Connecting..." banner overlays
content instead of pushing rows down when it appears/disappears.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 16:40:39 +00:00
4 changed files with 134 additions and 39 deletions
@@ -1044,6 +1044,13 @@ class MusicAssistantProvider with ChangeNotifier {
if (currentMedia != null) {
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') {
final durationSecs = (currentMedia['duration'] as num?)?.toInt();
final albumName = currentMedia['album'] as String?;
+16 -13
View File
@@ -186,14 +186,24 @@ class _NewHomeScreenState extends State<NewHomeScreen> with AutomaticKeepAliveCl
);
}
return Column(
return Stack(
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)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: colorScheme.primaryContainer.withOpacity(0.5),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
color: colorScheme.primaryContainer.withOpacity(0.9),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -215,13 +225,6 @@ class _NewHomeScreenState extends State<NewHomeScreen> with AutomaticKeepAliveCl
],
),
),
Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
color: colorScheme.primary,
backgroundColor: colorScheme.surface,
child: _buildConnectedView(context, maProvider),
),
),
],
);
+91 -6
View File
@@ -68,6 +68,7 @@ class PcmAudioPlayer {
final List<Uint8List> _audioBuffer = [];
bool _isFeeding = false;
bool _isStarted = false;
bool _isAutoRecovering = false; // Tracks auto-recovery from setup errors
Completer<void>? _feedCompleter; // Tracks when current feed operation completes
// Error callback for operation failures
@@ -202,11 +203,32 @@ class PcmAudioPlayer {
/// Handle incoming audio data from the stream
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;
// 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) {
// Silently ignore audio data when paused or transitioning
// This prevents buffer accumulation during pause and any race conditions
// If we're paused and audio is arriving, this is likely a new stream
// 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;
}
@@ -218,15 +240,44 @@ class PcmAudioPlayer {
_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
if (!_isFeeding && _isStarted) {
_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
Future<void> _startPlayback() async {
if (_isStarted) return;
@@ -298,7 +349,41 @@ class PcmAudioPlayer {
// Check state again before the async feed call
if (samples.isNotEmpty && !_shouldBlockFeeding) {
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
if (!_shouldBlockFeeding) {
+1 -1
View File
@@ -1,7 +1,7 @@
name: ensemble
description: A minimalistic Music Assistant client built with Flutter
publish_to: 'none'
version: 2.2.0-beta+19
version: 2.2.1-beta+21
environment:
sdk: '>=3.0.0 <4.0.0'