#include "consumption_diagnostic.h" #include "source_ownership.h" // sbstreamer.cpp -- HTTP request broker that serves the FLAC stream to Sonos // // Copyright (c) 2026 Jaap van Vliet // // Original implementation for the YeneY project. // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Licensed under the PolyForm Noncommercial License 1.0.0. See LICENSE. // // THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #include "upnp/stale_stream.h" #include "sbstreamer.h" #include "timing_probe_runtime.h" #include "stream_session.h" #include "upnp/timing.h" #include "stream_close_log.h" #include "sbencoder.h" #include "stream_metrics.h" #include "stream_debug.h" #include "audio_mode.h" #include #include "sonos-position.h" #include #include #include #include #include #include #include #include #define SBSTREAMER_ICON "/pulseaudio.png" #define SBSTREAMER_CONTENT "audio/flac" #define SBSTREAMER_DESC "Audio stream from %s" #define SBSTREAMER_TIMEOUT 10000 // Four seconds waiting + bounded socket writes/EOF stays below ~five seconds. #define SBSTREAMER_HTTP_IDLE_TIMEOUT 4000 // Allow the device play -> LMS CLI -> strm u round trip a full five seconds. #define SBSTREAMER_RESUME_TIMEOUT 5000 #define SBSTREAMER_STANDBY_TIMEOUT 30000 #define SBSTREAMER_CHUNK 16384 using namespace bridge; // The LMS generation outlives every HTTP connection. Each ACTIVE request owns // a fresh FLAC encoder in that generation; STANDBY requests have no encoder. static std::shared_ptr g_enc; static std::mutex g_enc_mutex; static std::map streamRates; // Publish the next format before its ID, without holding g_enc_mutex while // new_squeezebox_stream_id takes the transport/resume locks. extern "C" void set_squeezebox_audio_rate(unsigned stream, unsigned rate) { std::lock_guard lock(g_enc_mutex); streamRates[stream] = rate; while (streamRates.size() > 2) streamRates.erase(streamRates.begin()); } // Ownership is independent of socket state. All request fields and slots are // protected by g_enc_mutex; only ACTIVE requests have an encoder. struct StreamRequest { std::chrono::steady_clock::time_point arrived = std::chrono::steady_clock::now(); unsigned long long id; unsigned stream; std::string restartTag; std::shared_ptr encoder; std::atomic replaced{false}; bool opened = false; bool pauseEnded = false; std::chrono::steady_clock::time_point expectedCloseAt{}; std::atomic heldResume{false}; std::atomic resumeAcknowledged{false}; std::atomic serving{false}; }; static bridge::StreamCloseLog closeLog; static std::atomic deferCloseLog{false}; extern "C" void configure_squeezebox_close_logging(bool own) { deferCloseLog = own; } static unsigned long long nextRequestId = 0; static unsigned ownershipStream = 0; static std::shared_ptr activeRequest; static std::shared_ptr canonicalEncoder; static std::string canonicalRestartTag; static std::vector> standbyRequests; // Caller holds g_enc_mutex, including when promoting a standby. Publish the // fresh encoder and its owner together so PCM always goes to the ACTIVE request. static void activateRequest(const std::shared_ptr& request, bool promoted) { if (g_enc) g_enc->retireProducer(); request->encoder = std::make_shared(request->stream); const auto rate = streamRates.find(request->stream); request->opened = request->encoder->open(audioMode() == AudioMode::Legacy ? 16 : 24, rate == streamRates.end() ? 44100 : rate->second); sonos_position_connection(request->stream, request->id); activeRequest = request; g_enc = request->encoder; streamMetrics().encoder([weak = std::weak_ptr(g_enc), connection = request->id] { auto enc = weak.lock(); if (!enc) return StreamMeasurement{}; const auto rate = enc->sampleRate(); double frame = 0, scale = 1; bool locked = streamPacingSonos() && sonos_stream_clock(enc->streamId(),rate,&frame,&scale); return StreamMeasurement{double(enc->frameOrigin.load())*1000/rate + enc->encodedAudioMs(), double(estimate_sonos_audible_frames(rate))*1000/rate, enc->hasAudio() && !enc->responseEnded(), connection, locked}; }); auto weak = std::weak_ptr(g_enc); g_enc->pacingClock([weak] { auto enc = weak.lock(); if (!enc) return StreamClock{}; double frame = 0, scale = 1; bool locked = sonos_stream_clock(enc->streamId(), enc->sampleRate(), &frame, &scale); return StreamClock{(frame-double(enc->frameOrigin.load()))*1000/enc->sampleRate(), scale, locked}; }); canonicalEncoder = g_enc; canonicalRestartTag = request->restartTag; if (promoted && deferCloseLog) closeLog.observe(request->stream); if (promoted) request->expectedCloseAt = std::chrono::steady_clock::now(); if (promoted) printf("stream %u: GET #%llu promoted\n", request->stream, request->id); printf("stream %u: GET #%llu ACTIVE\n", request->stream, request->id); } static unsigned endedByPause = 0; extern void ResumeSqueezeBox(unsigned current); extern void ResumeSqueezeBoxGetPair(unsigned, unsigned long long, unsigned long long, std::chrono::steady_clock::duration); extern std::string SqueezeBoxURL(unsigned current); extern "C" unsigned get_squeezebox_stream_id(void); extern "C" unsigned get_lms_stream_serial(void); extern "C" int yeney_is_paused(void); extern "C" int sonos_output_running(void); // Signal only the HTTP lifetime. Its worker sends EOF and retains the encoder // until the next same-ID GET replaces it. No generation or FLAC close here. static void endSqueezeboxResponse(bool flush) { std::lock_guard lock(g_enc_mutex); endedByPause = get_squeezebox_stream_id(); if (deferCloseLog) closeLog.observe(endedByPause); if (activeRequest) activeRequest->expectedCloseAt = std::chrono::steady_clock::now(); const bool preserve = flush && activeRequest && activeRequest->heldResume && !activeRequest->serving; if (preserve) activeRequest->resumeAcknowledged = false; if (!preserve && g_enc && g_enc->streamId() == endedByPause) g_enc->endResponse(); // A pause ends existing requests, not a handoff to a standby. Standbys // have sent no headers and disconnect silently; later GETs may wait for PCM. for (auto& request : standbyRequests) request->pauseEnded = true; standbyRequests.clear(); } extern "C" void note_squeezebox_device_close(void) { if (deferCloseLog) closeLog.observe(get_squeezebox_stream_id()); std::lock_guard lock(g_enc_mutex); if (activeRequest) activeRequest->expectedCloseAt = std::chrono::steady_clock::now(); } extern "C" { void end_squeezebox_response(void) { endSqueezeboxResponse(false); } void flush_squeezebox_response(void) { endSqueezeboxResponse(true); } void hold_squeezebox_resume(unsigned stream) { std::lock_guard lock(g_enc_mutex); if (activeRequest && activeRequest->stream == stream && !activeRequest->encoder->hasAudio() && !activeRequest->encoder->responseEnded() && !activeRequest->encoder->cancelled()) activeRequest->heldResume = true; } int squeezebox_response_streaming(unsigned stream) { std::lock_guard lock(g_enc_mutex); return activeRequest && activeRequest->stream == stream && activeRequest->serving && g_enc && g_enc->hasAudio() && !g_enc->cancelled() && !g_enc->responseEnded(); } int squeezebox_request_open(unsigned stream) { std::lock_guard lock(g_enc_mutex); if (activeRequest && activeRequest->stream == stream) return 1; for (const auto& request : standbyRequests) if (request->stream == stream) return 1; return 0; } int squeezebox_response_open(unsigned stream) { std::lock_guard lock(g_enc_mutex); return activeRequest && activeRequest->stream == stream && g_enc && g_enc->streamId() == stream && !g_enc->cancelled() && !g_enc->responseEnded(); } int squeezebox_response_ended(unsigned stream) { std::lock_guard lock(g_enc_mutex); return stream != 0 && endedByPause == stream; } void acknowledge_squeezebox_resume(unsigned stream) { std::lock_guard lock(g_enc_mutex); if (endedByPause == stream) endedByPause = 0; if (activeRequest && activeRequest->stream == stream) activeRequest->resumeAcknowledged = true; } void invalidate_squeezebox_held_get(unsigned stream) { std::lock_guard lock(g_enc_mutex); if (g_enc && g_enc->streamId() == stream && !g_enc->hasAudio() && !g_enc->responseEnded()) { printf("stream %u: invalidating held GET for same-URL resume\n", stream); g_enc->cancel(); } } int encode_squeezebox_audio_cancellable(const char* data, int len, uint64_t firstFrame, int (*cancel)(void*), void* context) { auto cancelled = [=] { return cancel && cancel(context); }; unsigned stream = get_squeezebox_stream_id(); unsigned serial = get_lms_stream_serial(); auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (!cancelled() && sonos_output_running() && stream == get_squeezebox_stream_id() && serial == get_lms_stream_serial()) { std::shared_ptr enc; uint64_t requestId = 0; { std::lock_guard lock(g_enc_mutex); enc = g_enc; if (activeRequest && enc == activeRequest->encoder) requestId = activeRequest->id; } if (enc && enc->streamId() == stream && !enc->cancelled() && !enc->responseEnded() && !enc->producerRetired()) { if (!enc->hasAcceptedPcm()) enc->frameOrigin = firstFrame; int written = enc->write(data, len, SBSTREAMER_TIMEOUT, [=] { enc->frameOrigin = firstFrame; sonos_position_pcm(stream, requestId, firstFrame); }, [=] { return cancelled() || !sonos_output_running(); }); if (written == len) { if (timing_probe::enabled()) timing_probe::diagnostics().handed(stream, firstFrame, enc->sampleRate()); return 1; } if (cancelled() || !sonos_output_running()) return 0; // Active-request termination or resume may replace the encoder // while write waits. Retry the SAME PCM block on the new encoder. if (!enc->cancelled() && !enc->responseEnded() && !enc->producerRetired()) { printf("encode_squeezebox_audio: write() failed %d != %d\n", written, len); return 0; } } if (yeney_is_paused()) deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); if (std::chrono::steady_clock::now() >= deadline) { printf("encode_squeezebox_audio: timeout waiting for stream request\n"); return 0; } usleep(1000); } return 0; } void encode_squeezebox_audio(const char* data, int len, uint64_t firstFrame) { encode_squeezebox_audio_cancellable(data, len, firstFrame, nullptr, nullptr); } } // extern "C" void SBStreamer::registerWith(upnp::StreamServer& server) { server.registerStream(SBSTREAMER_CNAME, SBSTREAMER_URI, SBSTREAMER_DESC, SBSTREAMER_CONTENT, SBSTREAMER_ICON, [this](upnp::StreamRequest& request) { return HandleRequest(&request); }); } bool SBStreamer::HandleRequest(upnp::StreamRequest* handle) { if (IsAborted() || handle->aborted()) return false; const std::string& requestUri = handle->path(); if (requestUri.compare(0, strlen(SBSTREAMER_URI), SBSTREAMER_URI) != 0) return false; const auto method = handle->method(); if (method != upnp::StreamRequest::Method::Get && method != upnp::StreamRequest::Method::Head) return false; unsigned long long requestId = 0; if (method == upnp::StreamRequest::Method::Get) { { std::lock_guard lock(g_enc_mutex); requestId = ++nextRequestId; } printf("stream %d: GET #%llu headers: %s\n", atoi(handle->parameter("stream").c_str()), requestId, upnp::streamHeaderLog(handle->headers()).c_str()); } const std::string session = handle->parameter("session"); if (session != streamSessionToken()) { printf("stale request: session %s != %s\n", session.c_str(), streamSessionToken().c_str()); upnp::rejectStaleStream(*handle); return true; } switch (method) { case upnp::StreamRequest::Method::Get: { int stream = atoi(handle->parameter("stream").c_str()); streamSqueezeBox(handle, stream, requestId); return true; } case upnp::StreamRequest::Method::Head: { handle->reply(200, SBSTREAMER_CONTENT); return true; } default: return false; // unhandled method } } void SBStreamer::streamSqueezeBox(upnp::StreamRequest* handle, int stream, unsigned long long requestId) { StreamDebugRequest diagnostics(*handle); handle = &diagnostics; printf("Sonos requested stream %d\n", stream); // Bound a stalled peer as well as a stalled PCM producer. Sending uses // the socket directly, so a receive timeout alone is insufficient. handle->sendTimeout(500); auto request = std::make_shared(); { std::lock_guard lock(g_enc_mutex); request->id = requestId; request->stream = stream; request->restartTag = handle->parameter("restart"); } auto peerClosed = [handle, request] { return request->replaced.load() || handle->peerClosed(); }; unsigned current = get_squeezebox_stream_id(); if (stream <= 0 || (unsigned)stream > current) { handle->reply(400); return; } auto redirect = [&] { std::string url = SqueezeBoxURL(get_squeezebox_stream_id()); if (!handle->header("Range").empty()) url += "&restart=" + std::to_string(requestId); std::string response = "HTTP/1.1 302 Found\r\nLocation: " + url + "\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"; printf("stream %d: HTTP 302 -> %s\n", stream, url.c_str()); handle->send(response.c_str(), response.size()); }; if ((unsigned)stream < current) { redirect(); return; } if (speakerRelinquished.load() && yeney_is_paused()) { // Sonos can restore the previous queue after AirPlay. Do not request // LMS resume and do not turn a paused queue into a 503 retry loop. const std::string paused = "HTTP/1.1 200 OK\r\nContent-Type: audio/flac\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; handle->send(paused.c_str(), paused.size()); return; } const auto range = handle->header("Range"); bool forceRestart; { std::lock_guard lock(g_enc_mutex); forceRestart = !request->restartTag.empty() && request->restartTag != canonicalRestartTag; if (forceRestart && activeRequest) { activeRequest->replaced = true; activeRequest.reset(); } } if (!range.empty() && !forceRestart) { uint64_t offset = 0; std::shared_ptr canonical; { std::lock_guard lock(g_enc_mutex); canonical = canonicalEncoder; } auto restart = [&](const char* reason) { static std::mutex failureMutex; static auto last = std::chrono::steady_clock::time_point{}; { std::lock_guard lock(failureMutex); auto now = std::chrono::steady_clock::now(); if (now - last >= std::chrono::seconds(5)) { printf("stream %d: Range unavailable (%s) → restart\n", stream, reason); last = now; } } const auto url = SqueezeBoxURL(get_squeezebox_stream_id()) + "&restart=" + std::to_string(requestId); const auto response = "HTTP/1.1 302 Found\r\nLocation: " + url + "\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"; handle->send(response.data(), response.size()); }; if (!openByteRange(range, offset) || !canonical || canonical->streamId() != (unsigned)stream || canonical->cancelled() || canonical->responseEnded() || canonical->producerRetired()) { restart("invalid range or inactive history"); return; } char bytes[SBSTREAMER_CHUNK]; auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); int count; while ((count = canonical->history.read(offset, bytes, sizeof(bytes))) == 0 && !peerClosed() && !handle->aborted() && !IsAborted() && !canonical->historyFinished() && !canonical->responseEnded() && (unsigned)stream == get_squeezebox_stream_id() && std::chrono::steady_clock::now() < deadline) usleep(1000); if (peerClosed() || handle->aborted() || IsAborted()) return; if (count <= 0 || (unsigned)stream != get_squeezebox_stream_id()) { restart(count < 0 ? "evicted offset" : "offset not produced within 500 ms"); return; } bool superseded; { std::lock_guard lock(g_enc_mutex); superseded = canonical != canonicalEncoder || canonical->responseEnded() || canonical->cancelled() || (unsigned)stream != get_squeezebox_stream_id(); if (!superseded) { if (activeRequest) activeRequest->replaced = true; request->encoder = canonical; request->opened = true; request->serving = true; activeRequest = request; g_enc = canonical; canonical->useHistoryReader(); } } if (superseded) { restart("superseded history"); return; } diagnostics.encoder([canonical] { return canonical->encodedAudioMs(); }, true); const auto bounds = canonical->history.bounds(); printf("stream %d: Range resume from %llu (history %llu–%llu) → continued\n", stream, (unsigned long long)offset, (unsigned long long)bounds.first, (unsigned long long)bounds.second); // Finite partial response, unknown total entity length. Never advertise // an open last-byte position or append bytes beyond the stated range. const uint64_t rangeEnd = bounds.second; const std::string headers = "HTTP/1.1 206 Partial Content\r\nContent-Type: audio/flac\r\n" "Accept-Ranges: bytes\r\nContent-Range: bytes " + std::to_string(offset) + "-" + std::to_string(rangeEnd - 1) + "/*\r\nContent-Length: " + std::to_string(rangeEnd - offset) + "\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"; ConsumptionDiagnostic consumption(*handle, playerName().trueName, "stream"); bool sent = handle->send(headers.data(), headers.size()); while (sent && offset < rangeEnd && !peerClosed() && !handle->aborted() && !IsAborted() && !canonical->cancelled() && !canonical->responseEnded() && !canonical->producerRetired() && (unsigned)stream == get_squeezebox_stream_id()) { count = canonical->history.read(offset, bytes, std::min(sizeof(bytes), rangeEnd-offset)); if (count <= 0) break; // slow receiver fell out of the bounded window sent = handle->send(bytes, count); offset += count; } { std::lock_guard lock(g_enc_mutex); if (activeRequest == request) { activeRequest.reset(); if (!yeney_is_paused() && (unsigned)stream == get_squeezebox_stream_id() && !standbyRequests.empty()) { auto newest = std::max_element(standbyRequests.begin(), standbyRequests.end(), [](const auto& a, const auto& b) { return a->id < b->id; }); auto promoted = *newest; standbyRequests.erase(newest); activateRequest(promoted, true); } } } handle->disconnect(); return; } // Recovery GET+Range pairs on firmware 86.10 send Connection: close. // Give that plain probe 100 ms to close before committing a new encoder. bool recoveryProbe = false; { std::lock_guard lock(g_enc_mutex); recoveryProbe = canonicalEncoder && canonicalEncoder->hasAudio() && !canonicalEncoder->responseEnded() && !canonicalEncoder->cancelled() && canonicalEncoder->streamId() == (unsigned)stream && !yeney_is_paused() && handle->header("Connection") == "close"; } if (recoveryProbe) { auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); while (!peerClosed() && !handle->aborted() && !IsAborted() && std::chrono::steady_clock::now() < until) usleep(1000); if (peerClosed() || handle->aborted() || IsAborted()) return; } unsigned long long pairActive = 0; std::chrono::steady_clock::duration pairSeparation{}; { std::lock_guard lock(g_enc_mutex); // A track change may have won the race since request validation. if ((unsigned)stream == get_squeezebox_stream_id()) { if (ownershipStream != (unsigned)stream) { if (g_enc) g_enc->cancel(); activeRequest.reset(); standbyRequests.clear(); // old workers redirect on their next poll ownershipStream = stream; } // Preserve pause-ended encoders until a later GET replaces them. if (activeRequest && activeRequest->encoder->responseEnded()) activeRequest.reset(); if (!activeRequest) { activateRequest(request, false); } else { standbyRequests.push_back(request); pairSeparation = request->arrived - activeRequest->arrived; if (pairSeparation >= std::chrono::steady_clock::duration::zero() && pairSeparation <= upnp::timing::kGetPairWindow) pairActive = activeRequest->id; printf("stream %d: GET #%llu STANDBY\n", stream, request->id); } } } if (pairActive) ResumeSqueezeBoxGetPair(stream, pairActive, request->id, pairSeparation); std::shared_ptr enc; bool opened = false; auto standbyDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(SBSTREAMER_STANDBY_TIMEOUT); for (;;) { bool obsolete = false, disconnect = false; { std::lock_guard lock(g_enc_mutex); obsolete = (unsigned)stream != get_squeezebox_stream_id(); // An activated request must always take the active cleanup path, // even if its peer closes immediately after promotion. if (request->encoder) { enc = request->encoder; opened = request->opened; break; } if (peerClosed()) { printf("stream %d: GET #%llu standby closed by client\n", stream, request->id); disconnect = true; } else if (!obsolete && !request->pauseEnded && !(IsAborted() || handle->aborted()) && !yeney_is_paused() && activeRequest && activeRequest->encoder->hasAudio() && std::chrono::steady_clock::now() >= standbyDeadline) { printf("stream %d: GET #%llu standby timeout\n", stream, request->id); disconnect = true; } disconnect = disconnect || request->pauseEnded || (IsAborted() || handle->aborted()); if (obsolete || disconnect) standbyRequests.erase(std::remove(standbyRequests.begin(), standbyRequests.end(), request), standbyRequests.end()); } if (disconnect || obsolete) { if (obsolete && !disconnect) redirect(); handle->disconnect(); return; } usleep(5000); } // Only ACTIVE gets an encoder and headers. A held GET still means an // ACTIVE request waiting for PCM while LMS is paused, never a standby. bool waitForResumeAudio = yeney_is_paused() || request->heldResume; auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(SBSTREAMER_RESUME_TIMEOUT); auto waitingForAudio = [&] { return !enc->hasAudio() || (request->heldResume && !request->resumeAcknowledged); }; while (opened && !(speakerRelinquished.load() && yeney_is_paused()) && waitForResumeAudio && waitingForAudio() && !enc->cancelled() && !enc->responseEnded() && !(IsAborted() || handle->aborted()) && !peerClosed() && (unsigned)stream == get_squeezebox_stream_id() && std::chrono::steady_clock::now() < deadline) { // A marked resume already sent LMS play. Its q/s reply may clear the // transport state, but must not renew the hold or send another play. if (!request->heldResume) ResumeSqueezeBox(stream); usleep(10000); } char buf[SBSTREAMER_CHUNK]; int r = 0; const bool heldResume = request->heldResume; const bool closedResume = heldResume && peerClosed(); const bool expiredResume = heldResume && waitingForAudio() && std::chrono::steady_clock::now() >= deadline; if (opened && !(speakerRelinquished.load() && yeney_is_paused()) && !closedResume && (!waitForResumeAudio || !waitingForAudio()) && !enc->cancelled()) r = enc->read(buf, sizeof(buf), SBSTREAMER_HTTP_IDLE_TIMEOUT, false, peerClosed); bool streamReady = r >= 4 && memcmp(buf, "fLaC", 4) == 0; const std::string streamingHeaders = "HTTP/1.1 200 OK\r\nServer: " + handle->serverName() + "\r\nConnection: close\r\n" "Content-Type: audio/flac\r\nTransfer-Encoding: chunked\r\n\r\n"; if (speakerRelinquished.load() && yeney_is_paused()) { const std::string paused = "HTTP/1.1 200 OK\r\nContent-Type: audio/flac\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; handle->send(paused.c_str(), paused.size()); } else if (closedResume) { printf("held resume GET #%llu closed by client\n", request->id); } else if (!streamReady && waitForResumeAudio && (unsigned)stream != get_squeezebox_stream_id()) { if (heldResume) printf("held resume GET #%llu -> 302 stream %u\n", request->id, get_squeezebox_stream_id()); // LMS play after stop can start a new delivery generation. The held // GET follows that URL instead of reporting an audio failure. redirect(); } else if (!streamReady) { if (expiredResume) printf("held resume GET #%llu expired\n", request->id); printf("stream %d: no audio before timeout or connection replaced\n", stream); std::string error = "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; handle->send(error.c_str(), error.size()); } else { request->serving = true; if (heldResume) printf("held resume GET #%llu fed\n", request->id); printf("stream %d: serving current generation with fresh FLAC header\n", stream); diagnostics.encoder([enc] { return enc->encodedAudioMs(); }); const auto responseStarted = std::chrono::steady_clock::now(); ConsumptionDiagnostic consumption(*handle, playerName().trueName, "stream"); size_t sent = 0; bool sendFailed = false; auto send = [&](const char* data, size_t size) { if (sendFailed) return false; errno = 0; if (handle->send(data, size)) { sent += size; return true; } const int error = errno; // capture before logging or cleanup changes it sendFailed = true; const double seconds = std::chrono::duration( std::chrono::steady_clock::now() - responseStarted).count(); if (deferCloseLog && (error == EPIPE || error == ECONNRESET)) { closeLog.failed(stream, seconds, sent, error); return false; } bool expected = false; { std::lock_guard lock(g_enc_mutex); expected = (error == EPIPE || error == ECONNRESET) && request->expectedCloseAt != std::chrono::steady_clock::time_point{} && std::chrono::steady_clock::now() - request->expectedCloseAt < std::chrono::seconds(2); } if (expected) printf("stream %d: client closed after %.1f s (%zu bytes confirmed sent; %s; errno=%d)\n", stream, seconds, sent, strerror(error), error); else printf("stream %d: send to Sonos failed after %.1f s (%s; errno=%d; " "%zu bytes confirmed sent, partial failed write uncounted)\n", stream, seconds, error ? strerror(error) : "short write or closed socket", error, sent); return false; }; auto sendChunk = [&](const char* data, size_t size) { char header[16]; int length = snprintf(header, sizeof(header), "%x\r\n", (unsigned)size); return send(header, length) && send(data, size) && send("\r\n", 2); }; if (send(streamingHeaders.c_str(), streamingHeaders.size()) && sendChunk(buf, r)) { while (!(IsAborted() || handle->aborted()) && (r = enc->read(buf, sizeof(buf), SBSTREAMER_HTTP_IDLE_TIMEOUT, false, peerClosed)) > 0) { if (!sendChunk(buf, r)) break; } send("0\r\n\r\n", 5); } } if (peerClosed()) printf("stream %d: client closed connection\n", stream); { std::lock_guard lock(g_enc_mutex); const bool pauseEnded = enc->responseEnded(); const bool retain = enc == canonicalEncoder && enc->hasAudio() && !yeney_is_paused() && !enc->producerRetired() && (unsigned)stream == get_squeezebox_stream_id(); if (!pauseEnded && !retain) enc->cancel(); if (activeRequest == request) { activeRequest.reset(); if (!pauseEnded) { if (g_enc == enc && !retain) g_enc.reset(); if ((unsigned)stream == get_squeezebox_stream_id() && !standbyRequests.empty()) { auto newest = std::max_element(standbyRequests.begin(), standbyRequests.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) { return a->id < b->id; }); auto promoted = *newest; standbyRequests.erase(newest); activateRequest(promoted, true); } } } } // Preserve paused encoders and send EOF before disconnecting. handle->disconnect(); if (enc->cancelled() && !enc->responseEnded()) enc->close(); printf("stream %d: done\n", stream); printf("Done serving stream %d to Sonos\n", stream); }