diff --git a/README.md b/README.md index 770eb90..29323ed 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,18 @@ This library intends to be a Node.js wrapper for NDI v5 (and following) library. ## Features +- Find API + - [x] Listing Sources - Send API - [x] Creating Send Instance - [x] Sending Video Streams - [x] Sending Audio Streams - [x] Sending Audio/Video Streams +- Recv API + - [x] Setting Tally ## Roadmap -- Find API - - [ ] Listing Sources - Send API - [ ] Sending Metadata - [ ] Receiving Tally @@ -23,7 +25,6 @@ This library intends to be a Node.js wrapper for NDI v5 (and following) library. - [ ] Receiving Video Streams - [ ] Receiving Audio Streams - [ ] Receiving Metadata - - [ ] Setting Tally ## Installation diff --git a/binding.gyp b/binding.gyp index 6b49a76..c437cf0 100644 --- a/binding.gyp +++ b/binding.gyp @@ -8,9 +8,12 @@ "cflags_cc+": ["-fexceptions"], "sources": [ "src/ndi.cpp", + "src/find/find_sources.cpp", + "src/find/source_instance.cpp", "src/send/send_create.cpp", "src/send/send_instance.cpp", "src/structures/audio_frame.cpp", + "src/structures/tally_state.cpp", "src/structures/video_frame.cpp", ], "include_dirs": [ diff --git a/include/find/find_sources.d.ts b/include/find/find_sources.d.ts new file mode 100644 index 0000000..8d95a4e --- /dev/null +++ b/include/find/find_sources.d.ts @@ -0,0 +1,3 @@ +import type { SourceInstance } from './source_instance'; + +export function findSources (timeout?: number): Promise; diff --git a/include/find/find_sources.h b/include/find/find_sources.h new file mode 100644 index 0000000..b7ca2d9 --- /dev/null +++ b/include/find/find_sources.h @@ -0,0 +1,8 @@ +#include + +#ifndef _SRC_FIND_FIND_SOURCES_H_ +#define _SRC_FIND_FIND_SOURCES_H_ + +Napi::Value FindSources(const Napi::CallbackInfo&); + +#endif diff --git a/include/find/source_instance.d.ts b/include/find/source_instance.d.ts new file mode 100644 index 0000000..b273a9b --- /dev/null +++ b/include/find/source_instance.d.ts @@ -0,0 +1,9 @@ +import { TallyState } from '../structures/tally_state'; + +export interface SourceInstance { + setTally (tallyState: TallyState): void; + + get ipAddress(): string; + get name(): string; + get urlAddress(): string; +} diff --git a/include/find/source_instance.h b/include/find/source_instance.h new file mode 100644 index 0000000..791bdd3 --- /dev/null +++ b/include/find/source_instance.h @@ -0,0 +1,30 @@ +#include +#include + +#ifndef _SRC_STRUCTURES_SOURCE_INSTANCE_H_ +#define _SRC_STRUCTURES_SOURCE_INSTANCE_H_ + +class SourceInstance : public Napi::ObjectWrap { +public: + static void Init(Napi::Env env, Napi::Object exports); + static Napi::Object New(Napi::Env env, NDIlib_source_t *ndiSourceInstance); + static Napi::FunctionReference constructor; + + SourceInstance(const Napi::CallbackInfo &info); + ~SourceInstance(); + + void SetTally(const Napi::CallbackInfo &info); + + Napi::Value GetIpAddress(const Napi::CallbackInfo &info); + Napi::Value GetName(const Napi::CallbackInfo &info); + Napi::Value GetUrlAddress(const Napi::CallbackInfo &info); + + +private: + void Initialize(const Napi::CallbackInfo &info); + + NDIlib_source_t ndiSourceInstance; + NDIlib_recv_instance_t ndiRecvInstance; +}; + +#endif diff --git a/include/ndi.h b/include/ndi.h index 181507e..7bdf4bc 100644 --- a/include/ndi.h +++ b/include/ndi.h @@ -2,6 +2,8 @@ #include #include +#include +#include static Napi::Object Init(Napi::Env env, Napi::Object exports); static void onDestroyEnvironment(void *arg); diff --git a/include/structures/tally_state.d.ts b/include/structures/tally_state.d.ts new file mode 100644 index 0000000..e870e52 --- /dev/null +++ b/include/structures/tally_state.d.ts @@ -0,0 +1,11 @@ +export interface TallyState { + /** + * Whether a source should be marked as "onProgram", which can be understood as "LIVE", "Recording", etc. + */ + onProgram: boolean; + + /** + * Whether a source should be marked as "onPreview", which indicates that e.g the source is being previewed by a monitor. + */ + onPreview: boolean; +} diff --git a/include/structures/tally_state.h b/include/structures/tally_state.h new file mode 100644 index 0000000..9dfc431 --- /dev/null +++ b/include/structures/tally_state.h @@ -0,0 +1,17 @@ +#include +#include + +#ifndef _SRC_STRUCTURES_TALLY_STATE_H_ +#define _SRC_STRUCTURES_TALLY_STATE_H_ + +class TallyState { +public: + TallyState(const Napi::Object &); + + bool onProgram; + bool onPreview; + + operator NDIlib_tally_t() const; +}; + +#endif diff --git a/package-lock.json b/package-lock.json index 0030dce..4c85d10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ndi.js", - "version": "1.0.0", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ndi.js", - "version": "1.0.0", + "version": "1.0.5", "cpu": [ "x64", "arm64" @@ -27,7 +27,7 @@ "dotenv": "^10.0.0", "fs-extra": "^10.0.0", "jest": "^27.3.1", - "node-addon-api": "^4.2.0", + "node-addon-api": "^5.0.0", "node-gyp": "^8.4.1", "zx": "^4.3.0" } @@ -3923,9 +3923,9 @@ } }, "node_modules/node-addon-api": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz", - "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==", "dev": true }, "node_modules/node-fetch": { @@ -8351,9 +8351,9 @@ "dev": true }, "node-addon-api": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz", - "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==", "dev": true }, "node-fetch": { diff --git a/package.json b/package.json index 7603d2f..91f1beb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ndi.js", - "version": "1.0.5", + "version": "1.1.0", "description": "Wrapper library for NDI", "type": "module", "main": "src/index.js", @@ -42,7 +42,7 @@ "dotenv": "^10.0.0", "fs-extra": "^10.0.0", "jest": "^27.3.1", - "node-addon-api": "^4.2.0", + "node-addon-api": "^5.0.0", "node-gyp": "^8.4.1", "zx": "^4.3.0" }, diff --git a/src/find/find_sources.cpp b/src/find/find_sources.cpp new file mode 100644 index 0000000..537692d --- /dev/null +++ b/src/find/find_sources.cpp @@ -0,0 +1,37 @@ +#include +#include +#include + +Napi::Value FindSources(const Napi::CallbackInfo &info) { + NDIlib_find_create_t NDI_find_create_desc; + NDIlib_find_instance_t pNDI_find = + NDIlib_find_create_v2(&NDI_find_create_desc); + + uint32_t timeout_in_ms = 5000; + if (info.Length() == 1) { + timeout_in_ms = info[0].As(); + } + + if (!NDIlib_find_wait_for_sources(pNDI_find, timeout_in_ms)) { + return Napi::Array::New(info.Env()); + } else { + // Get the updated list of sources + uint32_t no_sources = 0; + const NDIlib_source_t *p_sources = + NDIlib_find_get_current_sources(pNDI_find, &no_sources); + + // Display all the sources. + Napi::Array sourcesList = Napi::Array::New(info.Env(), (size_t)no_sources); + + for (uint32_t i = 0; i < no_sources; i++) { + NDIlib_source_t p_source = NDIlib_source_t(p_sources[i]); + + Napi::Object sourceInstance = SourceInstance::New(info.Env(), &p_source); + sourcesList[i] = sourceInstance; + } + + NDIlib_find_destroy(pNDI_find); + + return sourcesList; + } +} diff --git a/src/find/source_instance.cpp b/src/find/source_instance.cpp new file mode 100644 index 0000000..4eb884d --- /dev/null +++ b/src/find/source_instance.cpp @@ -0,0 +1,84 @@ +#include +#include +#include + +using namespace std; + +Napi::Object SourceInstance::New(Napi::Env env, + NDIlib_source_t *ndiSourceInstance) { + return SourceInstance::constructor.Value().New({ + Napi::External::New(env, ndiSourceInstance), + }); +} + +SourceInstance::SourceInstance(const Napi::CallbackInfo &info) + : Napi::ObjectWrap(info) { + Napi::Env env = info.Env(); + + if (info.Length() < 1 || (info[0].Type() != napi_external)) { + throw Napi::Error::New( + env, "SourceInstances can only be constructed by native code"); + } + + NDIlib_source_t ndiSourceInstance = + *(info[0].As >().Data()); + + string str_ndi_name = ndiSourceInstance.p_ndi_name; + char *cstr_ndi_name = new char[str_ndi_name.length()]; + strcpy(cstr_ndi_name, str_ndi_name.c_str()); + + string str_url_address = ndiSourceInstance.p_url_address; + char *cstr_url_address = new char[str_url_address.length()]; + strcpy(cstr_url_address, str_url_address.c_str()); + + this->ndiSourceInstance = NDIlib_source_t(cstr_ndi_name, cstr_url_address); + + this->Initialize(info); +} + +void SourceInstance::Initialize(const Napi::CallbackInfo &info) { + NDIlib_recv_create_v3_t NDI_recv_create_desc(this->ndiSourceInstance); + + try { + this->ndiRecvInstance = NDIlib_recv_create_v3(&NDI_recv_create_desc); + + if (!this->ndiRecvInstance) { + throw Napi::Error::New(info.Env(), + "Could not initialize source receiver"); + } + } catch (const Napi::Error &error) { + error.ThrowAsJavaScriptException(); + } +} + +void SourceInstance::SetTally(const Napi::CallbackInfo &info) { + if (!info[0].IsObject()) { + Napi::TypeError::New(info.Env(), + "The tallyState argument is expected to be an object") + .ThrowAsJavaScriptException(); + return; + } + + NDIlib_tally_t tally_state = (TallyState)info[0].ToObject(); + NDIlib_recv_set_tally(this->ndiRecvInstance, &tally_state); +} + +Napi::Value SourceInstance::GetIpAddress(const Napi::CallbackInfo &info) { + return Napi::String::New(info.Env(), this->ndiSourceInstance.p_ip_address); +} + +Napi::Value SourceInstance::GetName(const Napi::CallbackInfo &info) { + return Napi::String::New(info.Env(), this->ndiSourceInstance.p_ndi_name); +} + +Napi::Value SourceInstance::GetUrlAddress(const Napi::CallbackInfo &info) { + return Napi::String::New(info.Env(), this->ndiSourceInstance.p_url_address); +} + +SourceInstance::~SourceInstance() { + if (!this->ndiRecvInstance) { + return; + } + + NDIlib_recv_destroy(this->ndiRecvInstance); +} diff --git a/src/index.d.ts b/src/index.d.ts index 4201343..b583b55 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,4 @@ +export * from '../include/find/find_sources'; export * from '../include/structures/audio_frame'; export * from '../include/structures/video_frame'; export * from '../include/send/send_create'; diff --git a/src/index.js b/src/index.js index cab0772..92b26fd 100644 --- a/src/index.js +++ b/src/index.js @@ -6,5 +6,10 @@ const addon = bindings('ndi'); */ const SendInstance = addon.SendInstance; -export { SendInstance }; +/** + * @type {import('./index').findSources} + */ +const findSources = addon.findSources; + +export { SendInstance, findSources }; export * from './structures/video_frame.js'; diff --git a/src/ndi.cpp b/src/ndi.cpp index 692a2ba..c326b8f 100644 --- a/src/ndi.cpp +++ b/src/ndi.cpp @@ -8,6 +8,9 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) { } SendInstance::Init(env, Napi::Value(env, exports).As()); + SourceInstance::Init(env, Napi::Value(env, exports).As()); + + exports.Set("findSources", Napi::Function::New(env, FindSources)); napi_status status = napi_add_env_cleanup_hook(env, onDestroyEnvironment, exports); @@ -28,6 +31,24 @@ void SendInstance::Init(Napi::Env env, Napi::Object exports) { exports.Set("SendInstance", func); } +Napi::FunctionReference SourceInstance::constructor; +void SourceInstance::Init(Napi::Env env, Napi::Object exports) { + // This method is used to hook the accessor and method callbacks + Napi::Function func = + DefineClass(env, "SourceInstance", + { + InstanceMethod<&SourceInstance::SetTally>("setTally"), + InstanceAccessor<&SourceInstance::GetIpAddress>("ipAddress"), + InstanceAccessor<&SourceInstance::GetName>("name"), + InstanceAccessor<&SourceInstance::GetUrlAddress>("urlAddress"), + }); + + SourceInstance::constructor = Napi::Persistent(func); + SourceInstance::constructor.SuppressDestruct(); + + exports.Set("SourceInstance", func); +} + static void onDestroyEnvironment(void *arg) { NDIlib_destroy(); } NODE_API_MODULE(ndi, Init) diff --git a/src/structures/tally_state.cpp b/src/structures/tally_state.cpp new file mode 100644 index 0000000..1db8e48 --- /dev/null +++ b/src/structures/tally_state.cpp @@ -0,0 +1,14 @@ +#include + +TallyState::TallyState(const Napi::Object &object) + : onProgram(object.Get("onProgram").ToBoolean()), + onPreview(object.Get("onPreview").ToBoolean()) {} + +TallyState::operator NDIlib_tally_t() const { + NDIlib_tally_t out; + + out.on_preview = this->onPreview; + out.on_program = this->onProgram; + + return out; +}