PricingDocsAccount

Plugin development

[object Object]

A plugin is an npm package installed on the host that adds something danio doesn't do itself, such as posting task status to a chat group or uploading films to a cloud drive. This page is about how to write one. For installing and managing plugins, see Plugins.

You write a plugin in JavaScript or TypeScript, and Bun runs it. danio starts one Bun process on the host and loads every enabled plugin into it.

A few things to know first

  • It runs only on the host. Artists' computers and render machines don't install Bun and don't run plugins.
  • danio manages the life cycle. Installing, enabling, updating, settings and logs are all in danio; you don't deploy a process yourself.
  • One plugin's error affects only that plugin. A plugin that fails to load is marked on its own and the rest carry on. When the Bun process exits, danio restarts it, waiting 1 second at first and at most 5 minutes. Even if every plugin crashes, rendering is not affected.
  • Permissions are declared. A plugin calls the workspace API through ctx.api and has only the permissions it declared and an admin confirmed.
  • It is not a sandbox. A plugin runs on the host with danio's rights, can use the network and can read and write files. Permissions limit only what it does to the workspace.

The package

A plugin is an npm package, or a local folder or a .tgz file. The danio field in package.json describes the plugin:

{
  "name": "@my-studio/danio-chat-notify",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "engines": { "danio": ">=1" },
  "danio": {
    "name": "Chat notifications",
    "summary": { "zh": "任务完成和出错时发到聊天群", "en": "Post task results to a chat group" },
    "permissions": [],
    "settings": {
      "type": "object",
      "required": ["webhookUrl"],
      "properties": {
        "webhookUrl": { "type": "string", "title": "Webhook URL", "secret": true }
      }
    }
  }
}
FieldDescription
mainThe entry file. When missing, module is used, and then index.js
engines.danioThe major version of the plugin interface it needs, currently 1. You can write >=1, ^1, 1, 1.x or *. If it isn't compatible, danio doesn't install it
danioRequired. A package without this field is not a danio plugin
danio.nameThe name shown; the package name when missing
danio.iconThe address of an icon, such as a data: address
danio.summaryA one-line description per language: { "zh": "…", "en": "…" }
danio.permissionsThe workspace permissions it needs, such as workspace.view. A plugin that doesn't call ctx.api can write []. For the full list of permissions, see HTTP API
danio.settingsA JSON Schema for the settings

Settings

danio.settings is a JSON Schema, and the admin fills in the settings in the interface according to it. The interface understands these:

  • The type of each item in properties can be string, number, integer or boolean; with an enum, it is a drop-down.
  • title is the name shown in the interface; the key is shown when it is missing.
  • When an item in required isn't filled in, the plugin's row says so.
  • An item with secret: true is write-only: its input is a password field, after saving only "set" is shown, and leaving it empty means no change. The plugin itself can read it in ctx.settings.

When the settings change, the plugin restarts.

The entry

The entry file exports a function by default. Its argument is ctx, and it returns the hooks the plugin implements. The function can be async:

export default async (ctx) => {
  ctx.log('plugin loaded')

  return {
    events: {
      types: ['job.done', 'job.attention'],
      handle: async (event) => {
        // handle the event
      },
    },
  }
}

If it doesn't export a function, or the function throws, the plugin fails to load and the interface shows why.

There are two hooks now: events subscribes to events and output receives films. Both are optional.

ctx

MemberDescription
ctx.apiA client for the workspace API: get(path), post(path, body), put(path, body), patch(path, body), delete(path). path must start with /api/v1/, and the result is parsed JSON. What it does is recorded under "Plugin: " and has only the permissions an admin confirmed. On an error it throws an error that starts with the status code, such as 403 …. A call takes at most 2 minutes
ctx.settingsThe settings the admin filled in, including the secret ones
ctx.stateThe plugin's own small key-value store: get(key), set(key, value), delete(key). Values are JSON, each at most 64 KB. It is kept in the host's database and goes with backups. get returns null when the key isn't there
ctx.logWrite to the log: ctx.log(…), and ctx.log.info, warn, error, debug. console.log in a plugin is recorded here too
ctx.notify(title, body)Show a notice in the danio interface, marked with the plugin it comes from
ctx.workspace{ id, name, addresses }: the workspace ID and name, and the list of the host's addresses

For everything else, use what Bun and npm offer, such as fetch.

events: subscribe to events

events: {
  types: ['job.done', 'shot.failed'],
  handle: async (event) => {
    const { type, data } = event
    // …
  },
}

types lists the event types to subscribe to; * means all. The event that handle receives has these fields:

FieldDescription
idThe event ID, for removing duplicates: the same event may arrive more than once
typeThe event type
atWhen it happened, in ISO 8601
dataThe event's data, see the table below

The event types and the fields in data:

TypeWhendata
job.submittedA new task was submittedjobId, version, by
shot.doneA chunk finished renderingjobId, version, shot (which chunk), frames, device
shot.failedA chunk failedThe same, plus error, attempts
job.attentionA task stopped for a person to act: repeated failures, missing files, a failed stitchjobId, version, reason (empty when paused by hand)
output.readyThe film is stitchedjobId, version, output
job.doneThe task is done, after output.readyThe same as output.ready
device.joinedA device joineddeviceId, device, access
device.removedA device was removeddeviceId, device

For events with a jobId, data also has job (the task name), project (the project name) and projectId.

An example that posts to a chat group when a task is done:

handle: async ({ type, data }) => {
  if (type !== 'job.done') return
  await fetch(ctx.settings.webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: `${data.project} / ${data.job} v${data.version} finished rendering` }),
  })
}

Delivery

danio first stores the event in the host's database, then gives it to the plugins that subscribed to its type, so it arrives at least once:

  • If handle throws, or doesn't return within 1 minute, danio retries, waiting 2 seconds at first, doubling each time, at most 1 hour.
  • After the plugin process crashes or the host restarts, events not yet delivered are delivered then.
  • Events that come while a plugin is disabled are kept and delivered when it is enabled again. Deleting the plugin deletes them too.
  • An event not delivered within a day is given up, and danio writes a log line.

output: deliver films

When a film is stitched, danio hands it to every plugin that implements output, which uploads it to a cloud drive or another system:

output: {
  handle: async ({ job, project, version, film }) => {
    const res = await fetch(`https://example.com/upload/${film.name}`, {
      method: 'PUT',
      body: Bun.file(film.path),
    })
    return { url: `https://example.com/films/${film.name}`, note: `Uploaded to ${project}` }
  },
}

The argument of handle has:

FieldDescription
jobId, job, project, projectId, versionThe task and the version
outputWhere the film is in the store
film.pathThe path of a temporary file on the host. The film has already been fetched from the store, so the plugin needs no store credentials
film.nameThe file name: the task name, the version number and the extension, such as sh010-Main_v2.mp4
film.sizeThe size in bytes
film.open()Returns a stream that reads the file

When handle returns, the temporary file is deleted.

The returned url and note are both optional. They show under Delivery in the task details, one row per version and plugin: sending, retrying, sent, failed.

When handle throws, danio retries as it does for events, waiting at most 1 hour between tries, and one try runs at most 1 hour. If it hasn't succeeded within a day, it is marked failed. After a failure, click Send again, or call POST /api/v1/jobs/{id}/handovers/{plugin}/retry, to hand it to the plugin again.

Install and debug locally

While you write a plugin you don't need to publish it to npm. Install a local folder or .tgz on the host:

# install a folder
danio plugin add ./my-plugin

# or pack it first
npm pack
danio plugin add ./my-plugin-1.0.0.tgz

danio plugin add first shows the permissions the plugin needs and enables it only after you answer y. Add --yes to skip the question.

After changing the code, run danio plugin update. danio reinstalls every plugin from where it came from, keeping settings and permissions.

CommandWhat it does
danio plugin [--json]List plugins: state, version, permissions, subscribed events, the latest error
danio plugin add <npm package | folder | .tgz> [--yes]Install, and enable after the permissions are confirmed
danio plugin on|off|rm <plugin>Enable, disable, delete
danio plugin updateReinstall every plugin from its own source
danio plugin config <plugin> [key=value]…Change settings; without values it asks one by one. secret items can't be written on the command line

For on and config, <plugin> can be the ID, the display name or the package name; off and rm accept only the ID. The ID is the package name without the @, with every character that isn't a lowercase letter, digit, ., _ or - replaced by -; for @my-studio/danio-chat-notify it is my-studio-danio-chat-notify.

These commands run on the host. In the interface, on the Plugins and integrations page, each plugin's menu has Log, which shows the plugin's log, including what console.log prints.

Where Bun comes from

danio looks in this order: the one named by the environment variable DANIO_BUN, the one on PATH, and the one downloaded earlier into runtimes/bun/ in the data directory. If there is none, it downloads the latest release from Bun's GitHub releases page, checks it against its SHASUMS256.txt and puts it there. Plugins are installed in data/plugins/<ID>/ in the data directory.