Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion c_src/sendspin_player/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
# build dirs. This also means `-DSENDSPIN_CPP_REF=...` cannot override;
# for scratch experiments against another ref use
# `-DFETCHCONTENT_SOURCE_DIR_SENDSPIN-CPP=<local-checkout>` instead.
set(SENDSPIN_CPP_REF "30514d5102c269a0c7fa6a13932d6bf7f2ae1abc" # v0.7.2
set(SENDSPIN_CPP_REF "9331ace6428979982c934384702d289958ca125e" # v0.8.0
Comment thread
bbangert marked this conversation as resolved.
CACHE STRING "sendspin-cpp commit SHA to pin" FORCE)

# Trim sendspin-cpp to just the player role. Other roles ship dead code on a
Expand Down
4 changes: 2 additions & 2 deletions c_src/sendspin_player/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ Differences:
## sendspin-cpp pin

Pinned via `FetchContent` to a **commit SHA** in `CMakeLists.txt`
(`SENDSPIN_CPP_REF` cache var). Currently `30514d51...`, upstream
`v0.7.2`. We pin to a SHA rather
(`SENDSPIN_CPP_REF` cache var). Currently `9331ace6...`, upstream
`v0.8.0`. We pin to a SHA rather
than a tag because git tags are mutable server-side; a retagged upstream
would otherwise silently flow into firmware. The friendly tag name (or
nearest-release note) lives in a comment beside the SHA for traceability.
Expand Down
23 changes: 18 additions & 5 deletions c_src/sendspin_player/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ struct ClientListener : SendspinClientListener {

// sendspin-cpp binds its WebSocket listener lazily — on the first
// client.loop() tick after the network provider reports ready — so
// start_server() returning does NOT mean the port accepts connections
// start() returning does NOT mean the port accepts connections
// yet. The Elixir side must not advertise the player over mDNS before
// the listener is up: Music Assistant's discovery connect is one-shot
// (aiosendspin `retry_initial_connection=False`), so a connection
Expand Down Expand Up @@ -792,9 +792,13 @@ int main(int argc, char* argv[]) {
emit_json(os.str());
}

if (!client.start_server()) {
emit_json("{\"event\":\"error\",\"kind\":\"start_server\","
"\"msg\":\"failed to bind WebSocket listener\"}");
// A false return means a role failed to start (thread spawn, ring
// buffer allocation); it does NOT mean the listener failed to bind,
// which cannot be known yet — start() only arms the server, and the
// bind happens on a later loop() tick (see the hook comment above).
if (!client.start()) {
Comment thread
bbangert marked this conversation as resolved.
emit_json("{\"event\":\"error\",\"kind\":\"start\","
"\"msg\":\"failed to start client roles\"}");
return 1;
}

Expand Down Expand Up @@ -874,7 +878,16 @@ int main(int argc, char* argv[]) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

client.disconnect(SendspinGoodbyeReason::SHUTDOWN);
// stop() rather than disconnect(): it sends the shutdown goodbye AND
// joins the role threads before returning. That ordering is load-
// bearing here — `client` is declared before `audio_sink` and the
// listeners, so reverse destruction would otherwise tear those down
// while ~SendspinClient() has yet to join the threads that call into
// them. Every PlayerListener callback dereferences `sink` (including
// on_audio_write on the role thread's hot path), so a late callback
// would touch a destroyed stack object. Upstream states the contract
// directly: listeners must outlive the client.
client.stop();
emit_json("{\"event\":\"shutdown\"}");
return 0;
}
5 changes: 3 additions & 2 deletions test/sendspin_player_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ defmodule SendspinPlayerContractTest do

Spawns the host build of the binary, drives stdin/stdout, and asserts
the documented event/command shapes. Does not require audio hardware —
the binary's `start_server()` binds a non-privileged WebSocket port and
does not touch ALSA until a stream begins.
the binary listens on a non-privileged WebSocket port (`start()` arms
the server; the bind lands on a later `loop()` tick) and does not touch
ALSA until a stream begins.
"""

use ExUnit.Case, async: false
Expand Down
Loading