From 2d2a9e8973bc8ee76aad9bfe1f03f17b532e8ab9 Mon Sep 17 00:00:00 2001 From: liu weikai Date: Fri, 16 Oct 2026 12:11:25 +0800 Subject: [PATCH] Keep music entry snapshots stable during paused restore --- docs/uiux/components/music.md | 3 +++ .../esp/common/src/music/music_runtime.cpp | 21 +++++++++++++++++-- tests/music/test_runtime.cpp | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/uiux/components/music.md b/docs/uiux/components/music.md index c1d4d372..57a3a466 100644 --- a/docs/uiux/components/music.md +++ b/docs/uiux/components/music.md @@ -120,6 +120,9 @@ observable PSRAM/SD/audio ports. It verifies lazy task creation, idle volume, hidden-pause cleanup and a UI card-change snapshot interleaved with worker restore. That regression fails before the saved-record ownership transfer and passes afterward; storage work remains outside the publication mutex. +Restore publishes the paused session before handing the record to the worker, +so a simultaneous entry snapshot keeps Now Playing routing. Published snapshots +are also scoped to their SD media session. The same test checks both conditions. Preview screenshots use explicit deterministic fixtures; they are not evidence of physical audio quality, bus timing, PSRAM peaks or real-device latency. diff --git a/platform/esp/common/src/music/music_runtime.cpp b/platform/esp/common/src/music/music_runtime.cpp index 1afa739f..9e91371b 100644 --- a/platform/esp/common/src/music/music_runtime.cpp +++ b/platform/esp/common/src/music/music_runtime.cpp @@ -84,6 +84,7 @@ class Service final : public ::music::IService TaskHandle_t worker_ = nullptr; Runtime* runtime_ = nullptr; ::music::Snapshot* published_ = nullptr; + uint32_t published_session_ = 0; ::music::ResumeState* resume_ = nullptr; ::music::Artwork* artwork_ = nullptr; ::music::Request* requests_ = nullptr; @@ -135,7 +136,11 @@ class Service final : public ::music::IService { Lock lock(*this); if (!published_) published_ = ::music::make<::music::Snapshot>(); - if (published_) *published_ = runtime.player.state; + if (published_) + { + *published_ = runtime.player.state; + published_session_ = sd::sd_media_session(); + } else failure_ = Failure::Memory; auto* art = runtime.player.artwork(); if (art != artwork_) @@ -192,6 +197,18 @@ class Service final : public ::music::IService // UI snapshot may invalidate its own saved session after a card change. if (resume_ && runtime->player.state.current.id == 0) { + // Publish the existing session before transferring it, so an + // entry snapshot cannot briefly route a paused user to Library. + if (!published_) published_ = ::music::make<::music::Snapshot>(); + if (published_) + { + published_->current = resume_->current; + published_->position_ms = resume_->position_ms; + published_->mode = resume_->mode; + published_->playback = ::music::Playback::Paused; + published_session_ = resume_->media_session; + } + else failure_ = Failure::Memory; saved = resume_; resume_ = nullptr; } @@ -352,7 +369,7 @@ class Service final : public ::music::IService Lock lock(*this, pdMS_TO_TICKS(20)); if (!lock) return false; if (resume_ && resume_->media_session != sd::sd_media_session()) ::music::destroy(resume_); - if (published_) out = *published_; + if (published_ && published_session_ == sd::sd_media_session()) out = *published_; else { using Snapshot = ::music::Snapshot; diff --git a/tests/music/test_runtime.cpp b/tests/music/test_runtime.cpp index efae317f..e8badd7c 100644 --- a/tests/music/test_runtime.cpp +++ b/tests/music/test_runtime.cpp @@ -134,6 +134,7 @@ int main(int argc, char** argv) { if (runtime_test::ready_calls != 2) return; // First call belongs to Player construction. runtime_test::on_ready = nullptr; + assert(snapshot().playback == music::Playback::Paused); ++runtime_test::session; const auto out = snapshot(); assert(out.playback == music::Playback::Stopped);