A native N-API addon that binds libCEC
through its C API (include/cecc.h), the same surface the .NET binding
(src/dotnetlib) uses. It lets Node.js applications control CEC-capable HDMI
devices — power a TV on/standby, become the active source, send remote keys,
read device state — and receive bus events (log, key presses, commands,
source changes, alerts).
It works anywhere libCEC and a C++ toolchain do: Linux, macOS, Raspberry Pi and Windows, over the Pulse-Eight USB-CEC adapter or a SoC-native CEC backend.
- libCEC installed with its development files. On Unix these are discovered
via
pkg-config(pkg-config --exists libcec) — on Debian/Ubuntu that is thelibcec8-devpackage; or build andmake installthis repository first. - Node.js ≥ 16 and a C++17 toolchain (
node-gypprerequisites: a compiler,make, and Python 3).
npm install libcec
The package carries sources, not a prebuilt binary: installing runs node-gyp rebuild, which compiles src/addon.cc against the libCEC already on the
machine. The requirements above have to be met first, or the install fails at
that step. Being built on the consumer's machine is also what lets one package
serve every platform and every Node ≥ 16.
cd src/nodejs
npm install # runs node-gyp rebuild, compiling src/addon.cc against libcec
node example/simple.js
The compiled addon lands at build/Release/cec_native.node.
Windows has no pkg-config, so tell node-gyp where libCEC's headers and its
cec.lib import library are with two environment variables (defaults point at an
installed USB-CEC Adapter SDK). From a repo build:
set LIBCEC_INCLUDE_DIR=..\..\include
set LIBCEC_LIB_DIR=..\..\build\Release\x64\lib
npm install
At runtime the addon needs cec.dll on the DLL search path — keep it next to
cec_native.node (a .node resolves its dependencies from its own directory).
The x64 Windows installer ships a prebuilt addon set up this way under its
nodejs folder, so end users don't need a compiler; see
docs/README.windows.md. The x86 installer does
not — Node.js has had no 32-bit Windows build since v23, so there is no
node.lib to link a 32-bit addon against.
client/cec-client.js is an interactive REPL modelled on the C++ cec-client:
the Node counterpart for exercising the library against real hardware and a
worked example of every API call. Run it with npm run client (or
node client/cec-client.js [port], or the installed cec-client-node bin):
$ npm run client
opening /dev/ttyACM0 ...
connection opened. type 'h' for help, 'q' to quit.
scan
...
tx 1F:82:10:00 # transfer raw bytes
on 0 # power on the TV
pow 0 # query the TV's power status
q
node client/cec-client.js --help lists the command-line options (--list-devices,
--info, --type, --monitor, --log-level, …).
const cec = require('libcec'); // or require('.') from this folder
const adapter = new cec.CecAdapter({
deviceName: 'CECNode',
deviceType: cec.CecDeviceType.RecordingDevice,
});
adapter.on('log', (m) => console.log(m.message));
adapter.on('keyPress', (k) => console.log('key', cec.userControlKeyToString(k.keycode)));
adapter.on('command', (c) => console.log('cmd', cec.opcodeToString(c.opcode)));
adapter.open(); // or adapter.open(port) with a path from detectAdapters()
adapter.powerOnDevices(cec.CecLogicalAddress.TV);
for (const addr of adapter.getActiveDevices())
console.log(cec.logicalAddressToString(addr), adapter.getDeviceOSDName(addr));
adapter.close();CecAdapter is an EventEmitter. libCEC fires its callbacks from its own
worker thread; the addon marshals each one onto the Node event loop via a
ThreadSafeFunction, so handlers run on the main thread like any other event.
| event | argument(s) | source callback |
|---|---|---|
log |
{ message, level, time } |
logMessage |
keyPress |
{ keycode, duration } |
keyPress |
command |
{ initiator, destination, opcode, parameters, ack, eom, opcodeSet, transmitTimeout } |
commandReceived |
sourceActivated |
(logicalAddress, activated) |
sourceActivated |
alert |
alert (a CecAlert) |
alert |
configurationChanged |
{ deviceName, deviceTypes, physicalAddress, logicalAddresses, cecVersion, adapterType, firmwareVersion, … } |
configurationChanged |
menuStateChanged |
state (a CecMenuState) |
menuStateChanged |
commandHandler |
same shape as command |
commandHandler (opt-in) |
commandHandler is off by default — pass { enableCommandHandler: true } to the
constructor to receive it. It routes every command through libCEC's blocking
command-handler path and carries the same data as the cheaper command event, so
it's only worth enabling if you specifically need that hook.
Lifecycle: open(port, timeout=10000), close().
Control: transmit(command), powerOnDevices(address), standbyDevices(address),
setActiveSource(deviceType), setInactiveView(), volumeUp(), volumeDown(),
muteAudio(), sendKeypress(destination, key, wait=true),
sendKeyRelease(destination, wait=true), setOSDString(destination, duration, message).
Queries: getActiveSource(), isActiveSource(address),
getDevicePowerStatus(address), getDeviceVendorId(address),
getDevicePhysicalAddress(address), getDeviceCecVersion(address),
getDeviceOSDName(address), getActiveDevices(), pollDevice(address),
rescanDevices(), pingAdapters(), detectAdapters(), getLibInfo().
Enum helpers (module-level): cecVersionToString, powerStatusToString,
logicalAddressToString, vendorIdToString, opcodeToString,
userControlKeyToString, plus the enum tables (CecLogicalAddress,
CecDeviceType, CecPowerStatus, CecUserControlCode, CecOpcode, …).
commandHandlerandmenuStateChangedcannot suppress libCEC's default handling from JavaScript. Both are dispatched on libCEC's callback thread and expect a synchronous "handled?" return; the addon always answers "not handled" so the library keeps its default behaviour. Honouring a JS return would mean blocking that thread on the Node event loop and racing libCEC's 1000ms timeout. They are therefore exposed as observe-only events.- Call
close()when done. It stops libCEC's worker thread before releasing the thread-safe callbacks, so no event can fire against a torn-down adapter.
GPL-2.0-or-later, or a commercial licence from Pulse-Eight — the same dual licence as libCEC itself.
