Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ Main client configuration passed to the `SendspinClient` constructor.
| `httpd_psram_stack` | `bool` | `false` | Allocate HTTP server task stack in PSRAM (ESP-IDF only) |
| `httpd_priority` | `unsigned` | `17` | FreeRTOS priority for the HTTP server task (ESP-IDF only) |
| `websocket_priority` | `unsigned` | `5` | FreeRTOS priority for the WebSocket client task (ESP-IDF only) |
| `server_port` | `uint16_t` | `8928` | WebSocket server port |
| `server_max_connections` | `uint8_t` | `2` | Maximum simultaneous WebSocket connections (default supports the handoff protocol) |
| `httpd_ctrl_port` | `uint16_t` | `0` | ESP-IDF httpd control port; `0` uses `ESP_HTTPD_DEF_CTRL_PORT + 1` to avoid conflict with the web_server component |
| `time_burst_size` | `uint8_t` | `8` | Number of messages per time sync burst |
Expand Down
3 changes: 2 additions & 1 deletion examples/basic_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ macOS has mDNS support built in; no extra dependencies needed.
```sh
./build/examples/basic_client/basic_client # default name "Basic Client"
./build/examples/basic_client/basic_client "My Player" # custom name
./build/examples/basic_client/basic_client -p 8930 # listen on a custom port
```

The client listens on port 8928. When mDNS is enabled it advertises `_sendspin._tcp` so Sendspin servers on the local network discover and connect automatically; otherwise tell the server to connect with `ws://<this-host>:8928/sendspin`.
The client listens on port 8928 by default. When mDNS is enabled it advertises `_sendspin._tcp` with the configured port so Sendspin servers on the local network discover and connect automatically; otherwise tell the server to connect with `ws://<this-host>:8928/sendspin`, replacing `8928` if you passed `-p`.

If PortAudio is available, audio is played through the default output device. Otherwise, audio is discarded (NullAudioSink).

Expand Down
36 changes: 29 additions & 7 deletions examples/basic_client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/// @file Host example application for sendspin-cpp.
///
/// Runs a SendspinClient on the host computer, listening for incoming
/// connections from a Sendspin server on port 8928. When built with mDNS
/// connections from a Sendspin server on the configured port. When built with mDNS
/// support (dns_sd.h available), advertises via mDNS so Sendspin servers
/// can discover and connect automatically; otherwise the user must connect
/// manually with `-u ws://<server-host>:<port>/<path>`.
Expand All @@ -25,6 +25,7 @@
///
/// Options:
/// -u URL Connect to a WebSocket URL (e.g. ws://192.168.1.10:8928/sendspin)
/// -p PORT Listen on PORT (default: 8928)
/// -l LEVEL Set log level: none, error, warn, info (default), debug, verbose
/// -v Verbose logging (same as -l verbose)
/// -q Quiet logging (same as -l error)
Expand All @@ -49,13 +50,14 @@
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>

using namespace sendspin;

static const uint16_t SENDSPIN_PORT = 8928;
static constexpr uint16_t DEFAULT_SENDSPIN_PORT = SendspinClientConfig::DEFAULT_SERVER_PORT;
static const char* SENDSPIN_PATH = "/sendspin";

// Tracks total audio bytes received (used when PortAudio is unavailable)
Expand Down Expand Up @@ -127,6 +129,7 @@ static void print_usage(const char* prog) {
fprintf(stderr, " name Friendly name (default: \"Basic Client\")\n\n");
fprintf(stderr, "Options:\n");
fprintf(stderr, " -u URL Connect to a WebSocket URL (e.g. ws://192.168.1.10:8928/sendspin)\n");
fprintf(stderr, " -p PORT Listen on PORT (default: %u)\n", DEFAULT_SENDSPIN_PORT);
fprintf(stderr, " -l LEVEL Log level: none, error, warn, info (default), debug, verbose\n");
fprintf(stderr, " -v Verbose logging (same as -l verbose)\n");
fprintf(stderr, " -q Quiet logging (same as -l error)\n");
Expand All @@ -143,6 +146,16 @@ static bool parse_log_level(const char* str, LogLevel& level) {
return false;
}

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;
}
port = static_cast<uint16_t>(value);
return true;
}
Comment thread
jb1228 marked this conversation as resolved.

int main(int argc, char* argv[]) {
// Set up signal handler for clean shutdown
std::signal(SIGINT, signal_handler);
Expand All @@ -151,12 +164,20 @@ int main(int argc, char* argv[]) {
// Parse command line options
LogLevel log_level = LogLevel::INFO;
std::string connect_url;
uint16_t server_port = DEFAULT_SENDSPIN_PORT;
int opt;
while ((opt = getopt(argc, argv, "u:l:vqh")) != -1) {
while ((opt = getopt(argc, argv, "u:p:l:vqh")) != -1) {
switch (opt) {
case 'u':
connect_url = optarg;
break;
case 'p':
if (!parse_port(optarg, server_port)) {
fprintf(stderr, "Invalid port: %s\n", optarg);
print_usage(argv[0]);
return 1;
}
break;
case 'l':
if (!parse_log_level(optarg, log_level)) {
fprintf(stderr, "Unknown log level: %s\n", optarg);
Expand Down Expand Up @@ -191,6 +212,7 @@ int main(int argc, char* argv[]) {
config.product_name = "sendspin-cpp host example";
config.manufacturer = "sendspin-cpp";
config.software_version = "0.1.0";
config.server_port = server_port;

// Create audio output and client
#ifdef SENDSPIN_HAS_PORTAUDIO
Expand Down Expand Up @@ -301,7 +323,7 @@ int main(int argc, char* argv[]) {
client.set_network_provider(&network_provider);

// Start the server
fprintf(stderr, "Starting Sendspin basic client on port %u...\n", SENDSPIN_PORT);
fprintf(stderr, "Starting Sendspin basic client on port %u...\n", server_port);

if (!client.start_server()) {
fprintf(stderr, "Failed to start server\n");
Expand All @@ -310,17 +332,17 @@ int main(int argc, char* argv[]) {

#ifdef SENDSPIN_HAS_MDNS
MdnsAdvertiser mdns;
if (!mdns.start(friendly_name, SENDSPIN_PORT, SENDSPIN_PATH)) {
if (!mdns.start(friendly_name, server_port, SENDSPIN_PATH)) {
fprintf(stderr, "Warning: mDNS advertisement failed, server still running\n");
fprintf(stderr, "Connect manually to ws://<this-host>:%u%s\n", SENDSPIN_PORT,
fprintf(stderr, "Connect manually to ws://<this-host>:%u%s\n", server_port,
SENDSPIN_PATH);
}
#else
fprintf(stderr,
"mDNS advertisement not compiled in. Either restart with "
"-u ws://<server-host>:<port>/<path> to dial a server, or tell a server "
"to connect to ws://<this-host>:%u%s.\n",
SENDSPIN_PORT, SENDSPIN_PATH);
server_port, SENDSPIN_PATH);
#endif

// Auto-connect if a URL was provided via -u
Expand Down
1 change: 1 addition & 0 deletions examples/tui_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ brew install portaudio
./build/examples/tui_client/tui_client # default name "TUI Client"
./build/examples/tui_client/tui_client "My Player" # custom name
./build/examples/tui_client/tui_client -u ws://192.168.1.10:8928/sendspin # connect to a specific server
./build/examples/tui_client/tui_client -p 8930 # listen on a custom port
./build/examples/tui_client/tui_client -V # disable visualizer
```

Expand Down
30 changes: 26 additions & 4 deletions examples/tui_client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/// name: Optional friendly name (default: "TUI Client")
///
/// Options:
/// -p PORT Listen on PORT (default: 8928)
/// -f FORMAT Audio format as codec:rate:bits:channels (repeatable)
/// -h Show usage

Expand All @@ -47,6 +48,7 @@
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <map>
Expand All @@ -58,7 +60,7 @@

using namespace sendspin;

static const uint16_t SENDSPIN_PORT = 8928;
static constexpr uint16_t DEFAULT_SENDSPIN_PORT = SendspinClientConfig::DEFAULT_SERVER_PORT;
static const char* SENDSPIN_PATH = "/sendspin";

// Big-endian helpers for binary visualizer data parsing
Expand Down Expand Up @@ -380,11 +382,22 @@ static bool parse_audio_format(const std::string& str, sendspin::AudioSupportedF
return true;
}

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;
}
port = static_cast<uint16_t>(value);
return true;
}

static void print_usage(const char* prog) {
fprintf(stderr, "Usage: %s [options] [name]\n", prog);
fprintf(stderr, " name Friendly name (default: \"TUI Client\")\n\n");
fprintf(stderr, "Options:\n");
fprintf(stderr, " -u URL Connect to a WebSocket URL (e.g. ws://192.168.1.10:8928/sendspin)\n");
fprintf(stderr, " -p PORT Listen on PORT (default: %u)\n", DEFAULT_SENDSPIN_PORT);
fprintf(stderr, " -f FORMAT Audio format as codec:rate:bits:channels (e.g. flac:48000:24:2)\n");
fprintf(stderr, " Can be specified multiple times. Codecs: flac, opus, pcm\n");
fprintf(stderr, " -V Disable visualizer\n");
Expand All @@ -395,13 +408,21 @@ int main(int argc, char* argv[]) {
// Parse command line options
bool enable_visualizer = true;
std::string connect_url;
uint16_t server_port = DEFAULT_SENDSPIN_PORT;
std::vector<sendspin::AudioSupportedFormatObject> audio_formats;
int opt;
while ((opt = getopt(argc, argv, "u:f:Vh")) != -1) {
while ((opt = getopt(argc, argv, "u:p:f:Vh")) != -1) {
switch (opt) {
case 'u':
connect_url = optarg;
break;
case 'p':
if (!parse_port(optarg, server_port)) {
fprintf(stderr, "Invalid port: %s\n", optarg);
print_usage(argv[0]);
return 1;
}
break;
case 'f': {
sendspin::AudioSupportedFormatObject fmt;
if (!parse_audio_format(optarg, fmt)) {
Expand Down Expand Up @@ -434,6 +455,7 @@ int main(int argc, char* argv[]) {
config.product_name = "sendspin-cpp host TUI";
config.manufacturer = "sendspin-cpp";
config.software_version = "0.1.0";
config.server_port = server_port;

// Create audio output
#ifdef SENDSPIN_HAS_PORTAUDIO
Expand Down Expand Up @@ -726,7 +748,7 @@ int main(int argc, char* argv[]) {
// Advertise via mDNS and browse for other servers (when compiled in)
#ifdef SENDSPIN_HAS_MDNS
MdnsAdvertiser mdns;
mdns.start(friendly_name, SENDSPIN_PORT, SENDSPIN_PATH);
mdns.start(friendly_name, server_port, SENDSPIN_PATH);

MdnsBrowser mdns_browser;
mdns_browser.start();
Expand All @@ -752,7 +774,7 @@ int main(int argc, char* argv[]) {
static_cast<uint16_t>(std::stoul(host_port.substr(colon + 1)));
} else {
state.connected_host = host_port;
state.connected_port = SENDSPIN_PORT;
state.connected_port = DEFAULT_SENDSPIN_PORT;
}
}
client.connect_to(connect_url);
Expand Down
11 changes: 7 additions & 4 deletions include/sendspin/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ struct SendspinClientConfig {
unsigned websocket_priority{5}; ///< FreeRTOS priority for the WebSocket client task
///< (ESP-IDF only)

uint8_t server_max_connections{2}; ///< Maximum simultaneous connections (default: 2 for
///< handoff protocol)
uint16_t httpd_ctrl_port{0}; ///< ESP-IDF httpd control port; 0 = ESP_HTTPD_DEF_CTRL_PORT
///< + 1 (avoids conflict with web_server component)
static constexpr uint16_t DEFAULT_SERVER_PORT = 8928U; ///< Default WebSocket server port

uint16_t server_port{DEFAULT_SERVER_PORT}; ///< WebSocket server port
uint8_t server_max_connections{2}; ///< Maximum simultaneous connections (default: 2
///< for handoff protocol)
uint16_t httpd_ctrl_port{0}; ///< ESP-IDF httpd control port; 0 = ESP_HTTPD_DEF_CTRL_PORT
///< + 1 (avoids conflict with web_server component)

static constexpr int64_t DEFAULT_BURST_INTERVAL_MS = 10000; ///< Default ms between bursts
static constexpr int64_t DEFAULT_BURST_TIMEOUT_MS = 10000; ///< Default burst timeout ms
Expand Down
1 change: 1 addition & 0 deletions src/connection_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ void ConnectionManager::init_server(SendspinClient* client) {
this->client_ = client;

this->ws_server_ = std::make_unique<SendspinWsServer>();
this->ws_server_->set_port(this->client_->config_.server_port);
this->ws_server_->set_max_connections(this->client_->config_.server_max_connections);
this->ws_server_->set_ctrl_port(this->client_->config_.httpd_ctrl_port);

Expand Down
4 changes: 2 additions & 2 deletions src/esp/ws_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ namespace sendspin {
//
// Lifecycle:
// 1. SendspinClient calls start() with callbacks and configuration
// 2. Server listens on port 8928 at /sendspin
// 2. Server listens on the configured port at /sendspin
// 3. open_callback() creates SendspinServerConnection instances
// 4. SendspinClient receives connection via new_connection_callback
// 5. SendspinClient manages connection ownership and handoff logic
Expand All @@ -68,7 +68,7 @@ bool SendspinWsServer::start(SendspinClient* client, bool task_stack_in_psram,
config.task_caps = MALLOC_CAP_SPIRAM;
}
config.task_priority = task_priority;
config.server_port = 8928;
config.server_port = this->server_port_;
config.max_open_sockets = this->max_connections_;
config.open_fn = SendspinWsServer::open_callback;
config.close_fn = SendspinWsServer::close_callback;
Expand Down
15 changes: 13 additions & 2 deletions src/esp/ws_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#pragma once

#include "sendspin/config.h"

#include <esp_http_server.h>

#include <functional>
Expand All @@ -41,7 +43,7 @@ class SendspinServerConnection;
* observer for routing and handoff decisions.
*
* Capabilities:
* - Accepts incoming WebSocket connections on a dedicated port
* - Accepts incoming WebSocket connections on a configurable dedicated port
* - Routes WebSocket messages directly via the session-pinned shared_ptr (no cross-thread
* find-by-sockfd lookup is needed)
* - Manages open/close callbacks to notify the client of connection lifecycle events
Expand Down Expand Up @@ -115,6 +117,12 @@ class SendspinWsServer {
this->max_connections_ = max_connections;
}

/// @brief Sets the TCP port the WebSocket server listens on
/// @param port Port number.
void set_port(uint16_t port) {
this->server_port_ = port;
}

/// @brief Overrides the ESP-IDF httpd control port
/// Defaults to 0 (uses ESP_HTTPD_DEF_CTRL_PORT + 1 to avoid conflict with web_server).
/// @param ctrl_port Control port number; 0 = use default.
Expand Down Expand Up @@ -172,11 +180,14 @@ class SendspinWsServer {
/// @brief The HTTP server handle
httpd_handle_t server_{nullptr};

// 8-bit fields
// Numeric fields

/// @brief Maximum number of simultaneous connections (default: 2 for handoff)
uint8_t max_connections_{2};

/// @brief TCP port the WebSocket server listens on
uint16_t server_port_{SendspinClientConfig::DEFAULT_SERVER_PORT};

/// @brief httpd control port override (0 = use ESP_HTTPD_DEF_CTRL_PORT + 1)
uint16_t ctrl_port_{0};
};
Expand Down
9 changes: 3 additions & 6 deletions src/host/ws_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ namespace sendspin {

static const char* const TAG = "sendspin.ws_server";

/// @brief Default WebSocket server port
static constexpr uint16_t DEFAULT_SERVER_PORT = 8928U;

SendspinWsServer::~SendspinWsServer() {
this->stop();
}
Expand All @@ -42,7 +39,7 @@ bool SendspinWsServer::start(SendspinClient* client, bool /*task_stack_in_psram*
this->client_ = client;

// Create IXWebSocket server on the configured port
this->server_ = std::make_unique<ix::WebSocketServer>(DEFAULT_SERVER_PORT, "0.0.0.0");
this->server_ = std::make_unique<ix::WebSocketServer>(this->server_port_, "0.0.0.0");

this->server_->setOnConnectionCallback(
[this](const std::weak_ptr<ix::WebSocket>& weak_ws,
Expand Down Expand Up @@ -106,8 +103,8 @@ bool SendspinWsServer::start(SendspinClient* client, bool /*task_stack_in_psram*
}
});

SS_LOGI(TAG, "Starting server on port: %d (max connections: %d)", DEFAULT_SERVER_PORT,
this->max_connections_);
SS_LOGI(TAG, "Starting server on port: %u (max connections: %d)",
static_cast<unsigned int>(this->server_port_), this->max_connections_);

auto result = this->server_->listen();
if (!result.first) {
Expand Down
Loading