Duktape (JavaScript)
The Duktape plugin is the JavaScript twin of the Lua plugin: a shell around one script. The server does sessions, ICE and DTLS, RTP routing and recording; your JavaScript decides what the plugin accepts, what it answers, and who receives whose media. Janus calls functions your script defines, and your script calls host functions that Janus registers as globals. You use it to write a custom plugin in JavaScript without touching Rust or C.
Like Lua, this plugin exists only when a script is configured: with no general.script there is nothing to attach to.
The engine is Boa, not Duktape
The C plugin runs Duktape. This port replaces it with Boa, a pure-Rust JavaScript engine (crate janus-script-js), so the plugin stays free of C. The C host API is kept: the C sample echotest.js loads and runs unchanged, including its Duktape.modSearch module loader. The name Duktape in the config file, the package name and the log lines is historic. getDuktapeVersion() returns 20700, a fixed number that imitates Duktape 2.7.0.
Enable and configure
Build with plugin-duktape, then create janus.plugin.duktape.jcfg:
general: {
path = "/etc/janus/js"
script = "/etc/janus/js/hello.js"
#config = "/path/to/a/file"
}| Key | Meaning |
|---|---|
script | The JavaScript file to load. Required: without it the plugin is not registered. |
path | The folder that getModulesFolder() returns inside the script. Scripts use it to find their own modules (see echotest.js, which reads janus-sdp.js from it with readFile). |
config | A path that is passed unchanged to your init() function. |
The full table is in the Duktape config reference. What the server logs at startup, for each kind of script:
// The Duktape (JavaScript) plugin only starts when janus.plugin.duktape.jcfg names a script in general.script.
// 1. A config file without general.script: the plugin is left out. Server log line:
// janus: plugin janus.plugin.duktape is not loaded: general.script is not set in its config file
// plugins in GET /janus/info: (neither Lua nor Duktape)
// 2. The sample script from the C repo (echotest.js) with its helper modules:
// Loading plugin 'libjanus_duktape.so'...
// [echotest.js] Loading script...
// [echotest.js] Modules folder: /srv/janus/duktape
// [echotest.js] Loading module: janus-sdp
// [echotest.js] Module loaded
// [echotest.js] Script loaded
// [echotest.js] Initializing...
// [echotest.js] Initialized
// GET /janus/info lists janus.plugin.echojs: {"name":"JavaScript based EchoTest","author":"Lorenzo Miniero","description":"This is echotest.js, a JavaScript/Duktape based clone of janus.plugin.echotest","version_string":"0.0.12","version":12}
// 3. A script that lacks the nine required functions: the plugin is not registered. Server log:
// Loading plugin 'libjanus_duktape.so'...
// [ERR] Function 'destroy' is missing in /srv/janus/duktape/bad.js
// [WARN] The 'janus.plugin.duktape' plugin could not be initialized
// plugins in GET /janus/info: (neither Lua nor Duktape)
// 4. A script with a syntax error: the plugin is not registered. Server log:
// Loading plugin 'libjanus_duktape.so'...
// [ERR] Error loading JS script /srv/janus/duktape/bad.js: SyntaxError: abrupt end
// [WARN] The 'janus.plugin.duktape' plugin could not be initialized
// plugins in GET /janus/info: (neither Lua nor Duktape)
// 5. A script file that does not exist: the plugin is not registered. Server log:
// janus: plugin janus.plugin.duktape is not loaded: No such file or directory (os error 2)
// plugins in GET /janus/info: (neither Lua nor Duktape)Unlike the Lua plugin, a Duktape script that fails to load says why: Function 'destroy' is missing in <file> for a missing required function, Error loading JS script <file>: SyntaxError: abrupt end for a syntax error. In both cases the plugin is not registered and is absent from GET /janus/info.
A script to start from
hello.js is the script the transcripts below run. It defines the nine required functions, some optional ones, and answers three kinds of request.
// hello.js: the smallest useful Janus JavaScript plugin (run by Boa in this port).
// Janus calls the nine functions below; the script calls the host functions (pushEvent, timeCallback, ...).
function init(config) {
console.log("hello.js: init, config file = " + config);
}
function destroy() {}
function createSession(id) {}
function destroySession(id) {}
function setupMedia(id) {}
function hangupMedia(id) {}
function resumeScheduler() {}
function querySession(id) {
return JSON.stringify({ script: "hello.js", id: id });
}
// Optional: Admin API message_plugin requests land here (a JSON string in, a JSON string out).
function handleAdminMessage(message) {
return message;
}
// Package name and version: optional, Janus uses these instead of the plugin's own.
function getPackage() { return "janus.plugin.hellojs"; }
function getName() { return "Hello script (JS)"; }
function getVersion() { return 1; }
function getVersionString() { return "0.0.1"; }
function getDescription() { return "A tiny JavaScript plugin for the docs"; }
function getAuthor() { return "docs"; }
// Called later by timeCallback(); the argument is always a string.
function replyLater(arg) {
var parts = arg.split(":");
pushEvent(parseInt(parts[0], 10), parts[1], JSON.stringify({ hello: "later" }), null);
}
// Return value: a JSON string is a synchronous reply, a positive number means "I will answer
// later with pushEvent", a negative number is an error.
function handleMessage(id, tr, msg, jsep) {
var req = JSON.parse(msg);
if (req.request === "ping")
return JSON.stringify({ hello: "world", you_sent: req });
if (req.request === "later") {
timeCallback("replyLater", id + ":" + tr, 300);
return 1;
}
return -1;
}By default the plugin is janus.plugin.duktape. If the script defines getPackage() the plugin is registered under that name (janus.plugin.hellojs here) with the name, author, description and version your get* functions return.
Try it
Your own script: synchronous, asynchronous, error
// hello.js (shown on the page) as the plugin. It sets its own package name, so the plugin is listed as janus.plugin.hellojs.
// → POST /janus
{
"janus": "create",
"transaction": "t-create"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-create",
"data": {
"id": 4100001234567908
}
}
// GET /janus/info lists it as janus.plugin.hellojs: {"name":"Hello script (JS)","author":"docs","description":"A tiny JavaScript plugin for the docs","version_string":"0.0.1","version":1}
// → POST /janus/4100001234567908
{
"janus": "attach",
"plugin": "janus.plugin.hellojs",
"transaction": "t-attach"
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-attach",
"data": {
"id": 4100002469135799
}
}
// A synchronous request: handleMessage returned the reply, so the HTTP answer carries it directly.
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-1",
"body": {
"request": "ping",
"n": 1
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-1",
"sender": 4100002469135799,
"plugindata": {
"plugin": "janus.plugin.hellojs",
"data": {
"hello": "world",
"you_sent": {
"request": "ping",
"n": 1
}
}
}
}
// An asynchronous request: handleMessage returned `1`, the reply comes later through pushEvent with the same transaction.
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-later",
"body": {
"request": "later"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 4100001234567908,
"transaction": "t-later"
}
// ⇠ event, long poll GET /janus/<session>
{
"janus": "event",
"session_id": 4100001234567908,
"transaction": "t-later",
"sender": 4100002469135799,
"plugindata": {
"plugin": "janus.plugin.hellojs",
"data": {
"hello": "later"
}
}
}
// An error: handleMessage returned a negative result.
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-2",
"body": {
"request": "nope"
}
}
// ← 200 OK
{
"janus": "error",
"session_id": 4100001234567908,
"transaction": "t-2",
"error": {
"code": 462,
"reason": "Duktape error"
}
}
// Admin API handle_info (not shown in full): plugin_specific is what querySession returned:
// "plugin_specific": {"script":"hello.js","id":2007654332}
// Admin API message_plugin: handleAdminMessage got the request text and returned it.
// → POST /admin
{
"janus": "message_plugin",
"plugin": "janus.plugin.hellojs",
"request": {
"request": "hello"
},
"transaction": "t-admin",
"admin_secret": "janusoverlord"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-admin",
"response": {
"request": "hello"
}
}Note the error: when handleMessage returns a negative number the client always gets {"code": 462, "reason": "Duktape error"}; the text is fixed, unlike Lua, where the script supplies it.
The sample echotest.js from the C repo
The C repository ships src/plugins/duktape/echotest.js and its helper janus-sdp.js. Put both in the folder named by general.path and point script at echotest.js; no stand-in modules are needed, because the script loads janus-sdp.js itself. It sets getPackage() to janus.plugin.echojs:
// echotest.js from the C repo, loaded as the plugin.
// The script sets its own package name: janus.plugin.echojs. The browser offer is the canned offer of parity/sip-peer/sdp.
// → POST /janus
{
"janus": "create",
"transaction": "t-create"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-create",
"data": {
"id": 4100001234567908
}
}
// → POST /janus/4100001234567908
{
"janus": "attach",
"plugin": "janus.plugin.echojs",
"transaction": "t-attach"
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-attach",
"data": {
"id": 4100002469135799
}
}
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-1",
"body": {
"audio": true,
"video": true
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-1",
"sender": 4100002469135799,
"plugindata": {
"plugin": "janus.plugin.echojs",
"data": {
"echotest": "response",
"result": "ok"
}
}
}
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-2",
"body": {
"audio": true,
"video": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 4100001234567908,
"transaction": "t-2"
}
// ⇠ event, long poll GET /janus/<session>
{
"janus": "event",
"session_id": 4100001234567908,
"transaction": "t-2",
"sender": 4100002469135799,
"plugindata": {
"plugin": "janus.plugin.echojs",
"data": {
"echotest": "event",
"result": "ok"
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 192.0.2.10\r\ns=-\r\n…"
}
}
// Admin API handle_info (not shown in full): plugin_specific is what the script's querySession returned:
// "plugin_specific": {"script":"echotest.js","id":2007654332}
// → POST /admin
{
"janus": "message_plugin",
"plugin": "janus.plugin.echojs",
"request": {
"request": "hello"
},
"transaction": "t-admin",
"admin_secret": "janusoverlord"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-admin",
"response": {
"request": "hello"
}
}
// Server log lines of the script (the script prints them with its own logger):
// [echotest.js] Created new session: 421561188
// [echotest.js] Handling message for session: 421561188
// [echotest.js] Handling message for session: 421561188
// [echotest.js] Resuming coroutines
// [echotest.js] Handling async message for session: 421561188
// [echotest.js] Got offer: [{"type":"v","name":"0"},{"type":"o","name":"- 4611731400430051336 2 IN IP4 1.1.1.1"},{"type":"s","name":"-"},{"type":"t","name":"0 0"
// [echotest.js] Pushing event:
// [echotest.js] Coroutines resumed
// [echotest.js] Queried session: 421561188
// [echotest.js] Got admin message: {"request": "hello"}With a real browser the media comes back:
// echotest.js with a real browser: headless Chrome with a fake camera and microphone.
// The script's setupMedia calls addRecipient(id, id), so the plugin relays the session's media back to itself.
// → POST /janus
{
"janus": "create",
"transaction": "t-create"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-create",
"data": {
"id": 4100001234567908
}
}
// → POST /janus/4100001234567908
{
"janus": "attach",
"plugin": "janus.plugin.echojs",
"transaction": "t-attach"
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-attach",
"data": {
"id": 4100002469135799
}
}
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-1",
"body": {
"audio": true,
"video": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 6100000000001000000 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 4100001234567908,
"transaction": "t-1"
}
// ⇠ event, long poll GET /janus/<session>
{
"janus": "event",
"session_id": 4100001234567908,
"transaction": "t-1",
"sender": 4100002469135799,
"plugindata": {
"plugin": "janus.plugin.echojs",
"data": {
"echotest": "event",
"result": "ok"
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 6100000000001000000 2 IN IP4 192.0.2.10\r\ns=-\r\n…"
}
}
// ⇠ event, long poll GET /janus/<session>
{
"janus": "webrtcup",
"session_id": 4100001234567908,
"sender": 4100002469135799
}
// Browser getStats about 4 seconds after webrtcup: sent audio yes, sent video yes; received back: audio packets > 0, video packets > 0.
// (counts: audio in 192, video in 225, audio out 207, video out 244)The C repository has no videoroom.js; only the echo sample exists for Duktape.
The host functions the port registers
The next transcript runs a script that asks typeof of every global, calls eventsIsEnabled(), and shows the error text thrown for a bad call.
// probe.js: reports the type of every global host function. The reply is exactly what the script returned.
// → POST /janus
{
"janus": "create",
"transaction": "t-create"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-create",
"data": {
"id": 4100001234567908
}
}
// → POST /janus/4100001234567908
{
"janus": "attach",
"plugin": "janus.plugin.duktape",
"transaction": "t-attach"
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-attach",
"data": {
"id": 4100002469135799
}
}
// → POST /janus/4100001234567908/4100002469135799
{
"janus": "message",
"transaction": "t-1",
"body": {
"request": "probe"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 4100001234567908,
"transaction": "t-1",
"sender": 4100002469135799,
"plugindata": {
"plugin": "janus.plugin.duktape",
"data": {
"types": {
"pushEvent": "function",
"closePc": "function",
"endSession": "function",
"relayRtp": "function",
"relayRtcp": "function",
"testExtraFunction": "function",
"readFile": "function",
"getModulesFolder": "function",
"pokeScheduler": "function",
"timeCallback": "function",
"eventsIsEnabled": "function",
"notifyEvent": "function",
"configureMedium": "function",
"addRecipient": "function",
"removeRecipient": "function",
"setBitrate": "function",
"setPliFreq": "function",
"setSubstream": "function",
"setTemporalLayer": "function",
"sendPli": "function",
"relayData": "function",
"relayTextData": "function",
"relayBinaryData": "function",
"startRecording": "function",
"stopRecording": "function",
"getDuktapeVersion": "function"
},
"eventsIsEnabled": 0,
"modulesFolder": "string",
"pushEventWithoutArguments": "Error: Invalid argument (expected DUK_TYPE_NUMBER, got DUK_TYPE_UNDEFINED)\n",
"engine": {
"Duktape": "object",
"require": "function",
"console": "object",
"getDuktapeVersion": 20700
}
}
}
}Reference: what your script defines
These nine functions are required. If one is missing the plugin does not load.
| Function | Called when | Return value |
|---|---|---|
init(config) | the plugin starts. config is general.config | none |
destroy() | Janus shuts down | none |
createSession(id) | a client attaches | none |
destroySession(id) | the client detaches | none |
querySession(id) | the Admin API asks for the handle (handle_info) | a JSON string, shown as plugin_specific |
handleMessage(id, transaction, message, jsep) | the client sends message | a string (JSON text) is the synchronous reply; a number >= 0 means "reply later with pushEvent"; a negative number is an error. message and jsep are JSON strings (jsep is null or undefined when absent) |
setupMedia(id) | the PeerConnection is up | none |
hangupMedia(id) | the PeerConnection is down | none |
resumeScheduler() | the scheduler was poked | none (an empty function is fine) |
Optional functions, with the same arguments as in Lua:
| Function | Called when |
|---|---|
handleAdminMessage(message) | an Admin API message_plugin arrives; return a JSON string |
incomingRtp(id, is_video, buf, len), incomingRtcp(id, is_video, buf, len) | an RTP or RTCP packet arrives. If defined, the default relay is skipped |
incomingTextData(id, buf, len, label, protocol), incomingBinaryData(…) | a data channel message arrives. incomingData is the deprecated name; the server logs a warning if you define it, and again for each binary message when there is no incomingTextData |
dataReady(id) | the data channel can send |
slowLink(id, uplink, is_video) | a slow link was detected |
substreamChanged(id, substream), temporalLayerChanged(id, temporal) | the simulcast layer changed |
getVersion(), getVersionString(), getDescription(), getName(), getAuthor(), getPackage() | at startup; the first answer is kept |
Session ids are JavaScript numbers (integers, no float formatting problem as in Lua). They are random non-zero 32-bit numbers.
Reference: what your script can call
All are globals, not methods of a namespace. A call with a wrongly typed argument throws an Error whose text starts with Invalid argument (expected DUK_TYPE_NUMBER, got DUK_TYPE_UNDEFINED): the verified reply of pushEvent() in the probe transcript. Catch it with try. The host functions of the port match janus_duktape.c (lines 1541 to 1590):
| Function | Arguments | What it does |
|---|---|---|
pushEvent(id, transaction, event, jsep) | session id, transaction string, event as a JSON string, JSEP as a JSON string or null | send an event to the client (the late reply to a request you answered with a number). The JSEP is parsed; a bad SDP throws |
notifyEvent(id, event) | session id or 0, JSON string | send an event to event handlers |
eventsIsEnabled() | none | 1 or 0 |
closePc(id), endSession(id) | session id | close the PeerConnection, or detach the handle |
configureMedium(id, medium, direction, enabled) | session id, "audio", "video" or "data", "in" or "out", boolean | allow or stop a medium |
addRecipient(id, recipient), removeRecipient(id, recipient) | session ids | route the session's media to another session, or stop |
setBitrate(id, bps), setPliFreq(id, seconds) | session id, number | REMB bitrate cap, PLI frequency |
setSubstream(id, n), setTemporalLayer(id, n) | session id, number | simulcast layer |
sendPli(id) | session id | request a keyframe |
relayRtp(id, is_video, buf, len), relayRtcp(id, is_video, buf, len) | session id, boolean, packet string, length | send a packet you built |
relayTextData(id, text, len [, label [, protocol]]), relayBinaryData(…), relayData(…) (deprecated) | session id, payload, length, optional label and protocol | send a data channel message |
startRecording(id, type, codec, folder, filename [, …]) | groups of four: "audio", "video" or "data", codec, folder, base file name | start .mjr recordings |
stopRecording(id, type [, …]) | session id, then types | stop recordings |
pokeScheduler() | none | wake the scheduler, which calls resumeScheduler() |
timeCallback(function_name, argument, ms) | global function name, string or null, delay | call that function after ms ms. hello.js uses it with replyLater |
readFile(path) | string | the text content of a file; throws Error opening file: <path> if it cannot be opened |
getModulesFolder() | none | general.path (or . when unset) |
getDuktapeVersion() | none | 20700 |
testExtraFunction() | none | returns 1234 (the C plugin's sample extra function) |
The engine also provides console.log, JSON, a CommonJS-style require and the Duktape object with modSearch, as the probe shows (Duktape: "object", require: "function", console: "object"). There is no janusLog: log with console.log.
Behaviour you may not expect
- The client error for a failed
handleMessage(a thrown exception, a negative number, an invalid JSON string or any other return type such asundefined) is always{"code": 462, "reason": "Duktape error"}, verified for all four. A thrown exception is logged once asDuktape error: Error: handler boom(verified). An invalid JSON reply and anundefinedreturn leave no log line here; C logsJSON error: on line N: …for the invalid JSON. - A JavaScript exception in the other hooks is logged once per call with C's text (
parity/status.json, waves 232 and 233). readFile("/nonexistent/file")throwsError: Error opening file: /nonexistent/file(verified), which a script cantry/catch.- The scheduler runs
resumeSchedulerandtimeCallbackfunctions on its own thread, one engine call at a time (the engine is locked like C'sduktape_mutex). - The engine has a loop-iteration limit for runaway scripts. The exact policy is an owner decision open in
parity/status.json; it was not run here.
Known gaps
- gap Boa, not Duktape. Scripts that rely on Duktape-only features (the Duktape error fields such as
lineNumber,Duktape.fin,Duktape.act, Duktape's custom Buffer types) may not behave the same. The Cechotest.jsruns. Its debug output of the parsed SDP shows avaluecarried from one SDP line to the next: that comes fromjanus-sdp.js(it declaresvar n, voutside the branch, sovkeeps its old value), which any ES engine does, not from Boa. - gap A reply that is not valid JSON is not logged (C logs the JSON error).
readFileon a file that is not valid UTF-8 is open (needs a capture from C).getDuktapeVersion()is a constant. - Not exercised here:
relayRtp,relayRtcp,startRecording,relayTextData, the data andslowLinkhooks, andDuktape.modSearchwith modules other thanjanus-sdp. They are written fromjanus_duktape.cand the Rust host table. The simulcast path, the PLI relay and the RTP record are ported and covered by the crate's tests (parity/status.json,plugin-duktape). - No
videoroom.jsexists in the C repository to try.
Test it in Janus Bench
Open the Bench Run all checks page and run duktape.echotest (group Duktape (JavaScript)). It attaches to janus.plugin.echojs, negotiates through the script and waits for echoed audio and video. The Bench has no result recorded for it yet; the duktape-echotest-media transcript above is the same flow run by hand, and it echoed both audio and video.