Live streams

Publishing streams

Send encrypted live data from Python, the native plugin, or any language.

Credentials

Publishing needs only a Kymoscope API key: create one in your account and put it in your script. A key has two parts, kymoscope_<auth>.<secret>. Only the auth part is ever sent to Kymoscope, to prove who is publishing. The secret part stays on your machine and is what your stream is encrypted with, so data stays end-to-end encrypted: the server can check the key but never decrypt the stream.

Your own streams open in any browser you sign in to, with nothing to type. Each browser holds a device key pair whose private half never leaves it; the sender seals the stream key to every device of your account (on register and every heartbeat, via the wrap_keys action), and the server only stores those sealed keys. You can see and revoke devices in your account. To let someone else watch, give them a password instead (pass password=), or the key; a key also lets them publish as you.

Python

Install the kymoscope package from PyPI (Python 3.9 or later).

Shell
pip install kymoscope
Python
import math, time
from kymoscope import LiveStream

API_KEY = "kymoscope_..."   # Account -> API & real-time streaming, or a project's API key
# state_path keeps this sender's identity, so a restart needs no new approval
with LiveStream(API_KEY, "Rig A", state_path="kymoscope-state.json") as stream:
    print("stream id:", stream.stream_id)
    while True:
        t = time.time()           # absolute (Unix seconds)
        stream.send([{"t": t, "signals": [
            {"name": "speed", "unit": "m/s", "value": 50 + 10 * math.sin(t)},
        ]}])
        time.sleep(1 / 60)
  • The package registers the stream, encrypts, keeps the stream alive and renews its credentials.
  • It only sends while at least one viewer is connected, unless it keeps a buffer (below).
  • visibility is "private", "public" or "project" (with project=): only the project’s members see a project stream. "org" (with org=) is legacy and publishes into the organization’s General project.
  • project="Bench rigs" (a name or id) publishes into a project of your organization; a project API key publishes into its project without it.
  • Leaving the with block (or close()) keeps the stream registered, so a restart with the same state_path resumes it in the project it is in. Pass delete_on_exit=True (or close(delete=True)) to delete it.

At kHz rates, send blocks by column with send_columns: lists, arrays or numpy arrays, a few times a second. Values travel as float32 (float64 for the channels you name), times as float64, and the block is split into as many messages as the relay’s size limit needs. If you send faster than your account’s bandwidth, send_columns waits instead of letting the relay drop data.

Python
import time
import numpy as np

RATE = 10_000                     # Hz
t0 = time.time()
n = 0
with LiveStream(API_KEY, "Test bench") as stream:
    while True:
        time.sleep(1 / 30)        # ~30 blocks a second
        due = int((time.time() - t0) * RATE) - n
        t = t0 + (n + np.arange(due)) / RATE
        stream.send_columns(t, {"accel_x": read_accel_x(due), "strain": read_strain(due)},
                            units={"accel_x": "g", "strain": "µε"})
        n += due

Keep every sample when the connection drops

Python
with LiveStream(API_KEY, "Rig A", state_path="kymoscope-state.json",
                buffer_path="kymoscope-buffer", buffer_max_mb=2048) as stream:
    ...

With buffer_path (a folder), the stream always sends, also with nobody watching, so a cloud recording keeps everything. While the relay is unreachable it keeps the encrypted messages on disk, then sends them in order when the relay is back. Viewers see that data in the recording, not in the live view; a crash can send a few messages twice. buffer_max_mb caps the disk used; when it is full, new samples are dropped and counted in stream.buffer_dropped. buffer_min_free_mb (default 0, off) also stops buffering when the disk has less free space than that. Since 0.3.0 the stream sends the compact format by default (4–8× less data); block_format=False sends the classic format for viewers on SDK versions before 0.2.0 or self-hosted relays before 0.2.0 (buffer_path always uses the compact one). With buffer_path, send absolute times (time.time()), so data sent after a restart lands in the right place in the recording.

Any language (personal streams)

The protocol for personal streams is small enough to implement anywhere with HTTPS, WebSocket, PBKDF2 and AES-GCM. It does not publish into end-to-end encrypted projects (the server answers sender_update_required); use the kymoscope Python package or the native plugins for those.

  1. Mint a publisher token. POST https://clahbbkqdskkhymymoge.supabase.co/functions/v1/stream-token with header x-api-key set to the auth part of the key only (everything before the .); never send the secret part. The response has token (a pt. token, 15 minutes), userId, relayUrl, maxBytesPerSec and maxViewers (the bandwidth and viewers per stream agreed for your account), maxMessageBytes (the relay’s message size limit) and maxRateHz (for senders of 0x02 batches). The token only works with the publishing endpoints below; it is not an account session.
  2. Derive the key. salt is 16 random bytes (base64). The key is PBKDF2-HMAC-SHA256(secret, salt, 600,000 iterations, 32 bytes), where secret is the part of the API key after the . (or a password, if you prefer one). key_check is AES-256-GCM of the bytes "kymoscope-live-key-check-v1" including the quotes: { iv, ciphertext }, both base64, ciphertext with the 16-byte tag appended.
  3. Register the stream. POST https://clahbbkqdskkhymymoge.supabase.co/functions/v1/stream-publisher with Authorization: Bearer <pt. token> and body { "action": "register", name, salt, key_check, visibility, project? }. The response has stream (id, share_code) and ticket, a pp. publish ticket valid for 10 minutes for this stream only. project is a project name or id. It is optional: a project API key publishes into its project without it. visibility: 'org' is still accepted and goes to the organization’s General project. Re-registering the same stream_id keeps the stream in the project it is in (for example after someone moved it in the app) while you may still publish there; only a project API key moves it elsewhere. Deleting the stream forgets that placement: the next register goes where the sender asks. To keep a placement made in the app, don’t delete the stream on exit.
  4. Connect. Open wss://<relay>/v1/streams/<id>/publish with Sec-WebSocket-Protocol: kymoscope-live.v1, bearer.<pp. ticket>. The relay sends {"type":"viewers","count":n}; send only while n > 0, and send the text ping every 30 seconds.
  5. Send batches. One binary message per batch: [kind][12-byte IV][AES-256-GCM(plaintext, aad = kind) ‖ tag], where the plaintext is either msgpack({frames:[{t, signals}]}) (kind 0x02) or a block of samples by column (kind 0x03, below). The relay enforces your account’s maxBytesPerSec (with up to 2 seconds of burst): messages past it are dropped, and sustained excess closes the socket. Pace your sending to it.
  6. Stay listed and authorised. Every ~10 s call stream-publisher with { "action": "heartbeat", stream_id }; it returns a fresh ticket. Before the socket’s ticket expires, send {"type":"auth","token":"<new ticket>"} as text, or the relay closes it with code 4401. On exit call { "action": "delete", stream_id }, and mint a new pt. token before the old one expires.

Column blocks (kind 0x03), for high sample rates. Little-endian; every block names its channels, so a viewer joining mid-stream needs nothing else:

Shell
u8   version = 1
u8   reserved = 0
u16  channel count C
u32  sample count n
C x  u8 bytes per value (4 = float32, 8 = float64), u8 name length, u8 unit length,
     name (UTF-8), unit (UTF-8)
zero padding to a multiple of 8 bytes
n x  float64 time (non-decreasing)
per channel: n values, zero-padded to a multiple of 8 bytes   (NaN = missing)

Native Le Mans Ultimate plugin

A self-contained C++ DLL streams the simulator’s telemetry (around 80 channels at 60 Hz) with the same protocol, using only Windows APIs. It is a working reference for embedded or native publishers.