Skip to content

Commit dfa2c69

Browse files
authored
Merge pull request #62 from Sendspin/chrisuthe/task/re-pin-sendspin-cpp-to-v0-8-0-and-adopt-its-start
Re-pin sendspin-cpp to v0.8.0 and adopt its start/stop lifecycle
2 parents 0c58c2d + f8ba1c6 commit dfa2c69

4 files changed

Lines changed: 29 additions & 46 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ endif()
2525

2626
# sendspin-cpp
2727

28-
set(SENDSPIN_GIT_TAG "v0.7.2" CACHE STRING
28+
set(SENDSPIN_GIT_TAG "v0.8.0" CACHE STRING
2929
"sendspin-cpp git tag to build against")
3030

3131
# player, metadata and controller (group transport verbs); no FORCE, so -D overrides still win.

docs/ROADMAP.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ the five files that log, and `tests/daemon_test.cpp`:
444444
process acquires *any* resource — rather than as a list of three current call sites. All
445445
three are live: `make_audio_sink()` already probes the device
446446
(`src/audio_sink.cpp:258,267`) and `PortAudioSink` then holds a `PortAudioGuard` that
447-
brings up the CoreAudio HAL's mach ports and helper threads, `start_server()` starts the
447+
brings up the CoreAudio HAL's mach ports and helper threads, `start()` starts the
448448
sync task's `std::thread`, and a `DNSServiceRef` is a per-process connection to
449449
`mDNSResponder`/`avahi-daemon`. Only the forking thread survives a fork, so an item 7
450450
control socket or an item 8 config file cannot quietly land above the line without
@@ -494,7 +494,7 @@ the five files that log, and `tests/daemon_test.cpp`:
494494
mDNS daemon.
495495

496496
**Three asks removed from this item rather than deferred**, all for one reason worth
497-
writing down: sendspin-cpp v0.7.0 logs through `src/platform/logging.h`'s bare
497+
writing down: sendspin-cpp v0.8.0 logs through `src/platform/logging.h`'s bare
498498
`fprintf(stderr, "E %s: " fmt "\n", tag, ...)` macros, gated on a single global
499499
`int ss_host_log_level`, with **no callback or sink hook**. Library lines cannot be
500500
reformatted, redirected or filtered per tag by any API call this layer can make.
@@ -614,7 +614,7 @@ stops the worse failure this item only documents: an operator running a player t
614614
authenticated. It is a breaking change for anyone passing such a URL today, which is why it is
615615
owed here rather than done here. Masking is what this item ships; refusing is the fix.
616616

617-
**A third thing this item does not claim: the library's own dial lines.** sendspin-cpp v0.7.0
617+
**A third thing this item does not claim: the library's own dial lines.** sendspin-cpp v0.8.0
618618
logs the URL it is dialling at `info` from `ConnectionManager::connect_to()` and again at
619619
`error` from `SendspinClientConnection`, through the same sink-less `SS_LOG*` macros that put
620620
timestamps and per-tag filtering out of reach above — so those lines still carry whatever the
@@ -942,9 +942,9 @@ advertised an adjustable delay with nothing behind it, leaving a spec requiremen
942942
`ConnectionManager::fnv1_hash()`, which lives in the library's uninstalled `src/` — so we store
943943
the number we are handed and hand it back, and never compute it. Discovery's own tie-break
944944
needs the raw id, which the hash cannot be turned back into.
945-
- **`CliPersistenceProvider` is installed before `add_player()` and `start_server()`**, and
945+
- **`CliPersistenceProvider` is installed before `add_player()` and `start()`**, and
946946
neither is negotiable: the pointer is copied into `PlayerRole` at construction, and
947-
`start_server()` is what loads the remembered hash. Installed after either, it is a provider
947+
`start()` is what loads the remembered hash. Installed after either, it is a provider
948948
the library never asks.
949949
- **Volume and mute are the CLI's own half**, since the provider has no hook for either.
950950
`PlayerListener` writes through on every server change, and startup seeds the sink, the
@@ -1663,9 +1663,9 @@ locally settable, and volume changes are ramped. Split out of item 4.
16631663
**This item's opening premise was wrong, and correcting it is part of what shipped.** It claimed
16641664
`set_static_delay_adjustable(true)` was advertised with no `on_static_delay_changed()` override,
16651665
so "a controller can offer the user a static delay this player then applies to nothing". Against
1666-
sendspin-cpp v0.7.0 that is not true, and it never was: `SyncTask::decode_chunk()` subtracts
1666+
sendspin-cpp v0.8.0 that is not true, and it never was: `SyncTask::decode_chunk()` subtracts
16671667
`get_effective_static_delay_ms()` from every chunk's client timestamp
1668-
(`_deps/sendspin-src/src/sync_task.cpp:593`), and that value becomes `decoded_timestamp`, which
1668+
(`_deps/sendspin-src/src/sync_task.cpp:597`), and that value becomes `decoded_timestamp`, which
16691669
is what `raw_error` is measured against — the drift correction itself. `get_effective_static_delay_ms()`
16701670
returns the stored delay precisely *because* adjustability is on. So the delay was already being
16711671
obeyed; the override was never the thing standing between the value and the audio path. A future
@@ -1681,7 +1681,7 @@ landed:
16811681
- **`status` reports it**, as `static delay: <n> ms`, read from `PlayerRole::get_static_delay_ms()`
16821682
rather than from a listener-held shadow. That is not a style choice: `update_static_delay()` does
16831683
not invoke the listener (only a server's `set_static_delay` does,
1684-
`_deps/sendspin-src/src/player_role.cpp:396-400`), so a shadow would be stale the moment the
1684+
`_deps/sendspin-src/src/player_role.cpp:403-408`), so a shadow would be stale the moment the
16851685
local knob below was used.
16861686
- **`sendspin-cli delay <0-5000>`** sets it locally. The first *mutating* request answered without
16871687
a server — `status` was previously the only locally answered one at all — which the spec
@@ -1736,7 +1736,7 @@ stdout sink stays a jump. Not an oversight.
17361736

17371737
**The `DEFAULT_SINK_VOLUME` mismatch was already closed, so it was re-documented rather than
17381738
fixed.** Item 8 made startup call `player.update_volume()`/`update_muted()` unconditionally, with
1739-
the volume falling back to `DEFAULT_SINK_VOLUME`, before `start_server()` — so the role and the
1739+
the volume falling back to `DEFAULT_SINK_VOLUME`, before `start()` — so the role and the
17401740
sink agree from before the first `client/state`, and the disagreement four comments still
17411741
described was no longer observable. Those four (`src/audio_sink.h`, `src/player_listener.h`,
17421742
`src/control.h`, `src/main.cpp`'s `status()`) now describe the current arrangement, keeping the
@@ -1758,7 +1758,7 @@ reasons those members exist: `PlayerListener` is still the only thing that knows
17581758
**One gap found while scoping this belongs upstream, not here.** `required_lead_time_ms` and
17591759
`min_buffer_ms` are **REQUIRED** in `client/state` per `roles/player/v1.md` — the server uses them
17601760
to decide how far ahead to send audio, scheduling the first chunk at least
1761-
`min_buffer_ms + static_delay_ms` out — and sendspin-cpp v0.7.0 implements neither anywhere. A
1761+
`min_buffer_ms + static_delay_ms` out — and sendspin-cpp v0.8.0 implements neither anywhere. A
17621762
grep of the whole library, headers and sources, matches neither name. Nothing in this repo can
17631763
supply them: `ClientPlayerStateObject`, which `PlayerRole::Impl::build_state_fields()` fills, has
17641764
no field for either. It needs a library change, not a change here.
@@ -1877,7 +1877,7 @@ a backoff and a bounded retry, for the backends whose version of it is a server
18771877
than a device-list rebuild; PortAudio's remains the one attempt described here.
18781878

18791879
No re-advertising of formats mid-session: `capabilities()` is answered once
1880-
before `start_server()`, so a rescan does not change what the server was told, and the refusal
1880+
before `start()`, so a rescan does not change what the server was told, and the refusal
18811881
path that already names the device and the format it would not take stays the mitigation, as the
18821882
comment on `capabilities()` has said since item 3. `NullAudioSink` is untouched, having no
18831883
device to lose.
@@ -1996,14 +1996,14 @@ item, but it should not be lost.** The spec has moved inbound arbitration to an
19961996
first `server/activate`, with "higher or equal is accepted, lower is rejected", plus a
19971997
persisted last-*playback* server.
19981998

1999-
Pinned `sendspin-cpp` v0.7.0 has **no `activities` and no `server/activate` at all**, and
1999+
Pinned `sendspin-cpp` v0.8.0 has **no `activities` and no `server/activate` at all**, and
20002000
still implements the older `connection_reason` DISCOVERY/PLAYBACK handoff. So this item is
20012001
gated on a library that speaks the newer shape, and is likely to arrive with a
20022002
`SENDSPIN_GIT_TAG` bump rather than on its own.
20032003

20042004
Item 5's `src/last_server.{h,cpp}` is the nearest thing that exists today and is
20052005
deliberately named for what it observes — the last server whose *handshake* completed, not
2006-
its last *playback* server, which v0.7.0 gives no way to know.
2006+
its last *playback* server, which v0.8.0 gives no way to know.
20072007

20082008
### 18. Native PulseAudio backend — *shipped (audible slice)*
20092009

@@ -2260,11 +2260,10 @@ no workaround either — the control socket answers questions, it does not annou
22602260
polled from the main loop, next to mDNS and the control socket. A non-zero exit or a
22612261
signal death is one `W hook:` line; a hook still running at shutdown is left to
22622262
finish, because an amplifier half-switched is worse than an orphan.
2263-
- **The stream's end is waited for on the way out.** `disconnect()` only enqueues it, and
2264-
it is `client.loop()` that delivers it, so without this a player killed mid-stream reaches
2265-
`return 0` with its stop hook unrun and the amplifier still on. The loop is pumped after
2266-
the disconnect until the listener reports the stream over, bounded by
2267-
`SHUTDOWN_DRAIN_MS` against a wait of about fifty, and says so and goes if that passes.
2263+
- **The stream's end is waited for on the way out.** `client.stop()` goodbyes every peer and
2264+
delivers `on_stream_end()` before it returns, so the hook is queued by the time control comes
2265+
back and the `hooks.flush()` below runs it. Without that wait a player killed mid-stream would
2266+
reach `return 0` with its stop hook unrun and the amplifier still on.
22682267
The stop hook it spawns is by definition the orphan case above: nothing waits on it.
22692268
- **Fired on the stream lifecycle, not on the format being accepted**, through a
22702269
`PlayerListener::on_stream_event` seam shaped like `AudioSink::on_frames_played`. A
@@ -2376,17 +2375,16 @@ detection may pick the wrong interface" — the same scenario, so it is worth sa
23762375
is not the same lever. That value goes into `client/hello`'s `device_info`: it is identity,
23772376
and it moves no socket.
23782377

2379-
**Why this is more than "it listens broadly".** Pinned v0.7.2 has **no inbound
2378+
**Why this is more than "it listens broadly".** Pinned v0.8.0 has **no inbound
23802379
authentication of any kind**: no PSK, no pairing gate on the inbound path, and — per item
2381-
17, whose text still cites v0.7.0, re-checked here at v0.7.2 — no `activities` /
2382-
`server/activate` either, since it still runs the older `connection_reason` handoff.
2380+
17 — no `activities` / `server/activate` either, since it still runs the older `connection_reason` handoff.
23832381
`server_max_connections` is the only inbound limit, and it counts sockets rather than
23842382
judging them. Reachability is therefore authorization: whatever reaches the listen port
23852383
completes the handshake and drives the player, and a bind address would be the only such
23862384
control the player itself offers.
23872385

23882386
**That is a statement about the pinned library, not about Sendspin.** The spec
2389-
authenticates in the handshake — pairing and a PSK, as item 6 records — and v0.7.2 does
2387+
authenticates in the handshake — pairing and a PSK, as item 6 records — and v0.8.0 does
23902388
not implement that half yet. This item's premise expires when it does.
23912389

23922390
**In proportion.** Most installs sit behind NAT, where no interface holds a routable

src/audio_sink.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,14 @@ class AudioSink {
100100
/// @param now_ms Monotonic milliseconds; derive timing from this, not from counting calls.
101101
virtual void poll(int64_t /*now_ms*/) {}
102102

103-
/// What the device will take, probed once before start_server().
103+
/// What the device will take, probed once before start().
104104
/// An unprobeable device must answer SinkCapabilities::permissive(), never an empty set.
105105
virtual SinkCapabilities capabilities() const {
106106
return SinkCapabilities::permissive();
107107
}
108108

109109
/// Reports frames that reached the DAC, for sync feedback; optional for instant sinks.
110-
/// Assign before start_server() and never after: backends read it from their audio thread.
110+
/// Assign before start() and never after: backends read it from their audio thread.
111111
std::function<void(uint32_t frames, int64_t timestamp)> on_frames_played;
112112

113113
protected:

src/main.cpp

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ using sendspin::LogLevel;
5656
/// Sleep between client.loop() calls; bounds main-loop reaction time only.
5757
constexpr int LOOP_INTERVAL_MS = 10;
5858

59-
/// How long shutdown keeps pumping client.loop() for the stream's end (~50 ms needed).
60-
constexpr int SHUTDOWN_DRAIN_MS = 500;
61-
6259
std::atomic<bool> g_running{true};
6360

6461
void handle_signal(int /*sig*/) {
@@ -600,7 +597,7 @@ int main(int argc, char* argv[]) {
600597

601598
sendspin::SendspinClient client(std::move(config));
602599

603-
// Before add_player() and start_server(), or the library never asks it.
600+
// Before add_player() and start(), or the library never asks it.
604601
client.set_persistence_provider(&persistence);
605602

606603
std::vector<sendspin::AudioSupportedFormatObject> formats = advertised_formats(*sink);
@@ -659,7 +656,7 @@ int main(int argc, char* argv[]) {
659656
player.update_volume(volume);
660657
player.update_muted(muted);
661658

662-
if (!client.start_server()) {
659+
if (!client.start()) {
663660
log_fatal(LOG_TAG, "could not start the Sendspin server on port %u", opts.port);
664661
return 1;
665662
}
@@ -668,7 +665,7 @@ int main(int argc, char* argv[]) {
668665
SENDSPIN_CLI_VERSION, opts.port, opts.name.c_str(), sink->name().c_str(),
669666
mdns_backend_name().c_str());
670667

671-
// After start_server(), so the advertised port is already accepting.
668+
// The first client.loop() tick starts the listener after mDNS registration.
672669
MdnsService mdns;
673670
start_advertising(mdns, opts);
674671

@@ -736,23 +733,11 @@ int main(int argc, char* argv[]) {
736733
}
737734

738735
cli_log(LogLevel::INFO, "Shutting down");
739-
// Before the client disconnects, so a restart does not race a stale record or socket.
736+
// Before the client stops, so a restart does not race a stale record or socket.
740737
mdns.stop();
741738
control_socket.close();
742-
client.disconnect(sendspin::SendspinGoodbyeReason::SHUTDOWN);
743-
// Pump until the stream ends so the stop hook runs; disconnect() only asks.
744-
for (int waited_ms = 0; player_listener.streaming(); waited_ms += LOOP_INTERVAL_MS) {
745-
if (waited_ms >= SHUTDOWN_DRAIN_MS) {
746-
cli_log(LogLevel::WARN,
747-
"The stream did not end within %d ms of disconnecting -- any --hook-stop "
748-
"has not run",
749-
SHUTDOWN_DRAIN_MS);
750-
break;
751-
}
752-
client.loop();
753-
hooks.poll();
754-
std::this_thread::sleep_for(std::chrono::milliseconds(LOOP_INTERVAL_MS));
755-
}
739+
// Returns stopped, having delivered on_stream_end() -- so the stop hook is already queued.
740+
client.stop();
756741
// The stop hook may be pending behind a hung start hook; run it anyway.
757742
hooks.flush();
758743
// The lambda references locals destroyed before the listener; drop it first.

0 commit comments

Comments
 (0)