mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
initial commit for windows wasapi native capture support
This commit is contained in:
+15
-3
@@ -20,7 +20,8 @@
|
||||
"conditions": [
|
||||
["OS=='mac'", {
|
||||
"sources": [
|
||||
"src/macos_capture.mm"
|
||||
"src/macos_capture.mm",
|
||||
"src/windows_capture_stub.cpp"
|
||||
],
|
||||
"xcode_settings": {
|
||||
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
||||
@@ -36,7 +37,17 @@
|
||||
}],
|
||||
["OS=='win'", {
|
||||
"sources": [
|
||||
"src/macos_capture_stub.cpp"
|
||||
"src/macos_capture_stub.cpp",
|
||||
"src/windows_capture.cpp"
|
||||
],
|
||||
"defines": [
|
||||
"WIN32_LEAN_AND_MEAN",
|
||||
"NOMINMAX"
|
||||
],
|
||||
"libraries": [
|
||||
"ole32.lib",
|
||||
"avrt.lib",
|
||||
"uuid.lib"
|
||||
],
|
||||
"msvs_settings": {
|
||||
"VCCLCompilerTool": {
|
||||
@@ -47,7 +58,8 @@
|
||||
}],
|
||||
["OS=='linux'", {
|
||||
"sources": [
|
||||
"src/macos_capture_stub.cpp"
|
||||
"src/macos_capture_stub.cpp",
|
||||
"src/windows_capture_stub.cpp"
|
||||
],
|
||||
"cflags_cc": ["-std=c++17", "-O3", "-ffast-math", "-fPIC"]
|
||||
}]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <napi.h>
|
||||
#include <cstring>
|
||||
#include "macos_capture.h"
|
||||
#include "windows_capture.h"
|
||||
#include "oscilloscope.h"
|
||||
#include "spectrum.h"
|
||||
#include "vectorscope.h"
|
||||
@@ -296,6 +297,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set("vectorscope", vecExports);
|
||||
|
||||
RegisterMacOSCapture(env, exports);
|
||||
RegisterWindowsCapture(env, exports);
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
#include "windows_capture.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <Audioclient.h>
|
||||
#include <Functiondiscoverykeys_devpkey.h>
|
||||
#include <Mmdeviceapi.h>
|
||||
#include <ksmedia.h>
|
||||
#include <mmreg.h>
|
||||
#include <propidl.h>
|
||||
#include <avrt.h>
|
||||
#include <wrl/client.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
constexpr size_t kMaxQueuedChunks = 256;
|
||||
constexpr size_t kDefaultDrainChunkLimit = 64;
|
||||
|
||||
struct OutputDeviceInfo {
|
||||
std::string id;
|
||||
std::string label;
|
||||
double sampleRate;
|
||||
UINT32 channelCount;
|
||||
bool isDefault;
|
||||
};
|
||||
|
||||
struct CapturedChunk {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
UINT32 channelCount = 2;
|
||||
double capturedAtMilliseconds = 0.0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
struct AudioFormatInfo {
|
||||
bool valid = false;
|
||||
bool isFloat = false;
|
||||
WORD channels = 0;
|
||||
DWORD sampleRate = 48000;
|
||||
WORD bitsPerSample = 0;
|
||||
WORD validBitsPerSample = 0;
|
||||
WORD bytesPerFrame = 0;
|
||||
WORD bytesPerSample = 0;
|
||||
};
|
||||
|
||||
double monotonicMilliseconds() {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return std::chrono::duration<double, std::milli>(now).count();
|
||||
}
|
||||
|
||||
std::string wideToUtf8(const std::wstring& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int sizeNeeded = WideCharToMultiByte(
|
||||
CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
||||
if (sizeNeeded <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(static_cast<size_t>(sizeNeeded), '\0');
|
||||
WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
0,
|
||||
value.c_str(),
|
||||
static_cast<int>(value.size()),
|
||||
result.data(),
|
||||
sizeNeeded,
|
||||
nullptr,
|
||||
nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring utf8ToWide(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int sizeNeeded =
|
||||
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), nullptr, 0);
|
||||
if (sizeNeeded <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(sizeNeeded), L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8,
|
||||
0,
|
||||
value.c_str(),
|
||||
static_cast<int>(value.size()),
|
||||
result.data(),
|
||||
sizeNeeded);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string hresultMessage(const char* operation, HRESULT hr) {
|
||||
std::ostringstream stream;
|
||||
stream << operation << " failed (0x" << std::hex << std::uppercase
|
||||
<< static_cast<unsigned long>(hr) << ")";
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
class ScopedCoInit {
|
||||
public:
|
||||
ScopedCoInit()
|
||||
: hr_(CoInitializeEx(nullptr, COINIT_MULTITHREADED)),
|
||||
usable_(SUCCEEDED(hr_) || hr_ == RPC_E_CHANGED_MODE) {}
|
||||
|
||||
~ScopedCoInit() {
|
||||
if (SUCCEEDED(hr_)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
bool usable() const {
|
||||
return usable_;
|
||||
}
|
||||
|
||||
HRESULT result() const {
|
||||
return hr_;
|
||||
}
|
||||
|
||||
private:
|
||||
HRESULT hr_;
|
||||
bool usable_;
|
||||
};
|
||||
|
||||
std::string getDeviceId(IMMDevice* device) {
|
||||
if (device == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
LPWSTR id = nullptr;
|
||||
const HRESULT hr = device->GetId(&id);
|
||||
if (FAILED(hr) || id == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring wideId(id);
|
||||
CoTaskMemFree(id);
|
||||
return wideToUtf8(wideId);
|
||||
}
|
||||
|
||||
std::string getDeviceFriendlyName(IMMDevice* device) {
|
||||
if (device == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ComPtr<IPropertyStore> properties;
|
||||
HRESULT hr = device->OpenPropertyStore(STGM_READ, &properties);
|
||||
if (FAILED(hr) || !properties) {
|
||||
return {};
|
||||
}
|
||||
|
||||
PROPVARIANT value;
|
||||
PropVariantInit(&value);
|
||||
hr = properties->GetValue(PKEY_Device_FriendlyName, &value);
|
||||
if (FAILED(hr)) {
|
||||
PropVariantClear(&value);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string label;
|
||||
if (value.vt == VT_LPWSTR && value.pwszVal != nullptr) {
|
||||
label = wideToUtf8(value.pwszVal);
|
||||
}
|
||||
PropVariantClear(&value);
|
||||
return label;
|
||||
}
|
||||
|
||||
AudioFormatInfo getFormatInfo(const WAVEFORMATEX* format) {
|
||||
AudioFormatInfo info;
|
||||
if (format == nullptr || format->nChannels == 0 || format->nBlockAlign == 0) {
|
||||
return info;
|
||||
}
|
||||
|
||||
info.valid = true;
|
||||
info.channels = format->nChannels;
|
||||
info.sampleRate = format->nSamplesPerSec;
|
||||
info.bitsPerSample = format->wBitsPerSample;
|
||||
info.validBitsPerSample = format->wBitsPerSample;
|
||||
info.bytesPerFrame = format->nBlockAlign;
|
||||
info.bytesPerSample = static_cast<WORD>(format->nBlockAlign / format->nChannels);
|
||||
info.isFloat = format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT;
|
||||
|
||||
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
const auto* extensible = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(format);
|
||||
info.isFloat = IsEqualGUID(extensible->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
|
||||
if (extensible->Samples.wValidBitsPerSample != 0) {
|
||||
info.validBitsPerSample = extensible->Samples.wValidBitsPerSample;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
float decodeSignedIntegerSample(const BYTE* data, WORD bytesPerSample, WORD validBitsPerSample) {
|
||||
if (data == nullptr || bytesPerSample == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const WORD totalBits = static_cast<WORD>(bytesPerSample * 8);
|
||||
const WORD validBits = static_cast<WORD>(
|
||||
std::max<WORD>(1, std::min<WORD>(validBitsPerSample == 0 ? totalBits : validBitsPerSample, 32)));
|
||||
|
||||
uint32_t rawBits = 0;
|
||||
for (WORD byteIndex = 0; byteIndex < bytesPerSample && byteIndex < 4; ++byteIndex) {
|
||||
rawBits |= static_cast<uint32_t>(data[byteIndex]) << (byteIndex * 8);
|
||||
}
|
||||
|
||||
const uint32_t validMask =
|
||||
validBits >= 32 ? std::numeric_limits<uint32_t>::max() : ((1u << validBits) - 1u);
|
||||
rawBits &= validMask;
|
||||
|
||||
int32_t signedValue = 0;
|
||||
if (validBits == 32) {
|
||||
signedValue = static_cast<int32_t>(rawBits);
|
||||
} else {
|
||||
const uint32_t signBit = 1u << (validBits - 1);
|
||||
if ((rawBits & signBit) != 0) {
|
||||
rawBits |= ~validMask;
|
||||
}
|
||||
signedValue = static_cast<int32_t>(rawBits);
|
||||
}
|
||||
|
||||
const double maxMagnitude = validBits == 32
|
||||
? static_cast<double>(std::numeric_limits<int32_t>::max())
|
||||
: static_cast<double>((1ULL << (validBits - 1)) - 1ULL);
|
||||
if (maxMagnitude <= 0.0) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(static_cast<double>(signedValue) / maxMagnitude);
|
||||
}
|
||||
|
||||
float readFrameSample(const BYTE* frameData, UINT32 channelIndex, const AudioFormatInfo& format) {
|
||||
if (frameData == nullptr || !format.valid || format.bytesPerSample == 0 ||
|
||||
channelIndex >= format.channels) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const BYTE* samplePtr = frameData + static_cast<size_t>(channelIndex) * format.bytesPerSample;
|
||||
if (format.isFloat) {
|
||||
if (format.bitsPerSample == 32 && format.bytesPerSample >= sizeof(float)) {
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, samplePtr, sizeof(float));
|
||||
return value;
|
||||
}
|
||||
|
||||
if (format.bitsPerSample == 64 && format.bytesPerSample >= sizeof(double)) {
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, samplePtr, sizeof(double));
|
||||
return static_cast<float>(value);
|
||||
}
|
||||
}
|
||||
|
||||
return decodeSignedIntegerSample(
|
||||
samplePtr, format.bytesPerSample, format.validBitsPerSample);
|
||||
}
|
||||
|
||||
bool getDeviceMixFormat(IMMDevice* device, double* outSampleRate, UINT32* outChannelCount) {
|
||||
if (device == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<IAudioClient> audioClient;
|
||||
HRESULT hr = device->Activate(
|
||||
__uuidof(IAudioClient),
|
||||
CLSCTX_ALL,
|
||||
nullptr,
|
||||
reinterpret_cast<void**>(audioClient.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr) || !audioClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WAVEFORMATEX* mixFormat = nullptr;
|
||||
hr = audioClient->GetMixFormat(&mixFormat);
|
||||
if (FAILED(hr) || mixFormat == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const AudioFormatInfo info = getFormatInfo(mixFormat);
|
||||
CoTaskMemFree(mixFormat);
|
||||
if (!info.valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outSampleRate != nullptr) {
|
||||
*outSampleRate = static_cast<double>(info.sampleRate);
|
||||
}
|
||||
if (outChannelCount != nullptr) {
|
||||
*outChannelCount = info.channels;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<OutputDeviceInfo> enumerateOutputDevices() {
|
||||
ScopedCoInit coInit;
|
||||
if (!coInit.usable()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ComPtr<IMMDeviceEnumerator> enumerator;
|
||||
HRESULT hr = CoCreateInstance(
|
||||
__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator));
|
||||
if (FAILED(hr) || !enumerator) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string defaultDeviceId;
|
||||
ComPtr<IMMDevice> defaultDevice;
|
||||
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &defaultDevice);
|
||||
if (SUCCEEDED(hr) && defaultDevice) {
|
||||
defaultDeviceId = getDeviceId(defaultDevice.Get());
|
||||
}
|
||||
|
||||
ComPtr<IMMDeviceCollection> collection;
|
||||
hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
|
||||
if (FAILED(hr) || !collection) {
|
||||
return {};
|
||||
}
|
||||
|
||||
UINT deviceCount = 0;
|
||||
hr = collection->GetCount(&deviceCount);
|
||||
if (FAILED(hr) || deviceCount == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<OutputDeviceInfo> devices;
|
||||
devices.reserve(deviceCount);
|
||||
|
||||
for (UINT index = 0; index < deviceCount; ++index) {
|
||||
ComPtr<IMMDevice> device;
|
||||
hr = collection->Item(index, &device);
|
||||
if (FAILED(hr) || !device) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string deviceId = getDeviceId(device.Get());
|
||||
if (deviceId.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string label = getDeviceFriendlyName(device.Get());
|
||||
if (label.empty()) {
|
||||
label = deviceId;
|
||||
}
|
||||
|
||||
double sampleRate = 48000.0;
|
||||
UINT32 channelCount = 2;
|
||||
getDeviceMixFormat(device.Get(), &sampleRate, &channelCount);
|
||||
|
||||
devices.push_back(OutputDeviceInfo{
|
||||
deviceId,
|
||||
label,
|
||||
sampleRate,
|
||||
channelCount,
|
||||
deviceId == defaultDeviceId,
|
||||
});
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
class WindowsNativeCaptureEngine {
|
||||
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;
|
||||
}
|
||||
|
||||
Napi::Array ListOutputDevices(Napi::Env env) {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
stopInternal();
|
||||
}
|
||||
|
||||
Napi::Object Drain(Napi::Env env, size_t maxChunks) {
|
||||
const size_t drainLimit =
|
||||
maxChunks == 0 ? kDefaultDrainChunkLimit : std::min(maxChunks, kMaxQueuedChunks);
|
||||
|
||||
std::deque<CapturedChunk> drained;
|
||||
uint64_t overwriteCount = 0;
|
||||
size_t queueDepth = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(chunkMutex_);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
return monotonicMilliseconds();
|
||||
}
|
||||
|
||||
private:
|
||||
bool startInternal(const std::string& requestedDeviceId, std::string* outErrorMessage) {
|
||||
stopInternal();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
startPending_ = true;
|
||||
startSucceeded_ = false;
|
||||
startError_.clear();
|
||||
stopRequested_.store(false);
|
||||
if (stopEvent_ != nullptr) {
|
||||
CloseHandle(stopEvent_);
|
||||
stopEvent_ = nullptr;
|
||||
}
|
||||
stopEvent_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (stopEvent_ == nullptr) {
|
||||
startPending_ = false;
|
||||
startError_ = "CreateEventW failed for Windows loopback capture.";
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = startError_;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
captureThread_ = std::thread(
|
||||
[this, requestedDeviceId]() { this->captureThreadMain(requestedDeviceId); });
|
||||
|
||||
std::unique_lock<std::mutex> lock(stateMutex_);
|
||||
startCondition_.wait(lock, [this]() { return !startPending_; });
|
||||
|
||||
if (!startSucceeded_) {
|
||||
const std::string errorMessage = startError_.empty()
|
||||
? "Native Windows loopback capture failed to start."
|
||||
: startError_;
|
||||
lock.unlock();
|
||||
stopInternal();
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = errorMessage;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void stopInternal() {
|
||||
std::thread captureThread;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
stopRequested_.store(true);
|
||||
if (stopEvent_ != nullptr) {
|
||||
SetEvent(stopEvent_);
|
||||
}
|
||||
if (captureThread_.joinable()) {
|
||||
captureThread = std::move(captureThread_);
|
||||
}
|
||||
}
|
||||
|
||||
if (captureThread.joinable()) {
|
||||
captureThread.join();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
if (stopEvent_ != nullptr) {
|
||||
CloseHandle(stopEvent_);
|
||||
stopEvent_ = nullptr;
|
||||
}
|
||||
active_ = false;
|
||||
startPending_ = false;
|
||||
startSucceeded_ = false;
|
||||
activeDeviceId_.clear();
|
||||
activeDeviceLabel_.clear();
|
||||
sampleRate_ = 48000.0;
|
||||
channelCount_ = 2;
|
||||
sequence_ = 0;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(chunkMutex_);
|
||||
chunkQueue_.clear();
|
||||
overwriteCount_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void captureThreadMain(const std::string& requestedDeviceId) {
|
||||
ScopedCoInit coInit;
|
||||
if (!coInit.usable()) {
|
||||
notifyStartFailure(hresultMessage("CoInitializeEx", coInit.result()));
|
||||
return;
|
||||
}
|
||||
|
||||
ComPtr<IMMDeviceEnumerator> enumerator;
|
||||
HRESULT hr = CoCreateInstance(
|
||||
__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator));
|
||||
if (FAILED(hr) || !enumerator) {
|
||||
notifyStartFailure(hresultMessage("CoCreateInstance(MMDeviceEnumerator)", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
ComPtr<IMMDevice> device;
|
||||
if (!requestedDeviceId.empty()) {
|
||||
const std::wstring requestedWide = utf8ToWide(requestedDeviceId);
|
||||
hr = enumerator->GetDevice(requestedWide.c_str(), &device);
|
||||
} else {
|
||||
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
|
||||
}
|
||||
|
||||
if (FAILED(hr) || !device) {
|
||||
notifyStartFailure(hresultMessage("Get audio endpoint", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string deviceId = getDeviceId(device.Get());
|
||||
std::string deviceLabel = getDeviceFriendlyName(device.Get());
|
||||
if (deviceLabel.empty()) {
|
||||
deviceLabel = deviceId.empty() ? "Windows Output Device" : deviceId;
|
||||
}
|
||||
|
||||
ComPtr<IAudioClient> audioClient;
|
||||
hr = device->Activate(
|
||||
__uuidof(IAudioClient),
|
||||
CLSCTX_ALL,
|
||||
nullptr,
|
||||
reinterpret_cast<void**>(audioClient.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr) || !audioClient) {
|
||||
notifyStartFailure(hresultMessage("IMMDevice::Activate(IAudioClient)", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
WAVEFORMATEX* mixFormat = nullptr;
|
||||
hr = audioClient->GetMixFormat(&mixFormat);
|
||||
if (FAILED(hr) || mixFormat == nullptr) {
|
||||
notifyStartFailure(hresultMessage("IAudioClient::GetMixFormat", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
const AudioFormatInfo format = getFormatInfo(mixFormat);
|
||||
if (!format.valid) {
|
||||
CoTaskMemFree(mixFormat);
|
||||
notifyStartFailure("Unsupported WASAPI mix format for Windows loopback capture.");
|
||||
return;
|
||||
}
|
||||
|
||||
REFERENCE_TIME defaultPeriod = 0;
|
||||
REFERENCE_TIME minimumPeriod = 0;
|
||||
audioClient->GetDevicePeriod(&defaultPeriod, &minimumPeriod);
|
||||
const DWORD sleepMilliseconds = static_cast<DWORD>(
|
||||
std::max<LONG64>(2, std::min<LONG64>(10, defaultPeriod / 10000 / 2)));
|
||||
|
||||
hr = audioClient->Initialize(
|
||||
AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, 0, 0, mixFormat, nullptr);
|
||||
if (FAILED(hr)) {
|
||||
CoTaskMemFree(mixFormat);
|
||||
notifyStartFailure(hresultMessage("IAudioClient::Initialize", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
ComPtr<IAudioCaptureClient> captureClient;
|
||||
hr = audioClient->GetService(IID_PPV_ARGS(&captureClient));
|
||||
if (FAILED(hr) || !captureClient) {
|
||||
CoTaskMemFree(mixFormat);
|
||||
notifyStartFailure(hresultMessage("IAudioClient::GetService(IAudioCaptureClient)", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
hr = audioClient->Start();
|
||||
if (FAILED(hr)) {
|
||||
CoTaskMemFree(mixFormat);
|
||||
notifyStartFailure(hresultMessage("IAudioClient::Start", hr));
|
||||
return;
|
||||
}
|
||||
|
||||
notifyStartSuccess(
|
||||
deviceId,
|
||||
deviceLabel,
|
||||
static_cast<double>(format.sampleRate),
|
||||
std::max<UINT32>(1, format.channels));
|
||||
|
||||
DWORD taskIndex = 0;
|
||||
HANDLE mmcssHandle = AvSetMmThreadCharacteristicsW(L"Audio", &taskIndex);
|
||||
|
||||
while (!stopRequested_.load()) {
|
||||
UINT32 packetFrames = 0;
|
||||
hr = captureClient->GetNextPacketSize(&packetFrames);
|
||||
if (FAILED(hr)) {
|
||||
break;
|
||||
}
|
||||
|
||||
while (packetFrames > 0 && !stopRequested_.load()) {
|
||||
BYTE* data = nullptr;
|
||||
UINT32 framesToRead = 0;
|
||||
DWORD flags = 0;
|
||||
hr = captureClient->GetBuffer(&data, &framesToRead, &flags, nullptr, nullptr);
|
||||
if (FAILED(hr)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesToRead > 0) {
|
||||
CapturedChunk chunk;
|
||||
chunk.channelCount = std::max<UINT32>(1, format.channels);
|
||||
chunk.capturedAtMilliseconds = monotonicMilliseconds();
|
||||
chunk.left.resize(framesToRead);
|
||||
chunk.right.resize(framesToRead);
|
||||
|
||||
if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0 && data != nullptr) {
|
||||
for (UINT32 frameIndex = 0; frameIndex < framesToRead; ++frameIndex) {
|
||||
const BYTE* frameData =
|
||||
data + static_cast<size_t>(frameIndex) * format.bytesPerFrame;
|
||||
const float left = readFrameSample(frameData, 0, format);
|
||||
const float right = format.channels > 1
|
||||
? readFrameSample(frameData, 1, format)
|
||||
: left;
|
||||
chunk.left[frameIndex] = left;
|
||||
chunk.right[frameIndex] = right;
|
||||
}
|
||||
} else {
|
||||
std::fill(chunk.left.begin(), chunk.left.end(), 0.0f);
|
||||
std::fill(chunk.right.begin(), chunk.right.end(), 0.0f);
|
||||
}
|
||||
|
||||
pushChunk(std::move(chunk));
|
||||
}
|
||||
|
||||
captureClient->ReleaseBuffer(framesToRead);
|
||||
hr = captureClient->GetNextPacketSize(&packetFrames);
|
||||
if (FAILED(hr)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (FAILED(hr) || stopRequested_.load()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (WaitForSingleObject(stopEvent_, sleepMilliseconds) == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mmcssHandle != nullptr) {
|
||||
AvRevertMmThreadCharacteristics(mmcssHandle);
|
||||
}
|
||||
|
||||
audioClient->Stop();
|
||||
CoTaskMemFree(mixFormat);
|
||||
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
void notifyStartSuccess(const std::string& deviceId,
|
||||
const std::string& deviceLabel,
|
||||
double sampleRate,
|
||||
UINT32 channelCount) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
active_ = true;
|
||||
activeDeviceId_ = deviceId;
|
||||
activeDeviceLabel_ = deviceLabel;
|
||||
sampleRate_ = sampleRate;
|
||||
channelCount_ = channelCount;
|
||||
sequence_ = 0;
|
||||
startSucceeded_ = true;
|
||||
startPending_ = false;
|
||||
startError_.clear();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(chunkMutex_);
|
||||
chunkQueue_.clear();
|
||||
overwriteCount_ = 0;
|
||||
}
|
||||
|
||||
startCondition_.notify_all();
|
||||
}
|
||||
|
||||
void notifyStartFailure(const std::string& message) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
startSucceeded_ = false;
|
||||
startPending_ = false;
|
||||
startError_ = message;
|
||||
active_ = false;
|
||||
}
|
||||
startCondition_.notify_all();
|
||||
}
|
||||
|
||||
void pushChunk(CapturedChunk chunk) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex_);
|
||||
chunk.sequence = ++sequence_;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(chunkMutex_);
|
||||
if (chunkQueue_.size() >= kMaxQueuedChunks) {
|
||||
chunkQueue_.pop_front();
|
||||
++overwriteCount_;
|
||||
}
|
||||
chunkQueue_.push_back(std::move(chunk));
|
||||
}
|
||||
|
||||
std::mutex stateMutex_;
|
||||
std::condition_variable startCondition_;
|
||||
std::mutex chunkMutex_;
|
||||
std::deque<CapturedChunk> chunkQueue_;
|
||||
std::thread captureThread_;
|
||||
HANDLE stopEvent_ = nullptr;
|
||||
std::atomic<bool> stopRequested_{false};
|
||||
|
||||
uint64_t overwriteCount_ = 0;
|
||||
uint64_t sequence_ = 0;
|
||||
bool active_ = false;
|
||||
bool startPending_ = false;
|
||||
bool startSucceeded_ = false;
|
||||
std::string startError_;
|
||||
std::string activeDeviceId_;
|
||||
std::string activeDeviceLabel_;
|
||||
double sampleRate_ = 48000.0;
|
||||
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());
|
||||
}
|
||||
|
||||
} // 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);
|
||||
}
|
||||
|
||||
#endif // defined(_WIN32)
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
void RegisterWindowsCapture(Napi::Env env, Napi::Object exports);
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "windows_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 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);
|
||||
}
|
||||
|
||||
} // 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);
|
||||
}
|
||||
+31
-12
@@ -60,25 +60,43 @@ try {
|
||||
function resolveNativeCaptureSupport(
|
||||
fallbackEntry: CaptureBackendSupportEntry,
|
||||
): CaptureBackendSupportEntry {
|
||||
if (process.platform !== 'darwin') {
|
||||
return fallbackEntry
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
const macosCapture = nativeAddonModule?.macosCapture
|
||||
if (!macosCapture) {
|
||||
return {
|
||||
kind: 'native-macos',
|
||||
available: false,
|
||||
reason: 'Native capture module is not available in this build.',
|
||||
}
|
||||
}
|
||||
|
||||
const macosCapture = nativeAddonModule?.macosCapture
|
||||
if (!macosCapture) {
|
||||
const support = macosCapture.getSupport()
|
||||
return {
|
||||
kind: 'native-macos',
|
||||
available: false,
|
||||
reason: 'Native capture module is not available in this build.',
|
||||
available: support.available,
|
||||
reason: support.reason,
|
||||
}
|
||||
}
|
||||
|
||||
const support = macosCapture.getSupport()
|
||||
return {
|
||||
kind: 'native-macos',
|
||||
available: support.available,
|
||||
reason: support.reason,
|
||||
if (process.platform === 'win32') {
|
||||
const windowsCapture = nativeAddonModule?.windowsCapture
|
||||
if (!windowsCapture) {
|
||||
return {
|
||||
kind: 'native-windows',
|
||||
available: false,
|
||||
reason: 'Native capture module is not available in this build.',
|
||||
}
|
||||
}
|
||||
|
||||
const support = windowsCapture.getSupport()
|
||||
return {
|
||||
kind: 'native-windows',
|
||||
available: support.available,
|
||||
reason: support.reason,
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackEntry
|
||||
}
|
||||
|
||||
const visualizerAPI = nativeAddonModule
|
||||
@@ -92,6 +110,7 @@ const visualizerAPI = nativeAddonModule
|
||||
const nativeCaptureAPI = nativeAddonModule
|
||||
? {
|
||||
macosCapture: nativeAddonModule.macosCapture,
|
||||
windowsCapture: nativeAddonModule.windowsCapture,
|
||||
}
|
||||
: null
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ import type {
|
||||
CaptureMode,
|
||||
CaptureSourceDescriptor,
|
||||
} from '../../types/capture'
|
||||
import type { NativeMacOSCaptureDrainResult, NativeMacOSCaptureStartResult } from '../../types/nativeCapture'
|
||||
import type {
|
||||
NativeCaptureDrainResult,
|
||||
NativeCaptureStartResult,
|
||||
NativeSystemCaptureAPI,
|
||||
} from '../../types/nativeCapture'
|
||||
|
||||
export type { CaptureMode } from '../../types/capture'
|
||||
|
||||
@@ -338,8 +342,8 @@ class ElectronDeviceCaptureBackend implements CaptureBackend {
|
||||
}
|
||||
}
|
||||
|
||||
class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
readonly kind = 'native-macos' as const
|
||||
abstract class NativePolledCaptureBackend implements CaptureBackend {
|
||||
abstract readonly kind: CaptureBackendKind
|
||||
|
||||
private readonly chunkListeners = new Set<(chunk: CaptureChunk) => void>()
|
||||
private pollTimer: number | null = null
|
||||
@@ -354,14 +358,14 @@ class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
}
|
||||
|
||||
async start(request?: CaptureBackendStartRequest): Promise<void> {
|
||||
const nativeCapture = window.nativeCaptureAPI?.macosCapture
|
||||
const nativeCapture = this.getNativeCaptureModule()
|
||||
if (!nativeCapture) {
|
||||
throw new Error('Native macOS capture module is not available in this build.')
|
||||
throw new Error(`${this.getBackendLabel()} capture module is not available in this build.`)
|
||||
}
|
||||
|
||||
const support = nativeCapture.getSupport()
|
||||
if (!support.available) {
|
||||
throw new Error(support.reason ?? 'Native macOS capture is unavailable.')
|
||||
throw new Error(support.reason ?? `${this.getBackendLabel()} capture is unavailable.`)
|
||||
}
|
||||
|
||||
const nativeNow = nativeCapture.nowMilliseconds()
|
||||
@@ -371,7 +375,7 @@ class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
request?.deviceId && request.deviceId !== DEFAULT_SYSTEM_SOURCE_ID
|
||||
? request.deviceId
|
||||
: undefined,
|
||||
) as NativeMacOSCaptureStartResult
|
||||
) as NativeCaptureStartResult
|
||||
|
||||
this.sampleRate = Math.max(1, Math.floor(startResult.sampleRate) || 48000)
|
||||
this.channelCount = Math.max(1, Math.floor(startResult.channelCount) || 2)
|
||||
@@ -382,12 +386,12 @@ class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.stopPolling()
|
||||
window.nativeCaptureAPI?.macosCapture.stop()
|
||||
this.getNativeCaptureModule()?.stop()
|
||||
this.active = false
|
||||
}
|
||||
|
||||
async listSources(): Promise<CaptureSourceDescriptor[]> {
|
||||
const nativeCapture = window.nativeCaptureAPI?.macosCapture
|
||||
const nativeCapture = this.getNativeCaptureModule()
|
||||
if (!nativeCapture) {
|
||||
return [getDefaultSystemSourceDescriptor()]
|
||||
}
|
||||
@@ -437,7 +441,7 @@ class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
if (!this.active) return
|
||||
|
||||
try {
|
||||
const result = window.nativeCaptureAPI?.macosCapture.drain(32) as NativeMacOSCaptureDrainResult | undefined
|
||||
const result = this.getNativeCaptureModule()?.drain(32) as NativeCaptureDrainResult | undefined
|
||||
if (result) {
|
||||
for (const chunk of result.chunks) {
|
||||
const routedChunk: CaptureChunk = {
|
||||
@@ -454,7 +458,7 @@ class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Native macOS capture poll failed:', error)
|
||||
console.error(`${this.getBackendLabel()} capture poll failed:`, error)
|
||||
this.active = false
|
||||
this.stopPolling()
|
||||
return
|
||||
@@ -472,6 +476,33 @@ class NativeMacOSCaptureBackend implements CaptureBackend {
|
||||
this.pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract getNativeCaptureModule(): NativeSystemCaptureAPI | null
|
||||
protected abstract getBackendLabel(): string
|
||||
}
|
||||
|
||||
class NativeMacOSCaptureBackend extends NativePolledCaptureBackend {
|
||||
readonly kind = 'native-macos' as const
|
||||
|
||||
protected getNativeCaptureModule(): NativeSystemCaptureAPI | null {
|
||||
return window.nativeCaptureAPI?.macosCapture ?? null
|
||||
}
|
||||
|
||||
protected getBackendLabel(): string {
|
||||
return 'Native macOS'
|
||||
}
|
||||
}
|
||||
|
||||
class NativeWindowsCaptureBackend extends NativePolledCaptureBackend {
|
||||
readonly kind = 'native-windows' as const
|
||||
|
||||
protected getNativeCaptureModule(): NativeSystemCaptureAPI | null {
|
||||
return window.nativeCaptureAPI?.windowsCapture ?? null
|
||||
}
|
||||
|
||||
protected getBackendLabel(): string {
|
||||
return 'Native Windows'
|
||||
}
|
||||
}
|
||||
|
||||
class NativeUnavailableCaptureBackend implements CaptureBackend {
|
||||
@@ -731,9 +762,23 @@ class AudioCapture {
|
||||
}
|
||||
|
||||
private createNativeBackend(supportEntry: CaptureBackendSupportEntry): CaptureBackend {
|
||||
const backend = supportEntry.kind === 'native-macos' && supportEntry.available
|
||||
? new NativeMacOSCaptureBackend(supportEntry)
|
||||
: new NativeUnavailableCaptureBackend(supportEntry)
|
||||
let backend: CaptureBackend
|
||||
|
||||
switch (supportEntry.kind) {
|
||||
case 'native-macos':
|
||||
backend = supportEntry.available
|
||||
? new NativeMacOSCaptureBackend(supportEntry)
|
||||
: new NativeUnavailableCaptureBackend(supportEntry)
|
||||
break
|
||||
case 'native-windows':
|
||||
backend = supportEntry.available
|
||||
? new NativeWindowsCaptureBackend(supportEntry)
|
||||
: new NativeUnavailableCaptureBackend(supportEntry)
|
||||
break
|
||||
default:
|
||||
backend = new NativeUnavailableCaptureBackend(supportEntry)
|
||||
break
|
||||
}
|
||||
|
||||
backend.subscribe((chunk) => this.handleChunk(backend.kind, chunk))
|
||||
return backend
|
||||
|
||||
+26
-11
@@ -1,9 +1,9 @@
|
||||
export interface NativeMacOSCaptureSupport {
|
||||
export interface NativeCaptureSupport {
|
||||
available: boolean
|
||||
reason: string | null
|
||||
}
|
||||
|
||||
export interface NativeMacOSCaptureSource {
|
||||
export interface NativeCaptureSource {
|
||||
id: string
|
||||
label: string
|
||||
kind: 'system'
|
||||
@@ -12,14 +12,14 @@ export interface NativeMacOSCaptureSource {
|
||||
channelCount: number
|
||||
}
|
||||
|
||||
export interface NativeMacOSCaptureStartResult {
|
||||
export interface NativeCaptureStartResult {
|
||||
sampleRate: number
|
||||
channelCount: number
|
||||
deviceId: string
|
||||
deviceLabel: string
|
||||
}
|
||||
|
||||
export interface NativeMacOSCapturedChunk {
|
||||
export interface NativeCapturedChunk {
|
||||
left: Float32Array
|
||||
right: Float32Array
|
||||
channelCount: number
|
||||
@@ -27,21 +27,36 @@ export interface NativeMacOSCapturedChunk {
|
||||
sequence: number
|
||||
}
|
||||
|
||||
export interface NativeMacOSCaptureDrainResult {
|
||||
chunks: NativeMacOSCapturedChunk[]
|
||||
export interface NativeCaptureDrainResult {
|
||||
chunks: NativeCapturedChunk[]
|
||||
overwriteCount: number
|
||||
queueDepth: number
|
||||
}
|
||||
|
||||
export interface NativeMacOSCaptureAPI {
|
||||
getSupport: () => NativeMacOSCaptureSupport
|
||||
listOutputDevices: () => NativeMacOSCaptureSource[]
|
||||
start: (deviceId?: string) => NativeMacOSCaptureStartResult
|
||||
export interface NativeSystemCaptureAPI {
|
||||
getSupport: () => NativeCaptureSupport
|
||||
listOutputDevices: () => NativeCaptureSource[]
|
||||
start: (deviceId?: string) => NativeCaptureStartResult
|
||||
stop: () => void
|
||||
drain: (maxChunks?: number) => NativeMacOSCaptureDrainResult
|
||||
drain: (maxChunks?: number) => NativeCaptureDrainResult
|
||||
nowMilliseconds: () => number
|
||||
}
|
||||
|
||||
export type NativeMacOSCaptureSupport = NativeCaptureSupport
|
||||
export type NativeMacOSCaptureSource = NativeCaptureSource
|
||||
export type NativeMacOSCaptureStartResult = NativeCaptureStartResult
|
||||
export type NativeMacOSCapturedChunk = NativeCapturedChunk
|
||||
export type NativeMacOSCaptureDrainResult = NativeCaptureDrainResult
|
||||
export type NativeMacOSCaptureAPI = NativeSystemCaptureAPI
|
||||
|
||||
export type NativeWindowsCaptureSupport = NativeCaptureSupport
|
||||
export type NativeWindowsCaptureSource = NativeCaptureSource
|
||||
export type NativeWindowsCaptureStartResult = NativeCaptureStartResult
|
||||
export type NativeWindowsCapturedChunk = NativeCapturedChunk
|
||||
export type NativeWindowsCaptureDrainResult = NativeCaptureDrainResult
|
||||
export type NativeWindowsCaptureAPI = NativeSystemCaptureAPI
|
||||
|
||||
export interface NativeCaptureAPI {
|
||||
macosCapture: NativeMacOSCaptureAPI
|
||||
windowsCapture: NativeWindowsCaptureAPI
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user