Skip to content

Streaming ​

Streaming turns media that something else produces into a WebRTC broadcast. You define mountpoints. Each is a named source: usually an RTP feed that ffmpeg, GStreamer or a camera sends to a UDP port on the server. Browsers watch a mountpoint, and the plugin relays the packets to every viewer. It can also play a file, pull from an RTSP camera, and carry text over a data channel. Use it for one-to-many broadcasts such as a radio feed, a camera or a restream from another system.

Enable and configure ​

Streaming is not in the default build. Build janus with --features plugin-streaming. Mountpoints live in janus.plugin.streaming.jcfg or are created at run time with the create request. This is the mountpoint the Bench dev configuration uses:

ini
general: {
}

rtp-sample: {
	type = "rtp"            # rtp | live (a file, shared) | ondemand (a file, per viewer) | rtsp
	id = 1
	description = "Opus/VP8 live stream coming from external source"
	audio = true
	video = true
	audioport = 5002        # UDP port the plugin listens on for the audio RTP
	audiopt = 111           # payload type of that audio
	audiocodec = "opus"
	videoport = 5004
	videopt = 100
	videocodec = "vp8"
	secret = "adminpwd"     # needed to edit, destroy, enable, disable, record
}

The full key table, including pin, is_private, simulcast ports, SRTP, RTSP and file options, is in the plugin files reference. A media list with mid, port, pt and codec per stream can replace the audio* and video* keys, and is the only way to make a mountpoint with more than one audio or video stream.

Try it ​

Manage mountpoints ​

list, info, create, edit, enable, disable, destroy, recording and kick_all are synchronous: the reply to the POST is the answer. Attach a handle to janus.plugin.streaming (walkthrough), then:

sh
curl -s -X POST http://127.0.0.1:8088/janus/$SESSION/$HANDLE \
  -d '{"janus":"message","transaction":"t1","body":{"request":"list"}}'
jsonc
// Streaming mountpoint management: list, info, create, edit, enable, disable, recording, kick_all, destroy. All synchronous.

// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary, with the dev configuration (mountpoint 1 from the config file, secret adminpwd).

// → POST /janus
{
  "janus": "create",
  "transaction": "t-1"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-1",
  "data": {
    "id": 9873482636819065
  }
}

// → POST /janus/{session_id}
{
  "janus": "attach",
  "plugin": "janus.plugin.streaming",
  "transaction": "t-2"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-2",
  "data": {
    "id": 9728245592380508
  }
}

// list shows only public mountpoints and no secrets

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

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-3",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "list",
      "list": [
        {
          "id": 1,
          "type": "live",
          "description": "Opus/VP8 live stream coming from external source",
          "metadata": "Feed it with scripts/feed-rtp.mjs or ffmpeg",
          "enabled": true,
          "media": [
            {
              "mid": "a",
              "type": "audio",
              "label": "audio",
              "age_ms": 152
            },
            {
              "mid": "v",
              "type": "video",
              "label": "video",
              "age_ms": 152
            }
          ]
        }
      ]
    }
  }
}

// info without the secret hides ports, secret and PIN; with the secret it shows them

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "info",
    "id": 1
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-4",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "info",
      "info": {
        "id": 1,
        "description": "Opus/VP8 live stream coming from external source",
        "metadata": "Feed it with scripts/feed-rtp.mjs or ffmpeg",
        "enabled": true,
        "type": "live",
        "media": [
          {
            "mindex": 0,
            "type": "audio",
            "mid": "a",
            "label": "audio",
            "pt": 111,
            "codec": "opus",
            "rtpmap": "opus/48000/2",
            "age_ms": 152
          },
          {
            "mindex": 1,
            "type": "video",
            "mid": "v",
            "label": "video",
            "pt": 100,
            "codec": "vp8",
            "rtpmap": "VP8/90000",
            "age_ms": 152
          }
        ]
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-5",
  "body": {
    "request": "info",
    "id": 1,
    "secret": "adminpwd"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-5",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "info",
      "info": {
        "id": 1,
        "name": "rtp-sample",
        "description": "Opus/VP8 live stream coming from external source",
        "metadata": "Feed it with scripts/feed-rtp.mjs or ffmpeg",
        "secret": "adminpwd",
        "enabled": true,
        "viewers": 0,
        "type": "live",
        "media": [
          {
            "mindex": 0,
            "type": "audio",
            "mid": "a",
            "label": "audio",
            "pt": 111,
            "codec": "opus",
            "rtpmap": "opus/48000/2",
            "port": 5002,
            "age_ms": 153
          },
          {
            "mindex": 1,
            "type": "video",
            "mid": "v",
            "label": "video",
            "pt": 100,
            "codec": "vp8",
            "rtpmap": "VP8/90000",
            "port": 5004,
            "age_ms": 153
          }
        ]
      }
    }
  }
}

// create an RTP mountpoint with the media array; port 0 lets the plugin pick a free port

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-6",
  "body": {
    "request": "create",
    "type": "rtp",
    "id": 5000,
    "name": "docs-mp",
    "description": "Docs mountpoint",
    "secret": "s3",
    "pin": "4242",
    "media": [
      {
        "type": "audio",
        "mid": "a",
        "port": 0,
        "pt": 111,
        "codec": "opus"
      },
      {
        "type": "video",
        "mid": "v",
        "port": 0,
        "pt": 100,
        "codec": "vp8"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-6",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "created",
      "created": "docs-mp",
      "permanent": false,
      "stream": {
        "id": 5000,
        "type": "live",
        "description": "Docs mountpoint",
        "is_private": false,
        "ports": [
          {
            "type": "audio",
            "mid": "a",
            "port": 10000
          },
          {
            "type": "video",
            "mid": "v",
            "port": 10001
          }
        ]
      }
    }
  }
}

// the older flat form still works and answers with a deprecation warning

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-7",
  "body": {
    "request": "create",
    "type": "rtp",
    "id": 5001,
    "audio": true,
    "audioport": 0,
    "audiopt": 111,
    "audiortpmap": "opus/48000/2",
    "video": false
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-7",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "created",
      "created": "mp-5001",
      "permanent": false,
      "stream": {
        "id": 5001,
        "type": "live",
        "description": "mp-5001",
        "is_private": false,
        "audio_port": 10002
      },
      "warning": "deprecated_api"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-8",
  "body": {
    "request": "destroy",
    "id": 5001
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-8",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "destroyed",
      "destroyed": 5001
    }
  }
}

// edit: change description and metadata, hide it from list

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-9",
  "body": {
    "request": "edit",
    "id": 5000,
    "secret": "s3",
    "new_description": "Edited",
    "new_metadata": "{\"v\":2}",
    "new_is_private": true
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-9",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "edited",
      "id": 5000,
      "permanent": false
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-10",
  "body": {
    "request": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-10",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "list",
      "list": [
        {
          "id": 1,
          "type": "live",
          "description": "Opus/VP8 live stream coming from external source",
          "metadata": "Feed it with scripts/feed-rtp.mjs or ffmpeg",
          "enabled": true,
          "media": [
            {
              "mid": "a",
              "type": "audio",
              "label": "audio",
              "age_ms": 154
            },
            {
              "mid": "v",
              "type": "video",
              "label": "video",
              "age_ms": 154
            }
          ]
        }
      ]
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-11",
  "body": {
    "request": "info",
    "id": 5000,
    "secret": "s3"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-11",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "info",
      "info": {
        "id": 5000,
        "name": "docs-mp",
        "description": "Edited",
        "metadata": "{\"v\":2}",
        "secret": "s3",
        "pin": "4242",
        "is_private": true,
        "enabled": true,
        "viewers": 0,
        "type": "live",
        "media": [
          {
            "mindex": 0,
            "type": "audio",
            "mid": "a",
            "label": "audio",
            "pt": 111,
            "codec": "opus",
            "rtpmap": "opus/48000/2",
            "port": 10000,
            "age_ms": 1
          },
          {
            "mindex": 1,
            "type": "video",
            "mid": "v",
            "label": "video",
            "pt": 100,
            "codec": "vp8",
            "rtpmap": "VP8/90000",
            "port": 10001,
            "age_ms": 1
          }
        ]
      }
    }
  }
}

// disable keeps the mountpoint but refuses viewers; enable undoes it

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-12",
  "body": {
    "request": "disable",
    "id": 5000,
    "secret": "s3"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-12",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "ok"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-13",
  "body": {
    "request": "enable",
    "id": 5000,
    "secret": "s3"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-13",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "ok"
    }
  }
}

// recording: start per medium with a base filename; stop needs the secret too when the mountpoint has one

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-14",
  "body": {
    "request": "recording",
    "action": "start",
    "id": 5000,
    "secret": "s3",
    "media": [
      {
        "mid": "a",
        "filename": "/tmp/janus/docs-mp-audio"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-14",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "ok"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-15",
  "body": {
    "request": "recording",
    "action": "stop",
    "id": 5000,
    "media": [
      {
        "mid": "a"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-15",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 453,
      "error": "Missing mandatory element (secret)"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-16",
  "body": {
    "request": "recording",
    "action": "stop",
    "id": 5000,
    "secret": "s3",
    "media": [
      {
        "mid": "a"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-16",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "ok"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-17",
  "body": {
    "request": "kick_all",
    "id": 5000,
    "secret": "s3"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-17",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "kicked_all"
    }
  }
}

// errors

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-18",
  "body": {
    "request": "edit",
    "id": 5000,
    "secret": "bad",
    "new_description": "x"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-18",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 457,
      "error": "Unauthorized (wrong secret)"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-19",
  "body": {
    "request": "destroy",
    "id": 5000,
    "secret": "s3"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-19",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "destroyed",
      "destroyed": 5000
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-20",
  "body": {
    "request": "destroy",
    "id": 5000,
    "secret": "s3"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-20",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 455,
      "error": "No such mountpoint/stream 5000"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-21",
  "body": {
    "request": "info"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-21",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 453,
      "error": "Missing mandatory element (id)"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-22",
  "body": {
    "request": "create",
    "type": "bogus"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-22",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 454,
      "error": "Unknown stream type 'bogus'...\n"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-23",
  "body": {
    "request": "create",
    "type": "rtp",
    "id": 1,
    "media": [
      {
        "type": "audio",
        "mid": "a",
        "port": 0,
        "pt": 111,
        "codec": "opus"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-23",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 456,
      "error": "A stream with the provided ID already exists"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-24",
  "body": {
    "request": "create",
    "type": "live",
    "id": 79,
    "filename": "/nonexistent.opus",
    "audio": true
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-24",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 456,
      "error": "Error creating 'live' stream"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-25",
  "body": {
    "request": "bogus"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-25",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 452,
      "error": "Unknown request 'bogus'"
    }
  }
}

// → POST /janus/{session_id}
{
  "janus": "destroy",
  "transaction": "t-26"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "t-26"
}

Port 0 in create makes the plugin pick a free port from its range; the reply lists it in stream.ports.

Send media to a mountpoint ​

An RTP mountpoint listens on its UDP port. Anything that sends RTP with the configured payload type there is relayed. The next transcript creates a mountpoint, waits, then sends five packets with Node's dgram and shows age_ms in info, the time since the last packet arrived.

jsonc
// Feeding an RTP mountpoint: the plugin listens on a UDP port; any RTP packet with the right payload type that arrives there is relayed. info shows age_ms, the time since the last packet.

// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary. The script sends five 20 ms Opus-sized RTP packets with Node's dgram; a real feeder is ffmpeg or GStreamer (see the page).

// → POST /janus
{
  "janus": "create",
  "transaction": "t-1"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-1",
  "data": {
    "id": 987348263681906
  }
}

// → POST /janus/{session_id}
{
  "janus": "attach",
  "plugin": "janus.plugin.streaming",
  "transaction": "t-2"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 987348263681906,
  "transaction": "t-2",
  "data": {
    "id": 6972824559238050
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-3",
  "body": {
    "request": "create",
    "type": "rtp",
    "id": 6000,
    "description": "Feed demo",
    "media": [
      {
        "type": "audio",
        "mid": "a",
        "port": 0,
        "pt": 111,
        "codec": "opus"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 987348263681906,
  "transaction": "t-3",
  "sender": 6972824559238050,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "created",
      "created": "mp-6000",
      "permanent": false,
      "stream": {
        "id": 6000,
        "type": "live",
        "description": "Feed demo",
        "is_private": false,
        "ports": [
          {
            "type": "audio",
            "mid": "a",
            "port": 10002
          }
        ]
      }
    }
  }
}

// 1.5 seconds without packets: age_ms has grown

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "info",
    "id": 6000
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 987348263681906,
  "transaction": "t-4",
  "sender": 6972824559238050,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "info",
      "info": {
        "id": 6000,
        "name": "mp-6000",
        "description": "Feed demo",
        "enabled": true,
        "viewers": 0,
        "type": "live",
        "media": [
          {
            "mindex": 0,
            "type": "audio",
            "mid": "a",
            "label": "audio",
            "pt": 111,
            "codec": "opus",
            "rtpmap": "opus/48000/2",
            "port": 10002,
            "age_ms": 1502
          }
        ]
      }
    }
  }
}

// after the packets: age_ms is back near zero

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-5",
  "body": {
    "request": "info",
    "id": 6000
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 987348263681906,
  "transaction": "t-5",
  "sender": 6972824559238050,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "info",
      "info": {
        "id": 6000,
        "name": "mp-6000",
        "description": "Feed demo",
        "enabled": true,
        "viewers": 0,
        "type": "live",
        "media": [
          {
            "mindex": 0,
            "type": "audio",
            "mid": "a",
            "label": "audio",
            "pt": 111,
            "codec": "opus",
            "rtpmap": "opus/48000/2",
            "port": 10002,
            "age_ms": 123
          }
        ]
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "t-6",
  "body": {
    "request": "destroy",
    "id": 6000
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 987348263681906,
  "transaction": "t-6",
  "sender": 6972824559238050,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "destroyed",
      "destroyed": 6000
    }
  }
}

For a real feed you can run the Bench's feeder, which sends Opus silence at payload type 111:

sh
node webapp/scripts/feed-rtp.mjs 5002

With ffmpeg or GStreamer, send RTP to the port with the same payload type and codec as the mountpoint (for example Opus at 111 for audiopt = 111). That is how the C documentation does it too; those tools were not run in this documentation build.

Watch ​

watch is asynchronous. The plugin answers with a preparing event carrying its SDP offer. You send start with your answer in jsep, and get starting. When ICE and DTLS finish the plugin sends started, and the core sends webrtcup. pause and start pause and resume, configure mutes single streams, stop ends the watch.

This transcript runs the signalling with a hand-made answer built from the server's own offer, so no media flows. It also shows the PIN errors and that disable does not refuse a watch.

jsonc
// Watching mountpoint 1: watch, start with an answer, pause, start, stop. The offer is the server's; the answer is built from it by the script, so ICE and DTLS never complete and no media flows.

// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary.

// → POST /janus
{
  "janus": "create",
  "transaction": "viewer-1"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "viewer-1",
  "data": {
    "id": 9873482636819065
  }
}

// → POST /janus/{session_id}
{
  "janus": "attach",
  "plugin": "janus.plugin.streaming",
  "transaction": "viewer-2"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "viewer-2",
  "data": {
    "id": 9728245592380508
  }
}

// watch is asynchronous: ack first, then a preparing event that carries the server's offer.

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-3",
  "body": {
    "request": "watch",
    "id": 1
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-3"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-3",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "preparing"
      }
    }
  },
  "jsep": {
    "type": "offer",
    "sdp": "v=0\r\no=- 4661856518455979 1 IN IP4 192.168.1.10\r\ns=Mountpoint 1\r\n…"
  }
}

// start with the answer in jsep.

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-4",
  "body": {
    "request": "start"
  },
  "jsep": {
    "type": "answer",
    "sdp": "v=0\r\no=- 5783672383433022915 2 IN IP4 127.0.0.1\r\ns=Mountpoint 1\r\n…"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-4"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-4",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "starting"
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-5",
  "body": {
    "request": "pause"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-5"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-5",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "pausing"
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-6",
  "body": {
    "request": "start"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-6"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-6",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "starting"
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-7",
  "body": {
    "request": "configure",
    "streams": [
      {
        "mid": "v",
        "send": false
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-7"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-7",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "event": "configured"
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-8",
  "body": {
    "request": "stop"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-8"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-8",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "idle"
      }
    }
  }
}

// errors: this handle never connected, so stop answered idle and it still counts as watching mountpoint 1: a second watch on it is error 460.

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer-9",
  "body": {
    "request": "watch",
    "id": 1
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "viewer-9"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "viewer-9",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 460,
      "error": "Already watching mountpoint 1"
    }
  }
}

// A new handle: no such mountpoint, then a mountpoint with a PIN.

// → POST /janus
{
  "janus": "create",
  "transaction": "viewer2-1"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "viewer2-1",
  "data": {
    "id": 14067003194067
  }
}

// → POST /janus/{session_id}
{
  "janus": "attach",
  "plugin": "janus.plugin.streaming",
  "transaction": "viewer2-2"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 14067003194067,
  "transaction": "viewer2-2",
  "data": {
    "id": 6084927222952167
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-3",
  "body": {
    "request": "watch",
    "id": 1234
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 14067003194067,
  "transaction": "viewer2-3"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 14067003194067,
  "transaction": "viewer2-3",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 455,
      "error": "No such mountpoint/stream 1234"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-4",
  "body": {
    "request": "create",
    "type": "rtp",
    "id": 77,
    "secret": "s",
    "pin": "1234",
    "media": [
      {
        "type": "audio",
        "mid": "a",
        "port": 0,
        "pt": 111,
        "codec": "opus"
      }
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 14067003194067,
  "transaction": "viewer2-4",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "created",
      "created": "mp-77",
      "permanent": false,
      "stream": {
        "id": 77,
        "type": "live",
        "description": "mp-77",
        "is_private": false,
        "ports": [
          {
            "type": "audio",
            "mid": "a",
            "port": 10003
          }
        ]
      }
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-5",
  "body": {
    "request": "watch",
    "id": 77
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 14067003194067,
  "transaction": "viewer2-5"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 14067003194067,
  "transaction": "viewer2-5",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 453,
      "error": "Missing mandatory element (pin)"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-6",
  "body": {
    "request": "watch",
    "id": 77,
    "pin": "bad"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 14067003194067,
  "transaction": "viewer2-6"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 14067003194067,
  "transaction": "viewer2-6",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "error_code": 457,
      "error": "Unauthorized (wrong pin)"
    }
  }
}

// disable does not refuse watch: the viewer gets an offer, but the plugin relays nothing while the mountpoint is disabled (C gates the relay, not the watch).

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-7",
  "body": {
    "request": "disable",
    "id": 77,
    "secret": "s"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 14067003194067,
  "transaction": "viewer2-7",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "ok"
    }
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-8",
  "body": {
    "request": "watch",
    "id": 77,
    "pin": "1234"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 14067003194067,
  "transaction": "viewer2-8"
}

// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
  "janus": "event",
  "session_id": 14067003194067,
  "transaction": "viewer2-8",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "preparing"
      }
    }
  },
  "jsep": {
    "type": "offer",
    "sdp": "v=0\r\no=- 5492240366593942 1 IN IP4 192.168.1.10\r\ns=Mountpoint 77\r\n…"
  }
}

// → POST /janus/{session_id}/{handle_id}
{
  "janus": "message",
  "transaction": "viewer2-9",
  "body": {
    "request": "destroy",
    "id": 77,
    "secret": "s"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 14067003194067,
  "transaction": "viewer2-9",
  "sender": 6084927222952167,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "destroyed",
      "destroyed": 77
    }
  }
}

The next one is the real thing from Chrome, recorded in a Janus Bench run (streaming.watch) with a feeder running: started, webrtcup, and RTP reaching the browser.

jsonc
// Watching mountpoint 1 from Chrome. Cut from a Janus Bench run (streaming.watch): after the answer, ICE and DTLS complete and the plugin sends started, then webrtcup.

// → sent
{
  "janus": "create",
  "transaction": "bench-mw29ks1b-2m"
}

// ← received
{
  "janus": "success",
  "transaction": "bench-mw29ks1b-2m",
  "data": {
    "id": 9873482636819065
  }
}

// → sent
{
  "janus": "attach",
  "plugin": "janus.plugin.streaming",
  "transaction": "bench-mw29ks1h-2n",
  "session_id": 9873482636819065
}

// ← received
{
  "janus": "success",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks1h-2n",
  "data": {
    "id": 9728245592380508
  }
}

// → sent
{
  "janus": "message",
  "handle_id": 9728245592380508,
  "body": {
    "request": "watch",
    "id": 1
  },
  "transaction": "bench-mw29ks1o-2o",
  "session_id": 9873482636819065
}

// ← received
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks1o-2o"
}

// ← received
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks1o-2o",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "preparing"
      }
    }
  },
  "jsep": {
    "type": "offer",
    "sdp": "v=0\r\no=- 4661856518455979 1 IN IP4 192.168.1.10\r\ns=Mountpoint 1\r\n…"
  }
}

// → sent
{
  "janus": "message",
  "handle_id": 9728245592380508,
  "body": {
    "request": "start"
  },
  "jsep": {
    "type": "answer",
    "sdp": "v=0\r\no=- 578367238343302291 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
  },
  "transaction": "bench-mw29ks23-2p",
  "session_id": 9873482636819065
}

// ← received
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks23-2p"
}

// ← received
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks23-2p",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "starting"
      }
    }
  }
}

// → sent
{
  "janus": "trickle",
  "handle_id": 9728245592380508,
  "candidate": {
    "candidate": "candidate:5040670031 1 udp 9406760849 192.168.1.10 56167 typ host generation 0 ufrag xCjH network-cost 999",
    "sdpMid": "a",
    "sdpMLineIndex": 0
  },
  "transaction": "bench-mw29ks2c-2q",
  "session_id": 9873482636819065
}

// ← received
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks2c-2q"
}

// → sent
{
  "janus": "trickle",
  "handle_id": 9728245592380508,
  "candidate": {
    "completed": true
  },
  "transaction": "bench-mw29ks5d-2r",
  "session_id": 9873482636819065
}

// ← received
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29ks5d-2r"
}

// ⇠ event pushed by the server
{
  "janus": "event",
  "session_id": 9873482636819065,
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "started"
      }
    }
  }
}

// ⇠ event pushed by the server
{
  "janus": "webrtcup",
  "session_id": 9873482636819065,
  "sender": 9728245592380508
}

// → sent
{
  "janus": "message",
  "handle_id": 9728245592380508,
  "body": {
    "request": "stop"
  },
  "transaction": "bench-mw29kt7j-2s",
  "session_id": 9873482636819065
}

// ← received
{
  "janus": "ack",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29kt7j-2s"
}

// ← received
{
  "janus": "event",
  "session_id": 9873482636819065,
  "transaction": "bench-mw29kt7j-2s",
  "sender": 9728245592380508,
  "plugindata": {
    "plugin": "janus.plugin.streaming",
    "data": {
      "streaming": "event",
      "result": {
        "status": "stopping"
      }
    }
  }
}

Reference ​

Synchronous requests ​

RequestParametersReply streaming
listnonelist, with list: for each public mountpoint id, type, description, metadata, enabled, media (mid, type, label, age_ms)
infoid (required), secretinfo, with info. The name, secret, pin, viewers and per-stream port fields, and is_private (only when true), appear only when the mountpoint has no secret or you pass the right one.
createtype (required: rtp, live, ondemand, rtsp), id, name, description, metadata, secret, pin, is_private, media (array), permanent, admin_key (when configured), and the type's own keys (filename, url, SRTP, simulcast, …)created, with created (the mountpoint's name), permanent and stream (id, type, description, is_private, and for RTP ports)
editid, secret (when the mountpoint has one), new_description, new_metadata, new_secret, new_pin (empty string removes it), new_is_private, permanent, edited_eventedited, with id and permanent
destroyid, secret, permanentdestroyed, with destroyed: the id
enable, disableid, secret; disable also stop_recording (default true)ok
recordingaction: start or stop, id, secret (when the mountpoint has one), media: array of {mid, filename} (start) or {mid} (stop). The old audio, video, data form is deprecated.ok
kick_allid, secretkicked_all

Asynchronous requests ​

Each returns an ack; the result is an event on the same handle with the same transaction.

RequestParametersResult result.status (or event)
watchid (required), pin (when the mountpoint has one), media (array of mids; empty means all), deprecated offer_audio, offer_video, offer_datapreparing, with the server's offer in jsep
startnone; the answer in jsep the first time and after a renegotiationstarting; later started when the PeerConnection is up
pausenonepausing
configurestreams: array of {mid, send, substream, temporal, fallback, spatial_layer, temporal_layer, min_delay, max_delay}event: "configured" (verified)
switchid: another RTP mountpoint with the same codecsswitched: "ok" and id
stopnonestopping; or idle when the viewer was not started yet (verified)

Error codes ​

CodeMeaning
450No message
451Invalid JSON
452Invalid request (an unknown request, or a missing media list in recording)
453Missing mandatory element (pin for a watch without the PIN, secret for an edit without it)
454Invalid element (for example an unknown stream type)
455No such mountpoint
456Can't create (the id exists, or the source could not start: a missing file, a port in use)
457Unauthorized (wrong secret or PIN)
458Can't switch
459Can't record
460Invalid state (for example already watching)
461Invalid SDP
470Unknown error

The transcripts above show 452, 453, 454 (an unknown type), 455, 456, 457 and 460. The others are from the C source.

Behaviour you may not expect ​

These differ from what the C doxygen text says. In each case the C source and the Rust binary agree.

  • type in list and info is live for an RTP mountpoint, and on demand for an on-demand file: it names the streaming context, not the create type. The doxygen only says "in line with the types introduced above".
  • The create reply names the mountpoint in created; the doxygen shows create. The destroy reply carries the id in destroyed, not id.
  • is_private defaults to false; the doxygen says "true by default".
  • disable does not block watch. A viewer can watch a disabled mountpoint and gets an offer, but nothing is relayed until it is enabled again (the C code gates the relay, not the watch).
  • recording with action stop needs secret if the mountpoint has one, as start does. Without it: 453.
  • age_ms starts counting at creation. A mountpoint nobody has fed shows a growing age_ms, not a missing field, so a rising number is how you tell that a feed has stopped.
  • The older flat form of create (audio, audioport, audiopt, audiortpmap, …) still works. The reply then has audio_port in stream and "warning":"deprecated_api".
  • A stop before the viewer has started answers idle and does not release the handle; a second watch on it is 460 "Already watching".
  • list returns mountpoints in the hash-table order of the C server, not sorted by id.

Known gaps ​

GapStatus
RTSP mountpointsThe client (DESCRIBE, SETUP, PLAY, keepalive, Basic and Digest auth, reconnect) is ported, with request text checked against the C server. It was not run here: it needs an RTSP server. rtsps:// (TLS) and redirects or NTLM through libcurl are not supported. gap
File mountpoints (live, ondemand)Ported, with the Ogg and OpusHead parsing and the clock arithmetic unit tested. Not run here. gap
Simulcast and SVC mountpointsLayer selection is ported, unit tested and compared with janus-rtp over 20 000 generated steps. No Bench check and no browser run. gap
Viewer sends its own offer in watchThe port reads only the type of the offer and sends none back. start ignores the answer, as C does.

Plain RTP viewing works end to end in Chrome: the Bench check streaming.watch connects and receives RTP from the feeder.

Test it in Janus Bench ​

Open the Streaming page, or run streaming.mountpoints and streaming.watch (both pass). Run the feeder (node webapp/scripts/feed-rtp.mjs <audio port>, which the headless runner does for you) to also see packets arrive. See Janus Bench.

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