// src/Services/AudioStreamService.h #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "WhipClient.h" #include "ConfigService.h" namespace snoop { class AudioStreamService { std::shared_ptr m_cfg; // WHIP std::unique_ptr m_whip; std::mutex m_whipMutex; std::string m_tmpKeyPath; // temp key extracted from keyctl (deleted on Stop) public: explicit AudioStreamService(std::shared_ptr cfg) : m_cfg(std::move(cfg)) {} ~AudioStreamService() { StopWhip(); } // Feed raw PCM (float32 interleaved), frames = samples per channel void OnOpus(const unsigned char* opusData, size_t opusBytes, int pcmFramesPerChannel) { std::lock_guard lk(m_whipMutex); if (m_whip) m_whip->PushOpus(opusData, opusBytes, pcmFramesPerChannel); } bool StartWhip(const std::string& whipUrl, int sampleRate=48000, int channels=1) { std::lock_guard lk(m_whipMutex); if (m_whip) { spdlog::info("WHIP already started"); return true; } if (!m_cfg) { spdlog::error("StartWhip requires ConfigService"); return false; } // certs from enrollment std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; if (!std::filesystem::exists(ca)) ca = "/etc/iot/keys/ca_chain.pem"; std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; // extract client key via keyctl auto tmpKey = ExtractClientKeyTemp(); if (!tmpKey) { spdlog::error("Cannot extract client key for WHIP (keyctl user iot-client-key)"); return false; } WhipClient::Params p{ .whipUrl = whipUrl, .caPath = ca.string(), .crtPath = crt.string(), .keyPath = tmpKey->string(), .sampleRate= sampleRate, .channels = channels }; m_whip = std::make_unique(p); try { m_whip->Start(); spdlog::info("WHIP started"); m_tmpKeyPath = *tmpKey; return true; } catch (const std::exception& e) { spdlog::error("WHIP start failed: {}", e.what()); std::error_code ec; std::filesystem::remove(*tmpKey, ec); m_whip.reset(); return false; } } void StopWhip() { std::lock_guard lk(m_whipMutex); if (m_whip) { m_whip->Stop(); m_whip.reset(); } if (!m_tmpKeyPath.empty()) { std::error_code ec; std::filesystem::remove(m_tmpKeyPath, ec); m_tmpKeyPath.clear(); } } private: static std::optional ExtractClientKeyTemp() { auto exec = [](const std::string& cmd) { std::array buf{}; std::string out; FILE* pipe = popen((cmd + " 2>&1").c_str(), "r"); if (!pipe) return std::string{}; while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr) out.append(buf.data()); pclose(pipe); return out; }; auto trim = [](std::string s){ auto b=s.find_first_not_of(" \t\r\n"), e=s.find_last_not_of(" \t\r\n"); return (b==std::string::npos) ? std::string{} : s.substr(b, e-b+1); }; std::string id = trim(exec("keyctl search @s user iot-client-key | tail -n1")); if (id.empty()) return std::nullopt; char tmpl[] = "/run/iot-whip-keyXXXXXX"; int fd = mkstemp(tmpl); if (fd < 0) return std::nullopt; close(fd); std::filesystem::path p(tmpl); exec("keyctl pipe " + id + " > " + p.string()); if (!std::filesystem::exists(p) || std::filesystem::file_size(p) == 0) { std::error_code ec; std::filesystem::remove(p, ec); return std::nullopt; } return p; } }; } // namespace snoop