lots of changes in code for ssl client

This commit is contained in:
tdv
2025-10-13 20:01:16 +03:00
parent 78b7d495d4
commit 9055a55ad3
9 changed files with 322 additions and 284 deletions

View File

@@ -3,13 +3,14 @@ project( snoop_device )
set( CMAKE_CXX_STANDARD 23 ) set( CMAKE_CXX_STANDARD 23 )
find_package(OpenSSL REQUIRED)
add_subdirectory( third_party/portaudio ) add_subdirectory( third_party/portaudio )
add_subdirectory( third_party/opus ) add_subdirectory( third_party/opus )
add_subdirectory( third_party/cpp-httplib ) add_subdirectory( third_party/cpp-httplib )
add_subdirectory( third_party/nlohmann_json ) add_subdirectory( third_party/nlohmann_json )
add_subdirectory( third_party/spdlog ) add_subdirectory( third_party/spdlog )
add_subdirectory( third_party/libogg ) add_subdirectory( third_party/libogg )
add_subdirectory( third_party/socket.io-client-cpp )
add_subdirectory( third_party/libdatachannel ) add_subdirectory( third_party/libdatachannel )
option(USE_ALSA_ADAPTER "Use ALSA Adapter" OFF) option(USE_ALSA_ADAPTER "Use ALSA Adapter" OFF)
@@ -39,4 +40,17 @@ set( HEADERS
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} ) add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
target_include_directories( ${PROJECT_NAME} PRIVATE src ) target_include_directories( ${PROJECT_NAME} PRIVATE src )
target_link_libraries( ${PROJECT_NAME} PRIVATE portaudio_static opus sioclient_tls nlohmann_json spdlog::spdlog_header_only Ogg::ogg httplib::httplib ) # Enable HTTPS (mTLS) support in cpp-httplib across ALL TUs of this target
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries( ${PROJECT_NAME} PRIVATE
portaudio_static
opus
nlohmann_json
spdlog::spdlog_header_only
Ogg::ogg
httplib::httplib
datachannel
OpenSSL::SSL
OpenSSL::Crypto
)

View File

@@ -2,6 +2,12 @@
set -e set -e
CPU_SERIAL=$(awk '/Serial/ {print $3}' /proc/cpuinfo) CPU_SERIAL=$(awk '/Serial/ {print $3}' /proc/cpuinfo)
# Fallback if CPU_SERIAL is empty
if [[ -z "$CPU_SERIAL" ]]; then
CPU_SERIAL="999999999999"
fi
KEK=$(echo -n "$CPU_SERIAL" | \ KEK=$(echo -n "$CPU_SERIAL" | \
openssl dgst -sha256 -hmac "server-provided-salt" | \ openssl dgst -sha256 -hmac "server-provided-salt" | \
awk '{print $2}') awk '{print $2}')

View File

@@ -37,9 +37,9 @@ public:
} }
// Feed raw PCM (float32 interleaved), frames = samples per channel // Feed raw PCM (float32 interleaved), frames = samples per channel
void OnPCM(const float* interleaved, size_t frames) { void OnOpus(const unsigned char* opusData, size_t opusBytes, int pcmFramesPerChannel) {
std::lock_guard lk(m_whipMutex); std::lock_guard lk(m_whipMutex);
if (m_whip) m_whip->PushPCM(interleaved, frames); if (m_whip) m_whip->PushOpus(opusData, opusBytes, pcmFramesPerChannel);
} }
bool StartWhip(const std::string& whipUrl, int sampleRate=48000, int channels=1) { bool StartWhip(const std::string& whipUrl, int sampleRate=48000, int channels=1) {

View File

@@ -207,7 +207,7 @@ namespace snoop
return u; return u;
} }
std::unique_ptr<httplib::Client> MakeClientMTLS(const Url &u, std::unique_ptr<httplib::SSLClient> MakeClientMTLS(const Url &u,
const std::filesystem::path &ca, const std::filesystem::path &ca,
const std::filesystem::path &crt, const std::filesystem::path &crt,
const std::filesystem::path &key) const std::filesystem::path &key)
@@ -215,10 +215,9 @@ namespace snoop
if (u.scheme == "https") if (u.scheme == "https")
{ {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port); auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port, crt.string().c_str(), key.string().c_str(), std::string());
cli->enable_server_certificate_verification(true); cli->enable_server_certificate_verification(true);
cli->set_ca_cert_path(ca.string().c_str()); cli->set_ca_cert_path(ca.string().c_str());
cli->set_client_cert_file(crt.string().c_str(), key.string().c_str(), nullptr);
cli->set_connection_timeout(10); cli->set_connection_timeout(10);
cli->set_read_timeout(120); cli->set_read_timeout(120);
cli->set_write_timeout(120); cli->set_write_timeout(120);
@@ -227,14 +226,14 @@ namespace snoop
throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled"); throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled");
#endif #endif
} }
else // else
{ // {
auto cli = std::make_unique<httplib::Client>(u.host.c_str(), u.port); // auto cli = std::make_unique<httplib::Client>(u.host.c_str(), u.port);
cli->set_connection_timeout(10); // cli->set_connection_timeout(10);
cli->set_read_timeout(120); // cli->set_read_timeout(120);
cli->set_write_timeout(120); // cli->set_write_timeout(120);
return cli; // return cli;
} // }
} }
// ----------------------------- Existing logic (adjusted) ----------------------------- // ----------------------------- Existing logic (adjusted) -----------------------------
@@ -421,7 +420,7 @@ namespace snoop
} }
} }
bool SendRecordedFileMTLS(httplib::Client &client, bool SendRecordedFileMTLS(httplib::SSLClient &client,
const std::string &filepath, const std::string &filepath,
unsigned long long int startedAt, unsigned long long int startedAt,
unsigned long long int stoppedAt) unsigned long long int stoppedAt)

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -90,7 +90,7 @@ namespace snoop
// If already inside window -> enter immediately // If already inside window -> enter immediately
if (now >= startMs && now < stopMs) if (now >= startMs && now < stopMs)
{ {
EnterDeepSleepUntil(stopMs); this->EnterDeepSleepUntil(stopMs);
} }
else else
{ {
@@ -101,6 +101,42 @@ namespace snoop
} }
} }
static std::optional<long long> ParseRfc3339UtcToMs(const std::string &s)
{
// Minimal, robust-ish parser for "YYYY-MM-DDTHH:MM:SS[.frac]Z"
// For production, consider date::parse or a small RFC3339 lib.
std::tm tm{};
char z = 'Z';
double frac = 0.0;
int n = 0;
// accept with optional fractional seconds
if (sscanf(s.c_str(), "%d-%d-%dT%d:%d:%d%n", &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec, &n) != 6)
return std::nullopt;
tm.tm_year -= 1900;
tm.tm_mon -= 1;
const char *p = s.c_str() + n;
if (*p == '.')
{
// consume fractional
++p;
while (isdigit(*p))
++p; // ignore exact fraction
}
if (*p != 'Z')
return std::nullopt;
time_t tt = timegm(&tm);
if (tt < 0)
return std::nullopt;
return (long long)tt * 1000LL;
}
static long long NowMs()
{
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
private: private:
std::shared_ptr<ConfigService> m_cfg; std::shared_ptr<ConfigService> m_cfg;
Handlers m_handlers; Handlers m_handlers;
@@ -161,42 +197,6 @@ namespace snoop
return u; return u;
} }
static long long NowMs()
{
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
static std::optional<long long> ParseRfc3339UtcToMs(const std::string &s)
{
// Minimal, robust-ish parser for "YYYY-MM-DDTHH:MM:SS[.frac]Z"
// For production, consider date::parse or a small RFC3339 lib.
std::tm tm{};
char z = 'Z';
double frac = 0.0;
int n = 0;
// accept with optional fractional seconds
if (sscanf(s.c_str(), "%d-%d-%dT%d:%d:%d%n", &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec, &n) != 6)
return std::nullopt;
tm.tm_year -= 1900;
tm.tm_mon -= 1;
const char *p = s.c_str() + n;
if (*p == '.')
{
// consume fractional
++p;
while (isdigit(*p))
++p; // ignore exact fraction
}
if (*p != 'Z')
return std::nullopt;
time_t tt = timegm(&tm);
if (tt < 0)
return std::nullopt;
return (long long)tt * 1000LL;
}
static std::optional<long long> JsonTimeToMs(const nlohmann::json &j) static std::optional<long long> JsonTimeToMs(const nlohmann::json &j)
{ {
if (j.contains("start_ms") && j.contains("stop_ms")) if (j.contains("start_ms") && j.contains("stop_ms"))
@@ -248,7 +248,7 @@ namespace snoop
} }
// Create HTTPS client configured for mTLS (or HTTP if base is http) // Create HTTPS client configured for mTLS (or HTTP if base is http)
std::unique_ptr<httplib::Client> MakeClient(const Url &u, std::unique_ptr<httplib::SSLClient> MakeClient(const Url &u,
const std::filesystem::path &ca, const std::filesystem::path &ca,
const std::filesystem::path &crt, const std::filesystem::path &crt,
const std::filesystem::path &key) const std::filesystem::path &key)
@@ -256,11 +256,9 @@ namespace snoop
if (u.scheme == "https") if (u.scheme == "https")
{ {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port); auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port, crt.string().c_str(), key.string().c_str(), std::string());
cli->enable_server_certificate_verification(true); cli->enable_server_certificate_verification(true);
cli->set_ca_cert_path(ca.string().c_str()); cli->set_ca_cert_path(ca.string().c_str());
cli->set_client_cert_file(crt.string().c_str(), key.string().c_str(), nullptr);
// Recommended timeouts for long polling-ish flows
cli->set_connection_timeout(10); cli->set_connection_timeout(10);
cli->set_read_timeout(60); cli->set_read_timeout(60);
cli->set_write_timeout(60); cli->set_write_timeout(60);
@@ -269,14 +267,14 @@ namespace snoop
throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided"); throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided");
#endif #endif
} }
else // else
{ // {
auto cli = std::make_unique<httplib::Client>(u.host.c_str(), u.port); // auto cli = std::make_unique<httplib::Client>(u.host.c_str(), u.port);
cli->set_connection_timeout(10); // cli->set_connection_timeout(10);
cli->set_read_timeout(60); // cli->set_read_timeout(60);
cli->set_write_timeout(60); // cli->set_write_timeout(60);
return cli; // return cli;
} // }
} }
// simple jittered sleep // simple jittered sleep
@@ -375,7 +373,7 @@ namespace snoop
return tasks; return tasks;
} }
void PostResult(httplib::Client &cli, const std::string &guid, void PostResult(httplib::SSLClient &cli, const std::string &guid,
uint64_t taskId, bool success, uint64_t taskId, bool success,
const std::string &resultJson, const std::string &err) const std::string &resultJson, const std::string &err)
{ {
@@ -431,7 +429,7 @@ namespace snoop
catch (const std::exception &e) catch (const std::exception &e)
{ {
spdlog::error("Key extraction failed: {}", e.what()); spdlog::error("Key extraction failed: {}", e.what());
SleepWithJitterOnce(rng, m_cfg->GetPollingSeconds(), m_cfg->GetJitterSeconds()); SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter());
continue; continue;
} }
const auto now = NowMs(); const auto now = NowMs();
@@ -512,7 +510,7 @@ namespace snoop
std::filesystem::remove(*tmpKey, ec); std::filesystem::remove(*tmpKey, ec);
} }
SleepWithJitterOnce(rng, m_cfg->GetPollingSeconds(), m_cfg->GetJitterSeconds()); SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter());
} }
} }
} }

View File

@@ -136,7 +136,7 @@ public:
{ {
std::filesystem::create_directories(keystoreDir); std::filesystem::create_directories(keystoreDir);
const std::string cmd = const std::string cmd =
"openssl enc -aes-256-gcm -pbkdf2 -salt " "openssl enc -aes-256-cbc -pbkdf2 -salt "
"-pass pass:" + kek + " " "-pass pass:" + kek + " "
"-in " + keyName + " " "-in " + keyName + " "
"-out " + encKeyPath.string() + " " "-out " + encKeyPath.string() + " "

View File

@@ -1,6 +1,6 @@
// src/Services/WhipClient.h // src/Services/WhipClient.h
#pragma once #pragma once
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <memory> #include <memory>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -11,53 +11,54 @@
#include <vector> #include <vector>
#include <regex> #include <regex>
#include <array> #include <array>
#include <random>
#include <cstdint>
#include <cstring>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <httplib.h> // build with CPPHTTPLIB_OPENSSL_SUPPORT #include <httplib.h> // build with CPPHTTPLIB_OPENSSL_SUPPORT
#include <rtc/rtc.hpp> // libdatachannel #include <rtc/rtc.hpp>
#include <rtc/peerconnection.hpp>
#include <rtc/description.hpp>
#include <rtc/track.hpp>
namespace snoop { namespace snoop
{
class WhipClient { class WhipClient
public: {
struct Params { public:
std::string whipUrl; // full WHIP endpoint (may already include ?token=...) struct Params
{
std::string whipUrl; // full WHIP endpoint (may include ?token=...)
std::string caPath; // CA chain std::string caPath; // CA chain
std::string crtPath; // client cert std::string crtPath; // client cert
std::string keyPath; // client key (temp extracted from keyctl) std::string keyPath; // client key (temp extracted from keyctl)
int sampleRate = 48000; int sampleRate = 48000;
int channels = 1; int channels = 1; // 1 or 2; timestamp advance uses PCM frames per channel
}; };
explicit WhipClient(Params p) explicit WhipClient(Params p) : m_p(std::move(p)) {}
: m_p(std::move(p)) {}
~WhipClient() { ~WhipClient() { Stop(); }
Stop();
}
void Start() { void Start()
{
std::lock_guard lk(m_mtx); std::lock_guard lk(m_mtx);
if (m_started) return; if (m_started)
return;
// ----- PeerConnection -----
rtc::Configuration cfg; rtc::Configuration cfg;
// No STUN/TURN required for MediaMTX WHIP (server is public/ICE-lite).
// cfg.iceServers = { }; // default
m_pc = std::make_shared<rtc::PeerConnection>(cfg); m_pc = std::make_shared<rtc::PeerConnection>(cfg);
// Optional: observe connection state / ICE m_pc->onStateChange([this](rtc::PeerConnection::State s)
m_pc->onStateChange([this](rtc::PeerConnection::State s){ { spdlog::info("WHIP pc state: {}", (int)s); });
spdlog::info("WHIP pc state: {}", (int)s); m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s)
}); { spdlog::info("WHIP gathering state: {}", (int)s); });
m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s){ m_pc->onLocalDescription([this](rtc::Description desc)
spdlog::info("WHIP gathering state: {}", (int)s); {
});
m_pc->onLocalDescription([this](rtc::Description desc){
// When we have the local SDP, POST (WHIP) to get the answer
try { try {
const std::string offer = std::string(desc); const std::string offer = std::string(desc);
auto [answer, resourceUrl] = PostOfferWHIP(offer); auto [answer, resourceUrl] = PostOfferWHIP(offer);
@@ -66,158 +67,171 @@ public:
spdlog::info("WHIP remote description set"); spdlog::info("WHIP remote description set");
} catch (const std::exception& e) { } catch (const std::exception& e) {
spdlog::error("WHIP POST offer failed: {}", e.what()); spdlog::error("WHIP POST offer failed: {}", e.what());
} } });
}); m_pc->onLocalCandidate([this](rtc::Candidate c)
m_pc->onLocalCandidate([this](rtc::Candidate c) { {
// Trickle via PATCH to WHIP resource (if server requires) if (!m_resourceUrl) return;
if (!m_resourceUrl.has_value()) return; try { PatchCandidateWHIP(c); }
try { catch (const std::exception& e) { spdlog::warn("WHIP PATCH candidate failed: {}", e.what()); } });
PatchCandidateWHIP(c);
} catch (const std::exception& e) {
spdlog::warn("WHIP PATCH candidate failed: {}", e.what());
}
});
// ----- Audio track / source ----- // Create a sendonly audio track
m_audioSource = rtc::CreateAudioSource(m_p.sampleRate, m_p.channels); rtc::Description::Audio audioDesc("audio", rtc::Description::Direction::SendOnly);
m_track = m_pc->addTrack(m_audioSource); // Opus PT typically negotiated to 111 by browsers; we'll use 111 in RTP header
m_track = m_pc->addTrack(audioDesc);
// ----- Create offer ----- // Initialize RTP state (random SSRC/seq)
m_pc->setLocalDescription(); // triggers onLocalDescription callback std::mt19937 rng{std::random_device{}()};
m_ssrc = std::uniform_int_distribution<uint32_t>()(rng);
m_seq = std::uniform_int_distribution<uint16_t>()(rng);
m_ts = 0; // RTP clock for Opus is 48kHz
// Create SDP offer (triggers onLocalDescription)
m_pc->setLocalDescription();
m_started = true; m_started = true;
} }
void Stop() { void Stop()
{
std::lock_guard lk(m_mtx); std::lock_guard lk(m_mtx);
if (!m_started) return; if (!m_started)
return;
if (m_track) m_track = nullptr; m_track.reset();
if (m_audioSource) m_audioSource = nullptr; m_pc.reset();
if (m_pc) m_pc = nullptr;
// No explicit WHIP DELETE here, but you can add it if server supports deleting the resource.
// cleanup resource url
m_resourceUrl.reset(); m_resourceUrl.reset();
m_started = false; m_started = false;
} }
// Feed PCM (float32) in interleaved format. frames = samples per channel. // Call this from your Opus encoder callback.
void PushPCM(const float* interleaved, size_t frames) { // opusData: encoded Opus frame bytes
// opusBytes: length
// pcmFramesPerChannel: number of PCM samples per channel represented by this Opus frame (e.g., 960 for 20ms @48k).
void PushOpus(const unsigned char *opusData, size_t opusBytes, int pcmFramesPerChannel)
{
std::lock_guard lk(m_mtx); std::lock_guard lk(m_mtx);
if (!m_audioSource) return; if (!m_track || !m_started)
// libdatachannel expects planar/int16? Actually CreateAudioSource accepts float32 return;
// and uses (frames, channels) to encode Opus internally.
// Push signature is: audioSource->pushFloat(interleaved, frames, channels, sampleRate) // Build RTP header (12 bytes)
m_audioSource->pushFloat(interleaved, (int)frames, m_p.channels, m_p.sampleRate); std::array<uint8_t, 12> rtp{};
rtp[0] = 0x80; // V=2, P=0, X=0, CC=0
rtp[1] = 0x80 | (m_pt & 0x7F); // M=1 (end of frame), PT
rtp[2] = uint8_t(m_seq >> 8);
rtp[3] = uint8_t(m_seq & 0xFF);
rtp[4] = uint8_t(m_ts >> 24);
rtp[5] = uint8_t((m_ts >> 16) & 0xFF);
rtp[6] = uint8_t((m_ts >> 8) & 0xFF);
rtp[7] = uint8_t(m_ts & 0xFF);
rtp[8] = uint8_t(m_ssrc >> 24);
rtp[9] = uint8_t((m_ssrc >> 16) & 0xFF);
rtp[10] = uint8_t((m_ssrc >> 8) & 0xFF);
rtp[11] = uint8_t(m_ssrc & 0xFF);
// Concatenate header + payload
rtc::binary packet;
packet.resize(rtp.size() + opusBytes);
std::memcpy(packet.data(), rtp.data(), rtp.size());
std::memcpy(packet.data() + rtp.size(), opusData, opusBytes);
// Send RTP on the media track
m_track->send(packet); // libdatachannel expects RTP bytes on tracks
// Advance RTP state
++m_seq;
// For Opus @ 48k clock, timestamp increments by the PCM frame count per channel
// (do not multiply by channels)
m_ts += static_cast<uint32_t>(pcmFramesPerChannel);
} }
private: private:
Params m_p; Params m_p;
std::shared_ptr<rtc::PeerConnection> m_pc; std::shared_ptr<rtc::PeerConnection> m_pc;
std::shared_ptr<rtc::AudioSource> m_audioSource;
std::shared_ptr<rtc::Track> m_track; std::shared_ptr<rtc::Track> m_track;
std::optional<std::string> m_resourceUrl; std::optional<std::string> m_resourceUrl;
std::mutex m_mtx; std::mutex m_mtx;
std::atomic<bool> m_started{false}; std::atomic<bool> m_started{false};
// RTP state
uint32_t m_ssrc = 0;
uint16_t m_seq = 0;
uint32_t m_ts = 0;
uint8_t m_pt = 111; // dynamic payload type commonly used for Opus
// --- WHIP HTTP helpers (mTLS-capable) --- // --- WHIP HTTP helpers (mTLS-capable) ---
static std::tuple<std::string,std::string> ExtractAnswerAndLocation(const httplib::Result& r) { static std::tuple<std::string, std::string> ExtractAnswerAndLocation(const httplib::Result &r)
// WHIP server responds 201 Created, body = SDP answer (text/plain), {
// Location header = resource URL for PATCH candidates. if (!r)
if (!r) throw std::runtime_error("No HTTP result"); throw std::runtime_error("No HTTP result");
if (r->status != 201 && r->status != 200) if (r->status != 201 && r->status != 200)
throw std::runtime_error("Unexpected WHIP status: " + std::to_string(r->status)); throw std::runtime_error("Unexpected WHIP status: " + std::to_string(r->status));
std::string answer = r->body; std::string answer = r->body;
std::string resourceUrl; std::string resourceUrl;
if (r->has_header("Location")) resourceUrl = r->get_header_value("Location"); if (r->has_header("Location"))
resourceUrl = r->get_header_value("Location");
return {answer, resourceUrl}; return {answer, resourceUrl};
} }
std::pair<std::string,std::string> PostOfferWHIP(const std::string& sdpOffer) { std::pair<std::string, std::string> PostOfferWHIP(const std::string &sdpOffer)
auto [cli, path] = MakeClientForUrl(m_p.whipUrl); {
// WHIP requires: auto [cli, path] = MakeClientForUrl(sdpOffer, m_p.whipUrl);
// POST <endpoint> Content-Type: application/sdp Body: offer SDP
// Response: 201 Created, body: answer SDP, Location: resource URL
auto res = cli->Post(path.c_str(), sdpOffer, "application/sdp"); auto res = cli->Post(path.c_str(), sdpOffer, "application/sdp");
auto [answer, resUrl] = ExtractAnswerAndLocation(res); auto [answer, resUrl] = ExtractAnswerAndLocation(res);
return {answer, resUrl}; return {answer, resUrl};
} }
void PatchCandidateWHIP(const rtc::Candidate& cand) { void PatchCandidateWHIP(const rtc::Candidate &cand)
if (!m_resourceUrl) return; {
auto [cli, path] = MakeClientForUrl(*m_resourceUrl); if (!m_resourceUrl)
// WHIP trickle: PATCH resource with Content-Type: application/trickle-ice-sdpfrag return;
// Body is an SDP fragment with "a=candidate:..." auto [cli, path] = MakeClientForUrl(std::string(""), *m_resourceUrl);
std::string frag = "a=" + std::string(cand); std::string frag = "a=" + std::string(cand);
auto res = cli->Patch(path.c_str(), frag, "application/trickle-ice-sdpfrag"); auto res = cli->Patch(path.c_str(), frag, "application/trickle-ice-sdpfrag");
if (!res || (res->status < 200 || res->status >= 300)) { if (!res || (res->status < 200 || res->status >= 300))
{
spdlog::warn("WHIP PATCH {} -> {}", path, res ? res->status : -1); spdlog::warn("WHIP PATCH {} -> {}", path, res ? res->status : -1);
} }
} }
// Parse URL, return httplib client + path, configured with mTLS if https // Build client + path for URL; uses mTLS when https
static std::pair<std::unique_ptr<httplib::Client>, std::string> std::pair<std::unique_ptr<httplib::SSLClient>, std::string>
MakeClientForUrl(const std::string& url) { MakeClientForUrl(const std::string &body, const std::string &url)
// scheme://host[:port]/path... {
(void)body;
static const std::regex re(R"(^(https?)://([^/:]+)(?::(\d+))?(/.*)?$)"); static const std::regex re(R"(^(https?)://([^/:]+)(?::(\d+))?(/.*)?$)");
std::smatch m; std::smatch m;
if (!std::regex_match(url, m, re)) { if (!std::regex_match(url, m, re))
throw std::runtime_error("Invalid URL: " + url); throw std::runtime_error("Invalid URL: " + url);
}
std::string scheme = m[1].str(); std::string scheme = m[1].str();
std::string host = m[2].str(); std::string host = m[2].str();
int port = m[3].matched ? std::stoi(m[3].str()) : (scheme=="https"?443:80); int port = m[3].matched ? std::stoi(m[3].str()) : (scheme == "https" ? 443 : 80);
std::string path = m[4].matched ? m[4].str() : "/"; std::string path = m[4].matched ? m[4].str() : "/";
// We need the mTLS params; capture via thread-local? Well pass via this pointer. if (scheme == "https")
// To access instance fields here, make this non-static or pass params through a lambda. {
// Simpler: look up global thread-local; or we restructure to capture this.
// For cleanliness, well make a non-static wrapper below.
throw std::logic_error("Use MakeClientForUrl_inst instead");
}
std::pair<std::unique_ptr<httplib::Client>, std::string>
MakeClientForUrl_inst(const std::string& url) {
static const std::regex re(R"(^(https?)://([^/:]+)(?::(\d+))?(/.*)?$)");
std::smatch m;
if (!std::regex_match(url, m, re)) {
throw std::runtime_error("Invalid URL: " + url);
}
std::string scheme = m[1].str();
std::string host = m[2].str();
int port = m[3].matched ? std::stoi(m[3].str()) : (scheme=="https"?443:80);
std::string path = m[4].matched ? m[4].str() : "/";
if (scheme == "https") {
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT #ifndef CPPHTTPLIB_OPENSSL_SUPPORT
throw std::runtime_error("https URL but CPPHTTPLIB_OPENSSL_SUPPORT not enabled"); throw std::runtime_error("https URL but CPPHTTPLIB_OPENSSL_SUPPORT not enabled");
#else #else
auto cli = std::make_unique<httplib::SSLClient>(host.c_str(), port); auto cli = std::make_unique<httplib::SSLClient>(host.c_str(), port, m_p.crtPath, m_p.keyPath, std::string());
cli->enable_server_certificate_verification(true); cli->enable_server_certificate_verification(true);
cli->set_ca_cert_path(m_p.caPath.c_str()); cli->set_ca_cert_path(m_p.caPath.c_str());
cli->set_client_cert_file(m_p.crtPath.c_str(), m_p.keyPath.c_str(), nullptr);
cli->set_connection_timeout(10); cli->set_connection_timeout(10);
cli->set_read_timeout(60); cli->set_read_timeout(60);
cli->set_write_timeout(60); cli->set_write_timeout(60);
return {std::move(cli), path}; return {std::move(cli), path};
#endif #endif
} else {
auto cli = std::make_unique<httplib::Client>(host.c_str(), port);
cli->set_connection_timeout(10);
cli->set_read_timeout(60);
cli->set_write_timeout(60);
return {std::move(cli), path};
} }
// else
// {
// auto cli = std::make_unique<httplib::Client>(host.c_str(), port);
// cli->set_connection_timeout(10);
// cli->set_read_timeout(60);
// cli->set_write_timeout(60);
// return {std::move(cli), path};
// }
} }
};
// Overload to call instance version
std::pair<std::unique_ptr<httplib::Client>, std::string>
MakeClientForUrl(const std::string& url) {
return MakeClientForUrl_inst(url);
}
};
} // namespace snoop } // namespace snoop

View File

@@ -49,20 +49,12 @@ namespace snoop
// WHIP-only streamer // WHIP-only streamer
AudioStreamService streamer(configService); AudioStreamService streamer(configService);
// PCM tap -> feed WHIP
std::function<void(const float *, int)> pcmTap =
[&](const float *interleaved, int frames)
{
streamer.OnPCM(interleaved, static_cast<size_t>(frames));
};
// Encoder: keep writing to local Ogg via writerService; (no Socket.IO send) // Encoder: keep writing to local Ogg via writerService; (no Socket.IO send)
TAudioEncoder encoder(sampleRate, channels, framesPerBuffer, [&](auto input, auto size) -> int TAudioEncoder encoder(sampleRate, channels, framesPerBuffer, [&](auto input, auto size) -> int
{ {
writerService->WriteAudioData(input, size, framesPerBuffer); writerService->WriteAudioData(input, size, framesPerBuffer);
return paContinue; }, streamer.OnOpus(reinterpret_cast<const unsigned char*>(input),static_cast<size_t>(size),framesPerBuffer);
pcmTap // NEW: raw PCM to WHIP return paContinue; });
);
snoop::DeviceControlService::Controls controls{ snoop::DeviceControlService::Controls controls{
.stopStreamNow = [&]() .stopStreamNow = [&]()
@@ -70,13 +62,13 @@ namespace snoop
.stopRecordingNow = [&]() .stopRecordingNow = [&]()
{ writerService->StopRecordingNow(); }}; { writerService->StopRecordingNow(); }};
g_taskSvc = std::make_unique<snoop::DeviceControlService>(configService, handlers, controls); snoop::DeviceControlService::Handlers handlers{};
static std::unique_ptr<snoop::DeviceControlService> g_taskSvc;
// ------------------------------ // ------------------------------
{ {
snoop::DeviceControlService::Handlers handlers{};
handlers.onStartStream = [](const snoop::DeviceControlService::Task &t) handlers.onStartStream = [&](const snoop::DeviceControlService::Task &t)
{ {
spdlog::info("start_stream payload: {}", t.payload.dump()); spdlog::info("start_stream payload: {}", t.payload.dump());
// TODO: start your streaming pipeline using payload["whipUrl"], etc. // TODO: start your streaming pipeline using payload["whipUrl"], etc.
@@ -96,7 +88,7 @@ namespace snoop
bool ok = streamer.StartWhip(whipUrl, sampleRate, channels); bool ok = streamer.StartWhip(whipUrl, sampleRate, channels);
return snoop::DeviceControlService::HandlerResult{ok, R"({"whip":"started"})", ok ? "" : "failed"}; return snoop::DeviceControlService::HandlerResult{ok, R"({"whip":"started"})", ok ? "" : "failed"};
}; };
handlers.onStopStream = [](const snoop::DeviceControlService::Task &t) handlers.onStopStream = [&](const snoop::DeviceControlService::Task &t)
{ {
spdlog::info("stop_stream payload: {}", t.payload.dump()); spdlog::info("stop_stream payload: {}", t.payload.dump());
// TODO: stop streaming // TODO: stop streaming
@@ -191,11 +183,10 @@ namespace snoop
return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()}; return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()};
} }
}; };
// static std::unique_ptr<snoop::DeviceControlService> g_taskSvc;
// g_taskSvc = std::make_unique<snoop::DeviceControlService>(configService, handlers);
} }
g_taskSvc = std::make_unique<snoop::DeviceControlService>(configService, handlers, controls);
while (true) while (true)
{ {
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));

View File

@@ -1,33 +1,49 @@
#!/bin/bash #!/bin/bash
SYSROOT="$(pwd)/sysroot" SYSROOT="$(pwd)/sysroot"
CLANG_PATH=/usr/local/opt/llvm@16/bin # CLANG_PATH=/usr/local/opt/llvm@16/bin
CLANG_PATH="/usr/lib/llvm-14/bin"
export PATH="$CLANG_PATH:$PATH" export PATH="$CLANG_PATH:$PATH"
export CC="clang" export CC="clang"
export CXX="clang++" export CXX="clang++"
export LD="ld.lld" export LD="ld.lld"
export PKG_CONFIG_PATH=$SYSROOT/usr/lib/arm-linux-gnueabihf/pkgconfig # export PKG_CONFIG_PATH=$SYSROOT/usr/lib/arm-linux-gnueabihf/pkgconfig
export CFLAGS="--target=armv7-linux-gnueabihf -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard --sysroot=$SYSROOT" # export CFLAGS="--target=armv7-linux-gnueabihf -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard --sysroot=$SYSROOT"
export CXXFLAGS="--target=armv7-linux-gnueabihf -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard --sysroot=$SYSROOT" # export CXXFLAGS="--target=armv7-linux-gnueabihf -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard --sysroot=$SYSROOT"
export LDFLAGS="--target=armv7-linux-gnueabihf -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard -fuse-ld=lld --sysroot=$SYSROOT" # export LDFLAGS="--target=armv7-linux-gnueabihf -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard -fuse-ld=lld --sysroot=$SYSROOT"
BUILD_DIR="./build" BUILD_DIR="./build"
# mkdir -p "$BUILD_DIR"
# rm -rf "$BUILD_DIR"/*
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR" mkdir -p "$BUILD_DIR"
rm -rf "$BUILD_DIR"/*
cd "$BUILD_DIR" cd "$BUILD_DIR"
# cmake -GNinja \
# -DCMAKE_SYSTEM_NAME=Linux \
# -DCMAKE_SYSROOT="$SYSROOT" \
# -DUSE_ALSA_ADAPTER=ON \
# ../../
# # -DCMAKE_BUILD_TYPE=Debug \
# # -DCMAKE_C_FLAGS="$CFLAGS -g" \
# # -DCMAKE_CXX_FLAGS="$CXXFLAGS -g" \
# # -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS -g" \
# # ../../
cmake -GNinja \ cmake -GNinja \
-DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSROOT="$SYSROOT" \
-DUSE_ALSA_ADAPTER=ON \ -DUSE_ALSA_ADAPTER=ON \
../../ -DCMAKE_BUILD_TYPE=Release \
# -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_C_COMPILER="$CC" \
# -DCMAKE_C_FLAGS="$CFLAGS -g" \ -DCMAKE_CXX_COMPILER="$CXX" \
# -DCMAKE_CXX_FLAGS="$CXXFLAGS -g" \ -DCMAKE_C_FLAGS="$CFLAGS" \
# -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS -g" \ -DCMAKE_CXX_FLAGS="$CXXFLAGS" \
# ../../ -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" \
..
ninja ninja