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).
pip install kymoscopeimport 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).
visibilityis"private","public"or"project"(withproject=): only the project’s members see a project stream."org"(withorg=) 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
withblock (orclose()) keeps the stream registered, so a restart with the samestate_pathresumes it in the project it is in. Passdelete_on_exit=True(orclose(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.
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 += dueKeep every sample when the connection drops
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.
- Mint a publisher token.
POST https://clahbbkqdskkhymymoge.supabase.co/functions/v1/stream-tokenwith headerx-api-keyset to the auth part of the key only (everything before the.); never send the secret part. The response hastoken(apt.token, 15 minutes),userId,relayUrl,maxBytesPerSecandmaxViewers(the bandwidth and viewers per stream agreed for your account),maxMessageBytes(the relay’s message size limit) andmaxRateHz(for senders of0x02batches). The token only works with the publishing endpoints below; it is not an account session. - Derive the key.
saltis 16 random bytes (base64). The key is PBKDF2-HMAC-SHA256(secret, salt, 600,000 iterations, 32 bytes), wheresecretis the part of the API key after the.(or a password, if you prefer one).key_checkis 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. - Register the stream.
POST https://clahbbkqdskkhymymoge.supabase.co/functions/v1/stream-publisherwithAuthorization: Bearer <pt. token>and body{ "action": "register", name, salt, key_check, visibility, project? }. The response hasstream(id,share_code) andticket, app.publish ticket valid for 10 minutes for this stream only.projectis 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 samestream_idkeeps 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 nextregistergoes where the sender asks. To keep a placement made in the app, don’t delete the stream on exit. - Connect. Open
wss://<relay>/v1/streams/<id>/publishwithSec-WebSocket-Protocol: kymoscope-live.v1, bearer.<pp. ticket>. The relay sends{"type":"viewers","count":n}; send only whilen > 0, and send the textpingevery 30 seconds. - Send batches. One binary message per batch:
[kind][12-byte IV][AES-256-GCM(plaintext, aad = kind) ‖ tag], where the plaintext is eithermsgpack({frames:[{t, signals}]})(kind0x02) or a block of samples by column (kind0x03, below). The relay enforces your account’smaxBytesPerSec(with up to 2 seconds of burst): messages past it are dropped, and sustained excess closes the socket. Pace your sending to it. - Stay listed and authorised. Every ~10 s call
stream-publisherwith{ "action": "heartbeat", stream_id }; it returns a freshticket. 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 newpt.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:
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.