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
+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__)