Skip to content

Add a source capture host example and sync the documentation - #117

Draft
chrisuthe wants to merge 12 commits into
source-role/05-opusfrom
source-role/06-example-docs
Draft

Add a source capture host example and sync the documentation#117
chrisuthe wants to merge 12 commits into
source-role/05-opusfrom
source-role/06-example-docs

Conversation

@chrisuthe

Copy link
Copy Markdown
Member

Part 6/6 of the source@v1 stack (tracker: #95).

What it adds: examples/source_client (captures the default PortAudio input and streams it, with an Opus switch) and the documentation sync — integration guide, internals, CLAUDE.md, README.

How it's used: build with examples on and run it against a Sendspin server; the README in the example folder has the details.

Note for maintainers: this also adds one clarifying sentence to docs/conventions.md — an example that exists solely to demonstrate one role may gate its whole CMake target on that role's option (an #ifdef leaving a do-nothing binary is dead code, not a guard). Flagged here for explicit sign-off; drop it if you prefer the rule implicit.

@chrisuthe
chrisuthe force-pushed the source-role/06-example-docs branch from 8f828e6 to 09cc38b Compare September 1, 2026 02:32
@chrisuthe chrisuthe closed this Sep 3, 2026
@chrisuthe chrisuthe reopened this Sep 4, 2026
@chrisuthe chrisuthe added the enhancement New feature or request label Sep 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical authorization and lifetime defects, plus callback-safety and failure-handling issues, must be resolved before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a host source-capture example using PortAudio, with PCM/Opus streaming, optional mDNS, and synchronized documentation.

Changes:

  • Introduces the source_client example and build configuration.
  • Documents source-role APIs, internals, usage, and build gating.
  • Updates project architecture and feature summaries.
File summaries
File Review
README.md Lists source support and the new example. No findings.
examples/source_client/README.md Documents building and running the example. No findings.
examples/source_client/main.cpp Critical (2 votes), L433: Enforce source authorization before exposing privacy-sensitive input.
Critical (1 vote), L423: Construct provider/listener before the client to satisfy lifetime contracts.
Moderate (1 vote), L255: Avoid mutexes and logging in the real-time audio callback.
Moderate (1 vote), L406: Exit the main loop when Pa_StartStream() fails.
Moderate (1 vote), L462: Use a steady-clock deadline and one interval constant.
Nit (1 vote), L257: Correct the inaccurate no-logging claim.
Nit (1 vote), L76: Extract the duplicated mDNS advertiser into shared example code.
examples/source_client/CMakeLists.txt Configures PortAudio and mDNS dependencies. No findings.
docs/internals.md Documents source pipeline internals. No findings.
docs/integration-guide.md Nit (1 vote), L189: Document pairing/trust requirements and consumer-facing pairing setup.
docs/conventions.md Clarifies build gating for single-role examples. No findings.
CMakeLists.txt Conditionally includes the source example. No findings.
CLAUDE.md Updates architecture documentation. No findings.
Review details

Suppressed comments (3)

docs/integration-guide.md:189

  • This presents the server start command as the only streaming gate, but the linked source@v1 requirements also require a paired (user trust) connection and mandate refusal at trust none. Add that requirement and the consumer-facing pairing setup here; the current implementation has no trust check, so this documentation currently describes a non-compliant and unsafe contract.
Streaming is gated by the server: the client never streams unsolicited, the default after connect is stopped, and permission does not survive reconnection. When the server commands start, the role opens the outbound stream and fires `on_streaming_started()`; from that point on, feed captured audio to `write_audio()`:

examples/source_client/main.cpp:259

  • The “no logging on the audio callback” claim is false: this callback calls SourceRole::write_audio(), whose ring-full path logs at src/source_task.cpp:207-218. Update the comment so users are not told this path is log-free.
            // Counted here and reported from the main loop: no logging on the audio
            // callback. A rejected write means the capture ring is full, or the stream
            // closed while this callback was in flight.

examples/source_client/main.cpp:77

  • This adds a third copy of the existing MdnsAdvertiser implementation (examples/basic_client/main.cpp:67-118 and examples/tui_client/main.cpp:79-110). That conflicts with the reuse-before-invention rule in docs/conventions.md:106-109 and lets registration/error handling drift independently. Extract the advertiser into examples/common and reuse it from all host examples.
// Manages mDNS service advertisement via dns_sd.h
class MdnsAdvertiser {
  • Files reviewed: 9/9 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/source_client/main.cpp Outdated
Comment thread examples/source_client/main.cpp
Comment thread examples/source_client/main.cpp
Comment thread examples/source_client/main.cpp Outdated
Comment thread examples/source_client/main.cpp Outdated
The example declares its listener and network provider before the
client (their raw pointers must outlive it), exits the main loop when
capture fails to start so shutdown closes the open stream, reports
capture drops on a steady-clock interval instead of counted loop
ticks, warns at startup that any handshaken server can start capture
on this protocol revision, and corrects the audio-callback logging
comment. The integration guide gains the matching authorization note
and the 20 ms default with 5 ms Opus support; internals documents the
binary send worker, its slot, and its reclamation path.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several new documentation claims contradict the implementation, and the example duplicates existing shared functionality.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

docs/internals.md:417

  • This is another reference to the nonexistent accepting_audio_ member. The gate opens by atomically adding STREAM_ACCEPTING to stream_state_ through try_open_audio_gate() (src/source_task.h:202-209), which is important because that compare-exchange also verifies that streaming is still desired.
3. **Flush the ring and open the gate**: the flush makes the first chunk live audio by construction, and only then is `accepting_audio_` set. A `SOURCE_STREAM` STREAMING_STARTED event is pushed onto the inbox ring.

docs/internals.md:419

  • The close path likewise clears the STREAM_ACCEPTING bit in stream_state_; there is no accepting_audio_ field. Using the actual state representation keeps this lifecycle description consistent with src/source_task.cpp:402-404.
5. **Close**: clear `accepting_audio_` first (so the tail is finite), send a final short chunk only if the encoder can take it (allowed at stream end by the spec but not required; an Opus remainder that is not a legal frame duration is dropped rather than padded), send `client-stream/end` from this same thread — ordered after the last chunk by construction — and push STREAMING_STOPPED.

examples/source_client/main.cpp:302

  • This is also a verbatim copy of examples/basic_client/main.cpp:149-157. Centralize port parsing in the same shared example CLI helper required by docs/conventions.md:106-109; otherwise validation changes must be kept synchronized manually.
static bool parse_port(const char* str, uint16_t& port) {
  • Files reviewed: 9/9 changed files
  • Comments generated: 6
  • Review effort level: Balanced

Comment thread docs/integration-guide.md Outdated
Comment thread docs/internals.md Outdated
Comment thread docs/internals.md Outdated
Comment thread docs/internals.md Outdated
Comment thread examples/source_client/main.cpp Outdated
Comment thread examples/source_client/main.cpp Outdated
Refresh the internals and integration-guide prose that the one-atomic
lifecycle rework left stale: the write gate is the STREAM_ACCEPTING bit
of stream_state_ (not accepting_audio_), desired state is STREAM_DESIRED
(not stream_requested_), the host ring takes a short mutex so the guide
no longer calls the path universally callback-safe, and the ESP worker
allocation paragraph distinguishes the per-message text contexts from
the make_shared BinarySendLookup and its owner-scoped reclamation.
Extract the duplicated MdnsAdvertiser and log-level parser into
examples/common and share them across all three host examples.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The helper extraction breaks supported no-PortAudio builds, and source internals documentation misstates send-completion behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment on lines 38 to 41
#ifdef SENDSPIN_HAS_PORTAUDIO
#include "cli_util.h"
#include "mdns_advertiser.h"
#include "portaudio_sink.h"
Comment on lines 34 to 37
#ifdef SENDSPIN_HAS_PORTAUDIO
#include "cli_util.h"
#include "mdns_advertiser.h"
#include "portaudio_sink.h"
Comment thread docs/internals.md

### Outbound send path

`SendspinConnection::send_binary_message()` is the transport contract the source task depends on (`src/connection.h`): callable from role task threads, and the completion callback fires **exactly once for every call** — inline before an error return, later from the transport, or from connection teardown for work that can never run. The task waits out every send's completion (`SOURCE_SEND_COMPLETE`) before reusing the staging buffer, which is also what makes destroying the task safe; the exactly-once contract is what keeps that unbounded wait from wedging.
Comment on lines +242 to +246
static bool parse_port(const char* str, uint16_t& port) {
char* end = nullptr;
unsigned long value = strtoul(str, &end, 10);
if (*str == '\0' || *end != '\0' || value == 0 || value > 65535UL) {
return false;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants