Live from the studio

A small program on a computer in your gallery takes the programme sound, streams it to Signa, and the signer on your overlay signs as people speak, a couple of seconds behind them.

What you need

  • An overlay already on screen in your graphics system.
  • A computer that can hear the programme: through a capture card (SDI), a sound card's line input, or a stream (SRT, RTMP, NDI-to-audio).
  • Node.js 18 or later (or Python 3.10) and ffmpeg on that computer.

Which sound

Send the cleanest speech you have: the presenters' microphones or a clean feed without music beds and effects. Signing follows speech; music and crowd noise only get in its way.

The bridge

Node.js
// signa-bridge.mjs: studio audio in, the signer on air.
// Needs Node 18+ and ffmpeg. Run:  npm i ws && node signa-bridge.mjs
import { spawn } from "node:child_process";
import WebSocket from "ws";

const KEY = process.env.SIGNA_KEY;          // sk_live_…
const OVERLAY = process.env.OVERLAY_ID;     // from POST /v1/overlays
const LANG = process.env.LANG_SPOKEN ?? "rw";
const TO = process.env.LANG_SIGNED ?? "rsl";
// Where the sound comes from: any ffmpeg input (see the table below).
const INPUT = (process.env.SIGNA_INPUT ?? "-f alsa -i default").split(" ");

function run() {
  const ws = new WebSocket("wss://api.usesigna.org/v1/live");
  let ffmpeg;

  ws.on("open", () =>
    ws.send(JSON.stringify({ type: "start", lang: LANG, token: KEY, overlay: OVERLAY, to: TO })),
  );

  ws.on("message", (data, binary) => {
    if (binary) return;
    const msg = JSON.parse(data.toString());
    if (msg.type === "ready") {
      // 16 kHz, mono, 16-bit PCM on stdout, in real time.
      ffmpeg = spawn("ffmpeg", ["-hide_banner", "-loglevel", "error", ...INPUT,
        "-vn", "-ac", "1", "-ar", "16000", "-f", "s16le", "pipe:1"]);
      ffmpeg.stdout.on("data", (pcm) => ws.readyState === ws.OPEN && ws.send(pcm));
      ffmpeg.on("exit", () => ws.close());
      console.log("On air: signing", LANG, "→", TO);
    }
    if (msg.type === "final") console.log("signed:", msg.text);
    if (msg.type === "error") console.error("Signa:", msg.message);
  });

  // Sessions end after 12 hours or a network drop: start again.
  ws.on("close", () => {
    ffmpeg?.kill();
    setTimeout(run, 2000);
  });
  ws.on("error", () => {});
}

run();
Python
# signa_bridge.py: the same in Python.  pip install websockets
import asyncio, json, os, shlex
import websockets

KEY, OVERLAY = os.environ["SIGNA_KEY"], os.environ["OVERLAY_ID"]
INPUT = shlex.split(os.environ.get("SIGNA_INPUT", "-f alsa -i default"))

async def session():
    async with websockets.connect("wss://api.usesigna.org/v1/live", max_size=None) as ws:
        await ws.send(json.dumps({"type": "start", "lang": "rw", "token": KEY,
                                  "overlay": OVERLAY, "to": "rsl"}))
        ff = None
        async for message in ws:
            msg = json.loads(message)
            if msg["type"] == "ready":
                ff = await asyncio.create_subprocess_exec(
                    "ffmpeg", "-hide_banner", "-loglevel", "error", *INPUT,
                    "-vn", "-ac", "1", "-ar", "16000", "-f", "s16le", "pipe:1",
                    stdout=asyncio.subprocess.PIPE)
                async def pump():
                    while chunk := await ff.stdout.read(3200):  # 100 ms
                        await ws.send(chunk)
                asyncio.create_task(pump())
            elif msg["type"] == "final":
                print("signed:", msg["text"])
        if ff:
            ff.kill()

async def main():
    while True:  # sessions end after 12 hours or a network drop
        try:
            await session()
        except Exception as e:
            print("reconnecting:", e)
        await asyncio.sleep(2)

asyncio.run(main())
Run it
SIGNA_KEY=sk_live_… OVERLAY_ID=5f0c2b8e-… \
SIGNA_INPUT="-f decklink -i 'DeckLink Mini Recorder'" \
node signa-bridge.mjs

Choosing the input

SIGNA_INPUT is whatever comes before the output in an ffmpeg command. Common studio sources:

SourceSIGNA_INPUT
SDI through a Blackmagic DeckLink card-f decklink -i 'DeckLink Mini Recorder'
Line input, Linux-f alsa -i hw:1
Line input, Windows-f dshow -i audio="Line In (USB Audio)"
Line input, macOS-f avfoundation -i :0
An SRT feed from your encoder-i srt://0.0.0.0:9000?mode=listener
An RTMP push from vMix, OBS or an encoder-listen 1 -i rtmp://0.0.0.0:1935/live/signa
An HLS or web stream-re -i https://…/playlist.m3u8
A test, from a file-re -i amakuru.mp4

ffmpeg -sources (and ffmpeg -f decklink -list_devices 1 -i dummy for DeckLink) lists what your computer has.

On air

  • Start the bridge before the programme and leave it running; it reconnects by itself.
  • The caption on the overlay shows what is being signed, so the gallery can see it working.
  • Between programmes, stop the bridge or send silence: live minutes count while it is connected.
  • To sign scripted parts (the news read from the prompter) word-perfect, send the script with /v1/overlays/:id/sign instead, and use live audio for the unscripted parts.