Skip to content

Lua ​

The Lua plugin is a shell around a Lua script. The server does the low-level work (sessions, ICE and DTLS, RTP routing, recording); your script decides what the plugin is: which requests it accepts, what it answers, and who receives whose media. Janus calls functions your script defines, and your script calls host functions Janus registers as Lua globals. You use it to write a custom plugin, such as an echo test or a small conferencing logic, without writing Rust or C.

Unlike the other plugins, this one exists only when a script is configured. There is no janus.plugin.lua to attach to unless general.script names a file that loads.

The Lua VM is C code

plugin-lua embeds Lua 5.3 through the mlua crate with vendored C sources. It needs a C compiler to build and is the documented exception to the pure-Rust rule (ADR-0008, under ADR-0007). It is Lua 5.3, the version the C server targets, not LuaJIT and not 5.4.

Enable and configure ​

Build with plugin-lua, then create janus.plugin.lua.jcfg:

text
general: {
	path = "/etc/janus/lua"
	script = "/etc/janus/lua/hello.lua"
	#config = "/path/to/a/file"
}
KeyMeaning
scriptThe Lua file to load. Required: without it the plugin is not registered.
pathA folder added to Lua's module search path, so require("json") finds /etc/janus/lua/json.lua. Several folders go in one string separated by semicolons.
configA path that is passed unchanged to your init() function. Only your script uses it.

The full table is in the Lua config reference. The plugin is checked at startup:

jsonc
// The Lua plugin only starts when janus.plugin.lua.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.lua 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.lua) with its helper modules:

//    Loading plugin 'libjanus_lua.so'...

//    [echotest.lua] Loading...

//    [echotest.lua] Loaded

//    [echotest.lua] Initializing...

//    [echotest.lua] Initialized

//    GET /janus/info lists janus.plugin.echolua: {"name":"Lua based EchoTest","author":"Lorenzo Miniero","description":"This is echotest.lua, a Lua 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_lua.so'...

//    [WARN] The 'janus.plugin.lua' 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_lua.so'...

//    [WARN] The 'janus.plugin.lua' 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.lua is not loaded: cannot read /srv/janus/lua/missing.lua: No such file or directory (os error 2

//    plugins in GET /janus/info: (neither Lua nor Duktape)

The transcript shows what the server logs for each case. A missing file and a config without general.script are logged with a reason. A script that lacks a required function, or has a syntax error, is not logged with a reason (see Known gaps): you only see The 'janus.plugin.lua' plugin could not be initialized.

A script to start from ​

This is hello.lua, the script the transcripts below run. It defines the nine required functions, a few optional ones, and answers three kinds of request.

lua
-- hello.lua: the smallest useful Janus Lua plugin.
-- Janus calls the nine functions below; the script calls the host functions (pushEvent, timeCallback, ...).
local json = require("json")

function init(config)
  janusLog(4, "hello.lua: init, config file = " .. tostring(config))
end

function destroy() end
function createSession(id) end
function destroySession(id) end
function setupMedia(id) end
function hangupMedia(id) end
function resumeScheduler() end

function querySession(id)
  return json.encode({ script = "hello.lua", id = id })
end

-- Optional: Admin API message_plugin requests land here (a JSON string in, a JSON string out).
function handleAdminMessage(message)
  return message
end

-- Package name and version: optional, Janus uses these instead of "janus.plugin.lua".
function getPackage() return "janus.plugin.hello" end
function getName() return "Hello script" end
function getVersion() return 1 end
function getVersionString() return "0.0.1" end
function getDescription() return "A tiny Lua plugin for the docs" end
function getAuthor() return "docs" end

-- Called later by timeCallback(); the argument is always a string.
function reply_later(arg)
  local id, tr = string.match(arg, "^(%d+):(.*)$")
  pushEvent(tonumber(id), tr, json.encode({ hello = "later" }), nil)
end

-- Return values:  0, json   synchronous reply
--                 1, nil    "I will answer later with pushEvent"
--                -1, text   error (text goes into the error reply)
function handleMessage(id, tr, msg, jsep)
  local req = json.decode(msg)
  if req.request == "ping" then
    return 0, json.encode({ hello = "world", you_sent = req })
  elseif req.request == "later" then
    -- id is a Lua float (session ids are pushed with lua_pushnumber), so format it as an integer first.
    timeCallback("reply_later", string.format("%d:%s", id, tr), 300)
    return 1, nil
  end
  return -1, "Unknown request"
end

The package name: by default the plugin is janus.plugin.lua. If the script defines getPackage(), the plugin is registered under that name instead (janus.plugin.hello here), and GET /janus/info lists it with the name, author, description and version from the script's getName, getAuthor, getDescription, getVersion and getVersionString.

Try it ​

Your own script: synchronous, asynchronous, error ​

jsonc
// hello.lua (shown on the page) as the plugin. It sets its own package name, so the plugin is listed as janus.plugin.hello.


// → 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.hello: {"name":"Hello script","author":"docs","description":"A tiny Lua plugin for the docs","version_string":"0.0.1","version":1}


// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.hello",
  "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.hello",
    "data": {
      "hello": "world",
      "you_sent": {
        "n": 1,
        "request": "ping"
      }
    }
  }
}


// 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.hello",
    "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": "Unknown request"
  }
}


// Admin API handle_info (not shown in full): plugin_specific is what querySession returned:

//   "plugin_specific": {"id":2007654332,"script":"hello.lua"}


// Admin API message_plugin: handleAdminMessage got the request text and returned it.

// → POST /admin
{
  "janus": "message_plugin",
  "plugin": "janus.plugin.hello",
  "request": {
    "request": "hello"
  },
  "transaction": "t-admin",
  "admin_secret": "janusoverlord"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-admin",
  "response": {
    "request": "hello"
  }
}

The sample echotest.lua from the C repo ​

The C repository ships src/plugins/lua/echotest.lua and videoroom.lua. They require three modules that C installs next to them (json, term.colors, socket) and two helper files (janus-sdp.lua, janus-logger.lua). This port has no such modules built in: the transcripts were recorded with pure-Lua stand-ins for json, term.colors and socket that live in parity/oracle/script-modules/lua/, plus the C sample's own helpers, all in the folder named by general.path.

The sample sets getPackage() to janus.plugin.echolua, so you attach to that name:

jsonc
// echotest.lua from the C repo, loaded as the plugin (with pure-Lua stand-ins for json, term.colors and socket from parity/oracle/script-modules).

// The script sets its own package name: janus.plugin.echolua. 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.echolua",
  "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.echolua",
    "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.echolua",
    "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": {"id":2007654332,"script":"echotest.lua"}


// → POST /admin
{
  "janus": "message_plugin",
  "plugin": "janus.plugin.echolua",
  "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.lua] Created new session: 2700277088.0

//    [echotest.lua] Handling message for session: 2700277088.0

//    [echotest.lua] Handling message for session: 2700277088.0

//    [echotest.lua] Resuming coroutines

//    [echotest.lua] Handling async message for session: 2700277088.0

//    [echotest.lua] Got offer: v=0

//    [echotest.lua] Pushing event:

//    [echotest.lua] Coroutines resumed

//    [echotest.lua] Queried session: 2700277088.0

//    [echotest.lua] Got admin message: {"request": "hello"}

The script parsed the offer, built an answer with its own SDP library, and pushed it from a coroutine that the scheduler resumed (pokeScheduler and resumeScheduler). With a real browser the media comes back too:

jsonc
// echotest.lua 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.echolua",
  "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.echolua",
    "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 226, audio out 207, video out 248)

videoroom.lua is the second sample. It does not override the package name, so it is janus.plugin.lua:

jsonc
// videoroom.lua from the C repo as the plugin. It does not override the package name, so the plugin is janus.plugin.lua.


// → 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.lua",
  "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": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.lua",
    "data": {
      "list": [],
      "videoroom": "success"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "create",
    "room": 1234,
    "description": "Lua room"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.lua",
    "data": {
      "room": 1234,
      "videoroom": "created"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-3",
  "body": {
    "request": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-3",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.lua",
    "data": {
      "list": {
        "1234": {
          "audioCodec": [
            "opus"
          ],
          "description": "Lua room",
          "participants": [],
          "privateIds": [],
          "publishers": 3,
          "roomId": 1234,
          "videoCodec": [
            "vp8"
          ]
        }
      },
      "videoroom": "success"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "exists",
    "room": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-4",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.lua",
    "data": {
      "exists": true,
      "room": 1234,
      "videoroom": "success"
    }
  }
}

The host functions the port registers ​

The next transcript runs a script that asks type() of every global. The reply is the script's own return value.

jsonc
// probe.lua: 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.lua",
  "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.lua",
    "data": {
      "eventsIsEnabled": 0,
      "lua": "Lua 5.3",
      "types": {
        "addRecipient": "function",
        "closePc": "function",
        "configureMedium": "function",
        "endSession": "function",
        "eventsIsEnabled": "function",
        "getModulesFolder": "nil",
        "janusLog": "function",
        "notifyEvent": "function",
        "pokeScheduler": "function",
        "pushEvent": "function",
        "readFile": "nil",
        "relayBinaryData": "function",
        "relayData": "function",
        "relayRtcp": "function",
        "relayRtp": "function",
        "relayTextData": "function",
        "removeRecipient": "function",
        "sendPli": "function",
        "setBitrate": "function",
        "setPliFreq": "function",
        "setSubstream": "function",
        "setTemporalLayer": "function",
        "startRecording": "function",
        "stopRecording": "function",
        "testExtraFunction": "function",
        "timeCallback": "function"
      },
      "wrongArgs": -1
    }
  }
}

Reference: what your script defines ​

Your script must define these nine functions. If one is missing, the plugin does not load.

FunctionCalled whenReturn value
init(config)the plugin starts. config is general.config, or nilnone
destroy()Janus shuts downnone
createSession(id)a client attachesnone
destroySession(id)the client detachesnone
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 messagetwo values: 0, json_text (reply now), 1, nil (reply later with pushEvent) or a negative number and an error text. message and jsep are JSON strings; jsep is nil when absent
setupMedia(id)the PeerConnection is upnone
hangupMedia(id)the PeerConnection is downnone
resumeScheduler()the scheduler was poked (pokeScheduler)none (an empty function is fine)

Optional functions:

FunctionCalled when
handleAdminMessage(message)an Admin API message_plugin request 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. Discouraged: slow
incomingTextData(id, buf, len, label, protocol), incomingBinaryData(id, buf, len, label, protocol)a data channel message arrives (the older incomingData is deprecated)
dataReady(id)the data channel is ready for sending
slowLink(id, uplink, is_video)Janus detected a slow link
substreamChanged(id, substream), temporalLayerChanged(id, temporal)the simulcast layer a session receives changed
getVersion(), getVersionString(), getDescription(), getName(), getAuthor(), getPackage()at startup; the first answer is kept

Session ids are Lua numbers that are floats. Janus pushes each id with lua_pushnumber, so "id: " .. id prints 4233110103.0. Use string.format("%d", id) or math.tointeger(id) when you build a string key or a transaction (the log of the echotest sample shows the .0).

Reference: what your script can call ​

Each function returns 0 on success, -1 on a wrong argument count or an unknown session (janusLog returns nothing on a wrong count). The table follows janus_lua.c.

FunctionArgumentsWhat it does
janusLog(level, text)log level (number), stringwrite to the Janus log
pushEvent(id, transaction, event, jsep)session id, the transaction of the request, event as a JSON string, JSEP as a JSON string or nilsend an event to the client (the late reply to a request you answered with 1, nil)
notifyEvent(id, event)session id or 0, JSON stringsend an event to event handlers
eventsIsEnabled()none1 if event handlers are enabled, else 0
closePc(id)session idclose the PeerConnection
endSession(id)session iddetach the plugin handle
configureMedium(id, medium, direction, enabled)session id, "audio", "video" or "data", "in" or "out", booleanallow or stop receiving or sending a medium
addRecipient(id, recipient), removeRecipient(id, recipient)session idsroute (or stop routing) the session's media to another session
setBitrate(id, bps)session id, numberforce a bitrate with REMB
setPliFreq(id, seconds)session id, numberhow often to send a PLI to the session
setSubstream(id, n), setTemporalLayer(id, n)session id, numberchoose the simulcast layer
sendPli(id)session idsend a keyframe request now
relayRtp(id, is_video, buf, len), relayRtcp(id, is_video, buf, len)session id, boolean, string with the packet, lengthsend a packet you built to a session
relayTextData(id, text, len [, label [, protocol]]), relayBinaryData(...)session id, payload, length, optional label and protocolsend a data channel message (relayData is the deprecated name)
startRecording(id, type, codec, folder, filename [, …])session id, then groups of four: "audio", "video" or "data", codec, folder, base file namestart .mjr recordings
stopRecording(id, type [, type…])session id, then "audio", "video", "data"stop recordings
pokeScheduler()nonewake the scheduler, which calls resumeScheduler()
timeCallback(function_name, argument, ms)global function name (string), a string or nil, delaycall that function after ms milliseconds; exactly three arguments
testExtraFunction()nonethe sample "extra" function of the C plugin: returns 1234

getModulesFolder and readFile do not exist in Lua (they are Duktape only): the probe shows them as nil.

Behaviour you may not expect ​

  • A script error is not fatal. An error raised inside handleMessage, a reply that is not valid JSON, or no return value at all gives the client {"janus": "error", "error": {"code": 462, "reason": "Lua error"}} (verified), and the plugin keeps running. C calls handleMessage with lua_call, which does not catch errors (janus_lua.c line 1958), so do not rely on the port's recovery matching C.
  • The error reply for a negative handleMessage result carries your text: {"janus": "error", "error": {"code": 462, "reason": "Unknown request"}}.
  • A function called through timeCallback runs with the string argument you gave it.
  • Numbers, floats and strings follow Lua 5.3 rules, including the .0 suffix when a whole float becomes text. Non-finite floats become null on the way to Janus, and a table (array or object) pushed from Janus into your script arrives as nil: pass JSON strings.
  • With getPackage() the plugin is not janus.plugin.lua any more. A client that attaches to janus.plugin.lua gets "no such plugin".

Known gaps ​

  • gap Script load errors are silent for the reason. A script without the nine functions, or with a syntax error, makes the plugin disappear with only The 'janus.plugin.lua' plugin could not be initialized in the log (see the lua-load transcript, cases 3 and 4). C logs Function 'x' is missing in <file> and Error loading Lua script <file>: <error>. The Duktape plugin of this port does log the reason, so the missing line is specific to the Lua plugin.
  • gap Runtime errors in callbacks are not logged. Verified at debug level 7: a handleMessage that calls error("handler boom") and a timeCallback function that calls error("timer boom") leave no line in the server log. Only the client sees Lua error. When a script misbehaves, add your own janusLog calls.
  • The Lua 5.3 VM is a C binding (ADR-0008). Differences from C that the ADR keeps on purpose: non-finite floats read as null, a non-UTF-8 string in a text position reads as null (binary payloads are exempt), arrays and objects pushed to the script are nil, janusLog with a wrong argument count returns nil, and tracebacks are dropped from error text.
  • getModulesFolder and readFile are not provided (as in C's Lua plugin).
  • Simulcast layer choice and the recording layer choice follow C (parity/status.json, wave 152); not exercised here.
  • Not exercised here: relayRtp and relayRtcp from a script, startRecording, relayTextData, the data callbacks and slowLink. They are written from janus_lua.c and the Rust host table. The echotest sample exercises addRecipient, configureMedium, pushEvent, notifyEvent, pokeScheduler and the get* overrides; hello.lua exercises timeCallback and pushEvent.

Test it in Janus Bench ​

Open the Bench Run all checks page and run lua.echotest (group Lua). It attaches to janus.plugin.echolua, negotiates through the script and waits for echoed audio and video. The Bench has no result recorded for it yet; the lua-echotest-media transcript above is the same flow run by hand, and it echoed both audio and video.

GPL-3.0-or-later, like the Janus gateway it ports.