prism-tui PoC

This commit is contained in:
Boof2015
2026-08-14 13:37:16 -04:00
parent 1a0185306a
commit 7047df3e38
41 changed files with 1995 additions and 635 deletions
+77 -149
View File
@@ -1,4 +1,4 @@
#include "linux_capture.h"
#include "system_audio_capture.h"
#if defined(__linux__)
@@ -406,150 +406,111 @@ private:
bool started_ = false;
};
class LinuxNativeCaptureEngine {
class LinuxNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture {
public:
Napi::Value GetSupport(const Napi::CallbackInfo& info) const {
Napi::Object support = Napi::Object::New(info.Env());
~LinuxNativeCaptureEngine() override {
stop();
}
Prism::Capture::Support getSupport() const override {
PulseContextConnection connection;
std::string errorMessage;
std::vector<OutputDeviceInfo> devices;
const bool available =
connection.connect("Prism Linux Capture Probe", &errorMessage) &&
connection.enumerateOutputDevices(&devices, &errorMessage);
support.Set("available", Napi::Boolean::New(info.Env(), available));
if (available) {
support.Set("reason", info.Env().Null());
} else {
const std::string reason = errorMessage.empty()
return {
available,
available ? std::string() : (errorMessage.empty()
? "Native Linux capture is unavailable."
: errorMessage;
support.Set("reason", Napi::String::New(info.Env(), reason));
}
return support;
: errorMessage),
};
}
Napi::Value ListOutputDevices(const Napi::CallbackInfo& info) const {
Napi::Env env = info.Env();
Napi::Array devicesArray = Napi::Array::New(env);
std::vector<Prism::Capture::OutputDevice> listOutputDevices() override {
PulseContextConnection connection;
std::string errorMessage;
std::vector<OutputDeviceInfo> devices;
if (!connection.connect("Prism Linux Capture Devices", &errorMessage) ||
!connection.enumerateOutputDevices(&devices, &errorMessage)) {
return devicesArray;
return {};
}
for (size_t index = 0; index < devices.size(); ++index) {
const auto& device = devices[index];
Napi::Object entry = Napi::Object::New(env);
entry.Set("id", Napi::String::New(env, device.id));
entry.Set("label", Napi::String::New(env, device.label));
entry.Set("kind", Napi::String::New(env, "system"));
entry.Set("isDefault", Napi::Boolean::New(env, device.isDefault));
entry.Set(
"sampleRate",
Napi::Number::New(env, static_cast<double>(device.sampleSpec.rate)));
entry.Set(
"channelCount",
Napi::Number::New(env, static_cast<double>(device.sampleSpec.channels)));
devicesArray.Set(static_cast<uint32_t>(index), entry);
std::vector<Prism::Capture::OutputDevice> result;
result.reserve(devices.size());
for (const auto& device : devices) {
result.push_back({
device.id,
device.label,
static_cast<double>(device.sampleSpec.rate),
static_cast<uint32_t>(device.sampleSpec.channels),
device.isDefault,
});
}
return devicesArray;
}
Napi::Value Start(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string requestedDeviceId;
if (info.Length() > 0 && info[0].IsString()) {
requestedDeviceId = info[0].As<Napi::String>().Utf8Value();
}
std::string errorMessage;
if (!startInternal(requestedDeviceId, &errorMessage)) {
const std::string reason = errorMessage.empty()
? "Native Linux monitor capture failed to start."
: errorMessage;
Napi::Error::New(env, reason)
.ThrowAsJavaScriptException();
return env.Null();
}
std::lock_guard<std::mutex> lock(stateMutex_);
Napi::Object result = Napi::Object::New(env);
result.Set("sampleRate", Napi::Number::New(env, sampleRate_));
result.Set("channelCount", Napi::Number::New(env, static_cast<double>(channelCount_)));
result.Set("deviceId", Napi::String::New(env, activeDeviceId_));
result.Set("deviceLabel", Napi::String::New(env, activeDeviceLabel_));
return result;
}
Napi::Value Stop(const Napi::CallbackInfo& info) {
stopInternal();
return info.Env().Undefined();
bool start(const std::string& requestedDeviceId,
Prism::Capture::StartResult* result,
std::string* errorMessage) override {
if (!startInternal(requestedDeviceId, errorMessage)) {
if (errorMessage != nullptr && errorMessage->empty()) {
*errorMessage = "Native Linux monitor capture failed to start.";
}
return false;
}
if (result != nullptr) {
std::lock_guard<std::mutex> lock(stateMutex_);
result->sampleRate = sampleRate_;
result->channelCount = channelCount_;
result->deviceId = activeDeviceId_;
result->deviceLabel = activeDeviceLabel_;
}
return true;
}
Napi::Value Drain(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
const size_t maxChunks = info.Length() > 0 && info[0].IsNumber()
? std::max<size_t>(1, info[0].As<Napi::Number>().Uint32Value())
: kDefaultDrainChunkLimit;
std::deque<CapturedChunk> drainedChunks;
size_t overwriteCount = 0;
size_t queueDepth = 0;
void stop() override {
stopInternal();
}
Prism::Capture::DrainResult drain(size_t maxChunks) override {
const size_t drainLimit = maxChunks == 0
? kDefaultDrainChunkLimit
: std::min(maxChunks, kMaxQueuedChunks);
std::deque<CapturedChunk> drained;
Prism::Capture::DrainResult result;
{
std::lock_guard<std::mutex> lock(chunkMutex_);
const size_t chunkCount = std::min(maxChunks, chunkQueue_.size());
for (size_t index = 0; index < chunkCount; ++index) {
drainedChunks.push_back(std::move(chunkQueue_.front()));
const size_t count = std::min(drainLimit, chunkQueue_.size());
for (size_t index = 0; index < count; ++index) {
drained.push_back(std::move(chunkQueue_.front()));
chunkQueue_.pop_front();
}
overwriteCount = overwriteCount_;
result.overwriteCount = overwriteCount_;
overwriteCount_ = 0;
queueDepth = chunkQueue_.size();
result.queueDepth = chunkQueue_.size();
}
Napi::Array chunks = Napi::Array::New(env, drainedChunks.size());
for (size_t index = 0; index < drainedChunks.size(); ++index) {
const auto& chunk = drainedChunks[index];
Napi::Object entry = Napi::Object::New(env);
Napi::Float32Array left = Napi::Float32Array::New(env, chunk.left.size());
Napi::Float32Array right = Napi::Float32Array::New(env, chunk.right.size());
if (!chunk.left.empty()) {
std::memcpy(left.Data(), chunk.left.data(), chunk.left.size() * sizeof(float));
}
if (!chunk.right.empty()) {
std::memcpy(right.Data(), chunk.right.data(), chunk.right.size() * sizeof(float));
}
entry.Set("left", left);
entry.Set("right", right);
entry.Set(
"channelCount",
Napi::Number::New(env, static_cast<double>(chunk.channelCount)));
entry.Set(
"capturedAtMilliseconds",
Napi::Number::New(env, chunk.capturedAtMilliseconds));
entry.Set(
"sequence",
Napi::Number::New(env, static_cast<double>(chunk.sequence)));
chunks.Set(static_cast<uint32_t>(index), entry);
result.chunks.reserve(drained.size());
while (!drained.empty()) {
auto chunk = std::move(drained.front());
drained.pop_front();
result.chunks.push_back({
std::move(chunk.left),
std::move(chunk.right),
chunk.channelCount,
chunk.capturedAtMilliseconds,
chunk.sequence,
});
}
Napi::Object result = Napi::Object::New(env);
result.Set("chunks", chunks);
result.Set(
"overwriteCount", Napi::Number::New(env, static_cast<double>(overwriteCount)));
result.Set("queueDepth", Napi::Number::New(env, static_cast<double>(queueDepth)));
return result;
}
Napi::Value NowMilliseconds(const Napi::CallbackInfo& info) const {
return Napi::Number::New(info.Env(), monotonicMilliseconds());
double nowMilliseconds() const override {
return monotonicMilliseconds();
}
const char* backendName() const override {
return "PulseAudio";
}
private:
@@ -885,47 +846,14 @@ private:
size_t overwriteCount_ = 0;
};
LinuxNativeCaptureEngine& GetLinuxNativeCaptureEngine() {
static LinuxNativeCaptureEngine engine;
return engine;
}
Napi::Value LinuxGetSupport(const Napi::CallbackInfo& info) {
return GetLinuxNativeCaptureEngine().GetSupport(info);
}
Napi::Value LinuxListOutputDevices(const Napi::CallbackInfo& info) {
return GetLinuxNativeCaptureEngine().ListOutputDevices(info);
}
Napi::Value LinuxStart(const Napi::CallbackInfo& info) {
return GetLinuxNativeCaptureEngine().Start(info);
}
Napi::Value LinuxStop(const Napi::CallbackInfo& info) {
return GetLinuxNativeCaptureEngine().Stop(info);
}
Napi::Value LinuxDrain(const Napi::CallbackInfo& info) {
return GetLinuxNativeCaptureEngine().Drain(info);
}
Napi::Value LinuxNowMilliseconds(const Napi::CallbackInfo& info) {
return GetLinuxNativeCaptureEngine().NowMilliseconds(info);
}
} // namespace
void RegisterLinuxCapture(Napi::Env env, Napi::Object exports) {
Napi::Object captureExports = Napi::Object::New(env);
captureExports.Set("getSupport", Napi::Function::New(env, LinuxGetSupport));
captureExports.Set(
"listOutputDevices", Napi::Function::New(env, LinuxListOutputDevices));
captureExports.Set("start", Napi::Function::New(env, LinuxStart));
captureExports.Set("stop", Napi::Function::New(env, LinuxStop));
captureExports.Set("drain", Napi::Function::New(env, LinuxDrain));
captureExports.Set("nowMilliseconds", Napi::Function::New(env, LinuxNowMilliseconds));
exports.Set("linuxCapture", captureExports);
namespace Prism::Capture {
std::unique_ptr<SystemAudioCapture> createSystemAudioCapture() {
return std::make_unique<LinuxNativeCaptureEngine>();
}
} // namespace Prism::Capture
#endif
-5
View File
@@ -1,5 +0,0 @@
#pragma once
#include <napi.h>
void RegisterLinuxCapture(Napi::Env env, Napi::Object exports);
-54
View File
@@ -1,54 +0,0 @@
#include "linux_capture.h"
namespace {
Napi::Value GetSupport(const Napi::CallbackInfo& info) {
Napi::Object support = Napi::Object::New(info.Env());
support.Set("available", Napi::Boolean::New(info.Env(), false));
support.Set(
"reason",
Napi::String::New(
info.Env(), "Native Linux output-device capture is unavailable on this platform."));
return support;
}
Napi::Value ListOutputDevices(const Napi::CallbackInfo& info) {
return Napi::Array::New(info.Env());
}
Napi::Value Start(const Napi::CallbackInfo& info) {
Napi::Error::New(
info.Env(), "Native Linux output-device capture is unavailable on this platform.")
.ThrowAsJavaScriptException();
return info.Env().Undefined();
}
Napi::Value Stop(const Napi::CallbackInfo& info) {
return info.Env().Undefined();
}
Napi::Value Drain(const Napi::CallbackInfo& info) {
Napi::Object result = Napi::Object::New(info.Env());
result.Set("chunks", Napi::Array::New(info.Env()));
result.Set("overwriteCount", Napi::Number::New(info.Env(), 0));
result.Set("queueDepth", Napi::Number::New(info.Env(), 0));
return result;
}
Napi::Value NowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), 0);
}
} // namespace
void RegisterLinuxCapture(Napi::Env env, Napi::Object exports) {
Napi::Object captureExports = Napi::Object::New(env);
captureExports.Set("getSupport", Napi::Function::New(env, GetSupport));
captureExports.Set(
"listOutputDevices", Napi::Function::New(env, ListOutputDevices));
captureExports.Set("start", Napi::Function::New(env, Start));
captureExports.Set("stop", Napi::Function::New(env, Stop));
captureExports.Set("drain", Napi::Function::New(env, Drain));
captureExports.Set("nowMilliseconds", Napi::Function::New(env, NowMilliseconds));
exports.Set("linuxCapture", captureExports);
}
-5
View File
@@ -1,5 +0,0 @@
#pragma once
#include <napi.h>
void RegisterMacOSCapture(Napi::Env env, Napi::Object exports);
+68 -150
View File
@@ -1,4 +1,4 @@
#include "macos_capture.h"
#include "system_audio_capture.h"
#if defined(__APPLE__)
@@ -319,140 +319,104 @@ std::string formatStatusMessage(const char* operation, OSStatus status) {
return std::string(operation) + " failed (" + std::to_string(static_cast<int>(status)) + ")";
}
class MacOSNativeCaptureEngine {
class MacOSNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture {
public:
Napi::Object GetSupport(Napi::Env env) {
Napi::Object support = Napi::Object::New(env);
if (@available(macOS 14.2, *)) {
support.Set("available", Napi::Boolean::New(env, true));
support.Set("reason", env.Null());
return support;
}
support.Set("available", Napi::Boolean::New(env, false));
support.Set(
"reason",
Napi::String::New(env, "Native output-device capture requires macOS 14.2 or newer."));
return support;
~MacOSNativeCaptureEngine() override {
stop();
}
Napi::Array ListOutputDevices(Napi::Env env) {
Napi::Array result = Napi::Array::New(env);
Prism::Capture::Support getSupport() const override {
if (@available(macOS 14.2, *)) {
return {true, {}};
}
return {false, "Native output-device capture requires macOS 14.2 or newer."};
}
std::vector<Prism::Capture::OutputDevice> listOutputDevices() override {
std::vector<Prism::Capture::OutputDevice> result;
if (!isSupported()) {
return result;
}
const auto devices = enumerateOutputDevices();
for (size_t index = 0; index < devices.size(); ++index) {
const auto& device = devices[index];
Napi::Object entry = Napi::Object::New(env);
entry.Set("id", Napi::String::New(env, device.uid));
entry.Set("label", Napi::String::New(env, device.label));
entry.Set("kind", Napi::String::New(env, "system"));
entry.Set("isDefault", Napi::Boolean::New(env, device.isDefault));
entry.Set("sampleRate", Napi::Number::New(env, device.sampleRate));
entry.Set(
"channelCount",
Napi::Number::New(env, static_cast<double>(device.channelCount)));
result.Set(static_cast<uint32_t>(index), entry);
result.reserve(devices.size());
for (const auto& device : devices) {
result.push_back({
device.uid,
device.label,
device.sampleRate,
static_cast<uint32_t>(device.channelCount),
device.isDefault,
});
}
return result;
}
Napi::Object Start(Napi::Env env, const std::string& requestedDeviceUid) {
Napi::Object result = Napi::Object::New(env);
const Napi::Object support = GetSupport(env);
if (!support.Get("available").As<Napi::Boolean>().Value()) {
Napi::Error::New(
env, support.Get("reason").As<Napi::String>().Utf8Value())
.ThrowAsJavaScriptException();
return result;
bool start(const std::string& requestedDeviceId,
Prism::Capture::StartResult* result,
std::string* errorMessage) override {
const auto support = getSupport();
if (!support.available) {
if (errorMessage != nullptr) {
*errorMessage = support.reason;
}
return false;
}
std::string errorMessage;
if (!startInternal(requestedDeviceUid, &errorMessage)) {
Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException();
return result;
if (!startInternal(requestedDeviceId, errorMessage)) {
return false;
}
std::lock_guard<std::mutex> lock(stateMutex_);
result.Set("sampleRate", Napi::Number::New(env, sampleRate_));
result.Set("channelCount", Napi::Number::New(env, static_cast<double>(channelCount_)));
result.Set("deviceId", Napi::String::New(env, activeDeviceUid_));
result.Set("deviceLabel", Napi::String::New(env, activeDeviceLabel_));
return result;
if (result != nullptr) {
std::lock_guard<std::mutex> lock(stateMutex_);
result->sampleRate = sampleRate_;
result->channelCount = static_cast<uint32_t>(channelCount_);
result->deviceId = activeDeviceUid_;
result->deviceLabel = activeDeviceLabel_;
}
return true;
}
void Stop() {
void stop() override {
std::lock_guard<std::mutex> lock(stateMutex_);
stopLocked();
}
Napi::Object Drain(Napi::Env env, size_t maxChunks) {
Prism::Capture::DrainResult drain(size_t maxChunks) override {
const size_t drainLimit =
maxChunks == 0 ? kDefaultDrainChunkLimit : std::min(maxChunks, kMaxQueuedChunks);
std::deque<CapturedChunk> drained;
uint64_t overwriteCount = 0;
Prism::Capture::DrainResult result;
{
std::lock_guard<std::mutex> queueLock(chunkMutex_);
overwriteCount = overwriteCount_;
result.overwriteCount = overwriteCount_;
const size_t count = std::min(drainLimit, chunkQueue_.size());
for (size_t index = 0; index < count; ++index) {
drained.push_back(std::move(chunkQueue_.front()));
chunkQueue_.pop_front();
}
result.queueDepth = chunkQueue_.size();
}
Napi::Array chunks = Napi::Array::New(env, drained.size());
for (size_t index = 0; index < drained.size(); ++index) {
CapturedChunk& chunk = drained[index];
Napi::Object entry = Napi::Object::New(env);
Napi::Float32Array left =
Napi::Float32Array::New(env, chunk.left.size());
Napi::Float32Array right =
Napi::Float32Array::New(env, chunk.right.size());
if (!chunk.left.empty()) {
std::memcpy(
left.Data(), chunk.left.data(), chunk.left.size() * sizeof(float));
}
if (!chunk.right.empty()) {
std::memcpy(
right.Data(), chunk.right.data(), chunk.right.size() * sizeof(float));
}
entry.Set("left", left);
entry.Set("right", right);
entry.Set(
"channelCount",
Napi::Number::New(env, static_cast<double>(chunk.channelCount)));
entry.Set(
"capturedAtMilliseconds",
Napi::Number::New(env, chunk.capturedAtMilliseconds));
entry.Set(
"sequence",
Napi::Number::New(env, static_cast<double>(chunk.sequence)));
chunks.Set(static_cast<uint32_t>(index), entry);
result.chunks.reserve(drained.size());
while (!drained.empty()) {
auto chunk = std::move(drained.front());
drained.pop_front();
result.chunks.push_back({
std::move(chunk.left),
std::move(chunk.right),
static_cast<uint32_t>(chunk.channelCount),
chunk.capturedAtMilliseconds,
chunk.sequence,
});
}
Napi::Object result = Napi::Object::New(env);
result.Set("chunks", chunks);
result.Set(
"overwriteCount",
Napi::Number::New(env, static_cast<double>(overwriteCount)));
result.Set(
"queueDepth",
Napi::Number::New(env, static_cast<double>(chunkQueue_.size())));
return result;
}
double NowMilliseconds() const {
double nowMilliseconds() const override {
return monotonicMilliseconds();
}
const char* backendName() const override {
return "CoreAudio";
}
private:
static OSStatus StaticIOProc(AudioObjectID inDevice,
const AudioTimeStamp* inNow,
@@ -780,60 +744,14 @@ private:
UInt32 channelCount_ = 2;
};
MacOSNativeCaptureEngine& engine() {
static MacOSNativeCaptureEngine instance;
return instance;
}
Napi::Value MacOSGetSupport(const Napi::CallbackInfo& info) {
return engine().GetSupport(info.Env());
}
Napi::Value MacOSListOutputDevices(const Napi::CallbackInfo& info) {
return engine().ListOutputDevices(info.Env());
}
Napi::Value MacOSStart(const Napi::CallbackInfo& info) {
std::string requestedDeviceUid;
if (info.Length() >= 1 && info[0].IsString()) {
requestedDeviceUid = info[0].As<Napi::String>().Utf8Value();
}
return engine().Start(info.Env(), requestedDeviceUid);
}
Napi::Value MacOSStop(const Napi::CallbackInfo& info) {
engine().Stop();
return info.Env().Undefined();
}
Napi::Value MacOSDrain(const Napi::CallbackInfo& info) {
size_t maxChunks = kDefaultDrainChunkLimit;
if (info.Length() >= 1 && info[0].IsNumber()) {
const int64_t requested = info[0].As<Napi::Number>().Int64Value();
if (requested > 0) {
maxChunks = static_cast<size_t>(requested);
}
}
return engine().Drain(info.Env(), maxChunks);
}
Napi::Value MacOSNowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), engine().NowMilliseconds());
}
} // namespace
void RegisterMacOSCapture(Napi::Env env, Napi::Object exports) {
Napi::Object captureExports = Napi::Object::New(env);
captureExports.Set("getSupport", Napi::Function::New(env, MacOSGetSupport));
captureExports.Set(
"listOutputDevices", Napi::Function::New(env, MacOSListOutputDevices));
captureExports.Set("start", Napi::Function::New(env, MacOSStart));
captureExports.Set("stop", Napi::Function::New(env, MacOSStop));
captureExports.Set("drain", Napi::Function::New(env, MacOSDrain));
captureExports.Set(
"nowMilliseconds", Napi::Function::New(env, MacOSNowMilliseconds));
exports.Set("macosCapture", captureExports);
namespace Prism::Capture {
std::unique_ptr<SystemAudioCapture> createSystemAudioCapture() {
return std::make_unique<MacOSNativeCaptureEngine>();
}
} // namespace Prism::Capture
#endif // defined(__APPLE__)
-54
View File
@@ -1,54 +0,0 @@
#include "macos_capture.h"
namespace {
Napi::Value GetSupport(const Napi::CallbackInfo& info) {
Napi::Object support = Napi::Object::New(info.Env());
support.Set("available", Napi::Boolean::New(info.Env(), false));
support.Set(
"reason",
Napi::String::New(
info.Env(), "Native macOS output-device capture is unavailable on this platform."));
return support;
}
Napi::Value ListOutputDevices(const Napi::CallbackInfo& info) {
return Napi::Array::New(info.Env());
}
Napi::Value Start(const Napi::CallbackInfo& info) {
Napi::Error::New(
info.Env(), "Native macOS output-device capture is unavailable on this platform.")
.ThrowAsJavaScriptException();
return info.Env().Undefined();
}
Napi::Value Stop(const Napi::CallbackInfo& info) {
return info.Env().Undefined();
}
Napi::Value Drain(const Napi::CallbackInfo& info) {
Napi::Object result = Napi::Object::New(info.Env());
result.Set("chunks", Napi::Array::New(info.Env()));
result.Set("overwriteCount", Napi::Number::New(info.Env(), 0));
result.Set("queueDepth", Napi::Number::New(info.Env(), 0));
return result;
}
Napi::Value NowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), 0);
}
} // namespace
void RegisterMacOSCapture(Napi::Env env, Napi::Object exports) {
Napi::Object captureExports = Napi::Object::New(env);
captureExports.Set("getSupport", Napi::Function::New(env, GetSupport));
captureExports.Set(
"listOutputDevices", Napi::Function::New(env, ListOutputDevices));
captureExports.Set("start", Napi::Function::New(env, Start));
captureExports.Set("stop", Napi::Function::New(env, Stop));
captureExports.Set("drain", Napi::Function::New(env, Drain));
captureExports.Set("nowMilliseconds", Napi::Function::New(env, NowMilliseconds));
exports.Set("macosCapture", captureExports);
}
+3 -5
View File
@@ -2,8 +2,7 @@
#include <algorithm>
#include <cstring>
#include <string>
#include "linux_capture.h"
#include "macos_capture.h"
#include "system_audio_capture_napi.h"
#include "windows_capture.h"
#include "oscilloscope.h"
#include "spectrum.h"
@@ -765,9 +764,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
lufsExports.Set("reset", Napi::Function::New(env, LUFSMeterReset));
exports.Set("lufsmeter", lufsExports);
RegisterMacOSCapture(env, exports);
RegisterWindowsCapture(env, exports);
RegisterLinuxCapture(env, exports);
RegisterSystemAudioCapture(env, exports);
RegisterWindowsMedia(env, exports);
RegisterWindowChrome(env, exports);
return exports;
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace Prism::Capture {
struct Support {
bool available = false;
std::string reason;
};
struct OutputDevice {
std::string id;
std::string label;
double sampleRate = 48000.0;
uint32_t channelCount = 2;
bool isDefault = false;
};
struct StartResult {
double sampleRate = 48000.0;
uint32_t channelCount = 2;
std::string deviceId;
std::string deviceLabel;
};
struct AudioChunk {
std::vector<float> left;
std::vector<float> right;
uint32_t channelCount = 2;
double capturedAtMilliseconds = 0.0;
uint64_t sequence = 0;
};
struct DrainResult {
std::vector<AudioChunk> chunks;
uint64_t overwriteCount = 0;
size_t queueDepth = 0;
};
class SystemAudioCapture {
public:
virtual ~SystemAudioCapture() = default;
virtual Support getSupport() const = 0;
virtual std::vector<OutputDevice> listOutputDevices() = 0;
virtual bool start(const std::string& requestedDeviceId,
StartResult* result,
std::string* errorMessage) = 0;
virtual void stop() = 0;
virtual DrainResult drain(size_t maxChunks = 64) = 0;
virtual double nowMilliseconds() const = 0;
virtual const char* backendName() const = 0;
};
std::unique_ptr<SystemAudioCapture> createSystemAudioCapture();
} // namespace Prism::Capture
+206
View File
@@ -0,0 +1,206 @@
#include "system_audio_capture_napi.h"
#include "system_audio_capture.h"
#include <algorithm>
#include <cstring>
#include <memory>
#include <string>
namespace {
constexpr size_t kDefaultDrainChunkLimit = 64;
constexpr size_t kMaxDrainChunkLimit = 256;
std::unique_ptr<Prism::Capture::SystemAudioCapture>& activeCapture() {
static std::unique_ptr<Prism::Capture::SystemAudioCapture> capture =
Prism::Capture::createSystemAudioCapture();
return capture;
}
const char* activeExportName() {
#if defined(__APPLE__)
return "macosCapture";
#elif defined(_WIN32)
return "windowsCapture";
#else
return "linuxCapture";
#endif
}
Napi::Object supportToNapi(Napi::Env env, const Prism::Capture::Support& support) {
Napi::Object result = Napi::Object::New(env);
result.Set("available", Napi::Boolean::New(env, support.available));
if (support.available || support.reason.empty()) {
result.Set("reason", env.Null());
} else {
result.Set("reason", Napi::String::New(env, support.reason));
}
return result;
}
Napi::Object drainToNapi(Napi::Env env, Prism::Capture::DrainResult&& drained) {
Napi::Array chunks = Napi::Array::New(env, drained.chunks.size());
for (size_t index = 0; index < drained.chunks.size(); ++index) {
auto& chunk = drained.chunks[index];
Napi::Object entry = Napi::Object::New(env);
Napi::Float32Array left = Napi::Float32Array::New(env, chunk.left.size());
Napi::Float32Array right = Napi::Float32Array::New(env, chunk.right.size());
if (!chunk.left.empty()) {
std::memcpy(left.Data(), chunk.left.data(), chunk.left.size() * sizeof(float));
}
if (!chunk.right.empty()) {
std::memcpy(right.Data(), chunk.right.data(), chunk.right.size() * sizeof(float));
}
entry.Set("left", left);
entry.Set("right", right);
entry.Set("channelCount", Napi::Number::New(env, chunk.channelCount));
entry.Set("capturedAtMilliseconds", Napi::Number::New(env, chunk.capturedAtMilliseconds));
entry.Set("sequence", Napi::Number::New(env, static_cast<double>(chunk.sequence)));
chunks.Set(static_cast<uint32_t>(index), entry);
}
Napi::Object result = Napi::Object::New(env);
result.Set("chunks", chunks);
result.Set("overwriteCount", Napi::Number::New(env, static_cast<double>(drained.overwriteCount)));
result.Set("queueDepth", Napi::Number::New(env, static_cast<double>(drained.queueDepth)));
return result;
}
Napi::Value GetSupport(const Napi::CallbackInfo& info) {
return supportToNapi(info.Env(), activeCapture()->getSupport());
}
Napi::Value ListOutputDevices(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
const auto devices = activeCapture()->listOutputDevices();
Napi::Array result = Napi::Array::New(env, devices.size());
for (size_t index = 0; index < devices.size(); ++index) {
const auto& device = devices[index];
Napi::Object entry = Napi::Object::New(env);
entry.Set("id", Napi::String::New(env, device.id));
entry.Set("label", Napi::String::New(env, device.label));
entry.Set("kind", Napi::String::New(env, "system"));
entry.Set("isDefault", Napi::Boolean::New(env, device.isDefault));
entry.Set("sampleRate", Napi::Number::New(env, device.sampleRate));
entry.Set("channelCount", Napi::Number::New(env, device.channelCount));
result.Set(static_cast<uint32_t>(index), entry);
}
return result;
}
Napi::Value Start(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string requestedDeviceId;
if (info.Length() >= 1 && info[0].IsString()) {
requestedDeviceId = info[0].As<Napi::String>().Utf8Value();
}
Prism::Capture::StartResult started;
std::string errorMessage;
if (!activeCapture()->start(requestedDeviceId, &started, &errorMessage)) {
Napi::Error::New(env, errorMessage.empty() ? "System audio capture failed to start." : errorMessage)
.ThrowAsJavaScriptException();
return env.Undefined();
}
Napi::Object result = Napi::Object::New(env);
result.Set("sampleRate", Napi::Number::New(env, started.sampleRate));
result.Set("channelCount", Napi::Number::New(env, started.channelCount));
result.Set("deviceId", Napi::String::New(env, started.deviceId));
result.Set("deviceLabel", Napi::String::New(env, started.deviceLabel));
return result;
}
Napi::Value Stop(const Napi::CallbackInfo& info) {
activeCapture()->stop();
return info.Env().Undefined();
}
Napi::Value Drain(const Napi::CallbackInfo& info) {
size_t maxChunks = kDefaultDrainChunkLimit;
if (info.Length() >= 1 && info[0].IsNumber()) {
const int64_t requested = info[0].As<Napi::Number>().Int64Value();
if (requested > 0) {
maxChunks = std::min(static_cast<size_t>(requested), kMaxDrainChunkLimit);
}
}
return drainToNapi(info.Env(), activeCapture()->drain(maxChunks));
}
Napi::Value NowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), activeCapture()->nowMilliseconds());
}
void RegisterCaptureObject(Napi::Env env, Napi::Object exports, const char* exportName) {
Napi::Object capture = Napi::Object::New(env);
capture.Set("getSupport", Napi::Function::New(env, GetSupport));
capture.Set("listOutputDevices", Napi::Function::New(env, ListOutputDevices));
capture.Set("start", Napi::Function::New(env, Start));
capture.Set("stop", Napi::Function::New(env, Stop));
capture.Set("drain", Napi::Function::New(env, Drain));
capture.Set("nowMilliseconds", Napi::Function::New(env, NowMilliseconds));
exports.Set(exportName, capture);
}
Napi::Value UnavailableGetSupport(const Napi::CallbackInfo& info) {
Napi::Object support = Napi::Object::New(info.Env());
support.Set("available", Napi::Boolean::New(info.Env(), false));
support.Set(
"reason",
Napi::String::New(info.Env(), "This system-audio capture backend is unavailable on the current platform."));
return support;
}
Napi::Value UnavailableListOutputDevices(const Napi::CallbackInfo& info) {
return Napi::Array::New(info.Env());
}
Napi::Value UnavailableStart(const Napi::CallbackInfo& info) {
Napi::Error::New(
info.Env(), "This system-audio capture backend is unavailable on the current platform.")
.ThrowAsJavaScriptException();
return info.Env().Undefined();
}
Napi::Value UnavailableStop(const Napi::CallbackInfo& info) {
return info.Env().Undefined();
}
Napi::Value UnavailableDrain(const Napi::CallbackInfo& info) {
Napi::Object result = Napi::Object::New(info.Env());
result.Set("chunks", Napi::Array::New(info.Env()));
result.Set("overwriteCount", Napi::Number::New(info.Env(), 0));
result.Set("queueDepth", Napi::Number::New(info.Env(), 0));
return result;
}
Napi::Value UnavailableNowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), 0);
}
void RegisterUnavailableCaptureObject(Napi::Env env, Napi::Object exports, const char* exportName) {
Napi::Object capture = Napi::Object::New(env);
capture.Set("getSupport", Napi::Function::New(env, UnavailableGetSupport));
capture.Set("listOutputDevices", Napi::Function::New(env, UnavailableListOutputDevices));
capture.Set("start", Napi::Function::New(env, UnavailableStart));
capture.Set("stop", Napi::Function::New(env, UnavailableStop));
capture.Set("drain", Napi::Function::New(env, UnavailableDrain));
capture.Set("nowMilliseconds", Napi::Function::New(env, UnavailableNowMilliseconds));
exports.Set(exportName, capture);
}
} // namespace
void RegisterSystemAudioCapture(Napi::Env env, Napi::Object exports) {
RegisterCaptureObject(env, exports, activeExportName());
#if !defined(__APPLE__)
RegisterUnavailableCaptureObject(env, exports, "macosCapture");
#endif
#if !defined(_WIN32)
RegisterUnavailableCaptureObject(env, exports, "windowsCapture");
#endif
#if defined(__APPLE__) || defined(_WIN32)
RegisterUnavailableCaptureObject(env, exports, "linuxCapture");
#endif
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <napi.h>
void RegisterSystemAudioCapture(Napi::Env env, Napi::Object exports);
+77 -129
View File
@@ -1,4 +1,7 @@
#ifndef PRISM_CAPTURE_CORE_ONLY
#include "windows_capture.h"
#endif
#include "system_audio_capture.h"
#if defined(_WIN32)
@@ -10,14 +13,16 @@
#include <mmreg.h>
#include <propidl.h>
#include <avrt.h>
#include <roapi.h>
#include <wrl/client.h>
#include <windows.h>
#ifndef PRISM_CAPTURE_CORE_ONLY
#include <roapi.h>
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.Control.h>
#include <winrt/Windows.Storage.Streams.h>
#endif
#include <algorithm>
#include <atomic>
@@ -39,9 +44,11 @@
namespace {
using Microsoft::WRL::ComPtr;
#ifndef PRISM_CAPTURE_CORE_ONLY
using winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession;
using winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
using winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
#endif
constexpr size_t kMaxQueuedChunks = 256;
constexpr size_t kDefaultDrainChunkLimit = 64;
@@ -131,6 +138,7 @@ std::string hresultMessage(const char* operation, HRESULT hr) {
return stream.str();
}
#ifndef PRISM_CAPTURE_CORE_ONLY
std::string winrtErrorMessage(const char* operation, const winrt::hresult_error& error) {
std::string message = hresultMessage(operation, error.code().value);
const std::wstring detailWide = error.message().c_str();
@@ -165,6 +173,7 @@ std::string playbackStatusToString(GlobalSystemMediaTransportControlsSessionPlay
return "Closed";
}
}
#endif
class ScopedCoInit {
public:
@@ -191,6 +200,7 @@ private:
bool usable_;
};
#ifndef PRISM_CAPTURE_CORE_ONLY
class ScopedRoInit {
public:
ScopedRoInit()
@@ -344,6 +354,7 @@ std::string getOrFetchThumbnail(
}
return result;
}
#endif
std::string getDeviceId(IMMDevice* device) {
if (device == nullptr) {
@@ -584,114 +595,90 @@ std::vector<OutputDeviceInfo> enumerateOutputDevices() {
return devices;
}
class WindowsNativeCaptureEngine {
class WindowsNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture {
public:
Napi::Object GetSupport(Napi::Env env) {
Napi::Object support = Napi::Object::New(env);
support.Set("available", Napi::Boolean::New(env, true));
support.Set("reason", env.Null());
return support;
~WindowsNativeCaptureEngine() override {
stop();
}
Napi::Array ListOutputDevices(Napi::Env env) {
Prism::Capture::Support getSupport() const override {
return {true, {}};
}
std::vector<Prism::Capture::OutputDevice> listOutputDevices() override {
const auto devices = enumerateOutputDevices();
Napi::Array result = Napi::Array::New(env, devices.size());
for (size_t index = 0; index < devices.size(); ++index) {
const auto& device = devices[index];
Napi::Object entry = Napi::Object::New(env);
entry.Set("id", Napi::String::New(env, device.id));
entry.Set("label", Napi::String::New(env, device.label));
entry.Set("kind", Napi::String::New(env, "system"));
entry.Set("isDefault", Napi::Boolean::New(env, device.isDefault));
entry.Set("sampleRate", Napi::Number::New(env, device.sampleRate));
entry.Set(
"channelCount",
Napi::Number::New(env, static_cast<double>(device.channelCount)));
result.Set(static_cast<uint32_t>(index), entry);
std::vector<Prism::Capture::OutputDevice> result;
result.reserve(devices.size());
for (const auto& device : devices) {
result.push_back({
device.id,
device.label,
device.sampleRate,
static_cast<uint32_t>(device.channelCount),
device.isDefault,
});
}
return result;
}
Napi::Object Start(Napi::Env env, const std::string& requestedDeviceId) {
std::string errorMessage;
if (!startInternal(requestedDeviceId, &errorMessage)) {
Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException();
return Napi::Object::New(env);
bool start(const std::string& requestedDeviceId,
Prism::Capture::StartResult* result,
std::string* errorMessage) override {
if (!startInternal(requestedDeviceId, errorMessage)) {
return false;
}
std::lock_guard<std::mutex> lock(stateMutex_);
Napi::Object result = Napi::Object::New(env);
result.Set("sampleRate", Napi::Number::New(env, sampleRate_));
result.Set(
"channelCount", Napi::Number::New(env, static_cast<double>(channelCount_)));
result.Set("deviceId", Napi::String::New(env, activeDeviceId_));
result.Set("deviceLabel", Napi::String::New(env, activeDeviceLabel_));
return result;
if (result != nullptr) {
std::lock_guard<std::mutex> lock(stateMutex_);
result->sampleRate = sampleRate_;
result->channelCount = static_cast<uint32_t>(channelCount_);
result->deviceId = activeDeviceId_;
result->deviceLabel = activeDeviceLabel_;
}
return true;
}
void Stop() {
void stop() override {
stopInternal();
}
Napi::Object Drain(Napi::Env env, size_t maxChunks) {
Prism::Capture::DrainResult drain(size_t maxChunks) override {
const size_t drainLimit =
maxChunks == 0 ? kDefaultDrainChunkLimit : std::min(maxChunks, kMaxQueuedChunks);
std::deque<CapturedChunk> drained;
uint64_t overwriteCount = 0;
size_t queueDepth = 0;
Prism::Capture::DrainResult result;
{
std::lock_guard<std::mutex> lock(chunkMutex_);
overwriteCount = overwriteCount_;
result.overwriteCount = overwriteCount_;
const size_t count = std::min(drainLimit, chunkQueue_.size());
for (size_t index = 0; index < count; ++index) {
drained.push_back(std::move(chunkQueue_.front()));
chunkQueue_.pop_front();
}
queueDepth = chunkQueue_.size();
result.queueDepth = chunkQueue_.size();
}
Napi::Array chunks = Napi::Array::New(env, drained.size());
for (size_t index = 0; index < drained.size(); ++index) {
auto& chunk = drained[index];
Napi::Object entry = Napi::Object::New(env);
Napi::Float32Array left = Napi::Float32Array::New(env, chunk.left.size());
Napi::Float32Array right = Napi::Float32Array::New(env, chunk.right.size());
if (!chunk.left.empty()) {
std::memcpy(left.Data(), chunk.left.data(), chunk.left.size() * sizeof(float));
}
if (!chunk.right.empty()) {
std::memcpy(right.Data(), chunk.right.data(), chunk.right.size() * sizeof(float));
}
entry.Set("left", left);
entry.Set("right", right);
entry.Set(
"channelCount",
Napi::Number::New(env, static_cast<double>(chunk.channelCount)));
entry.Set(
"capturedAtMilliseconds",
Napi::Number::New(env, chunk.capturedAtMilliseconds));
entry.Set(
"sequence",
Napi::Number::New(env, static_cast<double>(chunk.sequence)));
chunks.Set(static_cast<uint32_t>(index), entry);
result.chunks.reserve(drained.size());
while (!drained.empty()) {
auto chunk = std::move(drained.front());
drained.pop_front();
result.chunks.push_back({
std::move(chunk.left),
std::move(chunk.right),
static_cast<uint32_t>(chunk.channelCount),
chunk.capturedAtMilliseconds,
chunk.sequence,
});
}
Napi::Object result = Napi::Object::New(env);
result.Set("chunks", chunks);
result.Set(
"overwriteCount", Napi::Number::New(env, static_cast<double>(overwriteCount)));
result.Set("queueDepth", Napi::Number::New(env, static_cast<double>(queueDepth)));
return result;
}
double NowMilliseconds() const {
double nowMilliseconds() const override {
return monotonicMilliseconds();
}
const char* backendName() const override {
return "WASAPI";
}
private:
bool startInternal(const std::string& requestedDeviceId, std::string* outErrorMessage) {
stopInternal();
@@ -1063,47 +1050,7 @@ private:
UINT32 channelCount_ = 2;
};
WindowsNativeCaptureEngine& engine() {
static WindowsNativeCaptureEngine instance;
return instance;
}
Napi::Value WindowsGetSupport(const Napi::CallbackInfo& info) {
return engine().GetSupport(info.Env());
}
Napi::Value WindowsListOutputDevices(const Napi::CallbackInfo& info) {
return engine().ListOutputDevices(info.Env());
}
Napi::Value WindowsStart(const Napi::CallbackInfo& info) {
std::string requestedDeviceId;
if (info.Length() >= 1 && info[0].IsString()) {
requestedDeviceId = info[0].As<Napi::String>().Utf8Value();
}
return engine().Start(info.Env(), requestedDeviceId);
}
Napi::Value WindowsStop(const Napi::CallbackInfo& info) {
engine().Stop();
return info.Env().Undefined();
}
Napi::Value WindowsDrain(const Napi::CallbackInfo& info) {
size_t maxChunks = kDefaultDrainChunkLimit;
if (info.Length() >= 1 && info[0].IsNumber()) {
const int64_t requested = info[0].As<Napi::Number>().Int64Value();
if (requested > 0) {
maxChunks = static_cast<size_t>(requested);
}
}
return engine().Drain(info.Env(), maxChunks);
}
Napi::Value WindowsNowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), engine().NowMilliseconds());
}
#ifndef PRISM_CAPTURE_CORE_ONLY
Napi::Value WindowsMediaGetSupport(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
@@ -1264,20 +1211,12 @@ Napi::Value WindowsMediaSendSpotifyControl(const Napi::CallbackInfo& info) {
return env.Null();
}
}
#endif
} // namespace
void RegisterWindowsCapture(Napi::Env env, Napi::Object exports) {
Napi::Object captureExports = Napi::Object::New(env);
captureExports.Set("getSupport", Napi::Function::New(env, WindowsGetSupport));
captureExports.Set(
"listOutputDevices", Napi::Function::New(env, WindowsListOutputDevices));
captureExports.Set("start", Napi::Function::New(env, WindowsStart));
captureExports.Set("stop", Napi::Function::New(env, WindowsStop));
captureExports.Set("drain", Napi::Function::New(env, WindowsDrain));
captureExports.Set("nowMilliseconds", Napi::Function::New(env, WindowsNowMilliseconds));
exports.Set("windowsCapture", captureExports);
#ifndef PRISM_CAPTURE_CORE_ONLY
void RegisterWindowsMedia(Napi::Env env, Napi::Object exports) {
Napi::Object mediaExports = Napi::Object::New(env);
mediaExports.Set("getSupport", Napi::Function::New(env, WindowsMediaGetSupport));
mediaExports.Set(
@@ -1288,5 +1227,14 @@ void RegisterWindowsCapture(Napi::Env env, Napi::Object exports) {
Napi::Function::New(env, WindowsMediaSendSpotifyControl));
exports.Set("windowsMedia", mediaExports);
}
#endif
namespace Prism::Capture {
std::unique_ptr<SystemAudioCapture> createSystemAudioCapture() {
return std::make_unique<WindowsNativeCaptureEngine>();
}
} // namespace Prism::Capture
#endif // defined(_WIN32)
+1 -1
View File
@@ -2,4 +2,4 @@
#include <napi.h>
void RegisterWindowsCapture(Napi::Env env, Napi::Object exports);
void RegisterWindowsMedia(Napi::Env env, Napi::Object exports);
+1 -48
View File
@@ -2,43 +2,6 @@
namespace {
Napi::Value GetSupport(const Napi::CallbackInfo& info) {
Napi::Object support = Napi::Object::New(info.Env());
support.Set("available", Napi::Boolean::New(info.Env(), false));
support.Set(
"reason",
Napi::String::New(
info.Env(), "Native Windows output-device capture is unavailable on this platform."));
return support;
}
Napi::Value ListOutputDevices(const Napi::CallbackInfo& info) {
return Napi::Array::New(info.Env());
}
Napi::Value Start(const Napi::CallbackInfo& info) {
Napi::Error::New(
info.Env(), "Native Windows output-device capture is unavailable on this platform.")
.ThrowAsJavaScriptException();
return info.Env().Undefined();
}
Napi::Value Stop(const Napi::CallbackInfo& info) {
return info.Env().Undefined();
}
Napi::Value Drain(const Napi::CallbackInfo& info) {
Napi::Object result = Napi::Object::New(info.Env());
result.Set("chunks", Napi::Array::New(info.Env()));
result.Set("overwriteCount", Napi::Number::New(info.Env(), 0));
result.Set("queueDepth", Napi::Number::New(info.Env(), 0));
return result;
}
Napi::Value NowMilliseconds(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), 0);
}
Napi::Value MediaGetSupport(const Napi::CallbackInfo& info) {
Napi::Object support = Napi::Object::New(info.Env());
support.Set("available", Napi::Boolean::New(info.Env(), false));
@@ -62,17 +25,7 @@ Napi::Value SendSpotifyControl(const Napi::CallbackInfo& info) {
} // namespace
void RegisterWindowsCapture(Napi::Env env, Napi::Object exports) {
Napi::Object captureExports = Napi::Object::New(env);
captureExports.Set("getSupport", Napi::Function::New(env, GetSupport));
captureExports.Set(
"listOutputDevices", Napi::Function::New(env, ListOutputDevices));
captureExports.Set("start", Napi::Function::New(env, Start));
captureExports.Set("stop", Napi::Function::New(env, Stop));
captureExports.Set("drain", Napi::Function::New(env, Drain));
captureExports.Set("nowMilliseconds", Napi::Function::New(env, NowMilliseconds));
exports.Set("windowsCapture", captureExports);
void RegisterWindowsMedia(Napi::Env env, Napi::Object exports) {
Napi::Object mediaExports = Napi::Object::New(env);
mediaExports.Set("getSupport", Napi::Function::New(env, MediaGetSupport));
mediaExports.Set(