mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-16 00:00:31 +02:00
prism-tui PoC
This commit is contained in:
+77
-149
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
void RegisterLinuxCapture(Napi::Env env, Napi::Object exports);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
void RegisterMacOSCapture(Napi::Env env, Napi::Object exports);
|
||||
+68
-150
@@ -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__)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
void RegisterSystemAudioCapture(Napi::Env env, Napi::Object exports);
|
||||
+77
-129
@@ -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)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
void RegisterWindowsCapture(Napi::Env env, Napi::Object exports);
|
||||
void RegisterWindowsMedia(Napi::Env env, Napi::Object exports);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user