[1]:
# Install hypertools (run this first on Colab)
import importlib.util
if importlib.util.find_spec('hypertools') is None:
    %pip install -q "hypertools[interactive,lsl]"

Streaming from a Lab Streaming Layer (LSL) device

Lab Streaming Layer (LSL) is the de facto standard for streaming time-synchronized data – EEG, eye tracking, motion capture, physiological signals, and more – from acquisition hardware/software into downstream analysis tools over the local network. hyp.io.lsl_stream() (GH #130) resolves a live LSL stream and wraps it as a plain Python iterator of per-sample numeric vectors – exactly the shape hyp.plot’s streaming support (GH #101, see the streaming data tutorial) already expects from any generator – so an LSL stream plugs straight into hyp.plot(..., stream_init=..., stream_chunk=...) with no extra glue.

pylsl (which wraps the native liblsl library used by essentially every LSL-speaking device/app) is an optional dependency: pip install "hypertools[lsl]".

This tutorial doesn’t require any real hardware: it starts a synthetic LSL outlet on a background thread (publishing multi-channel oscillating data), so the whole example runs anywhere, on any machine, with no external device attached – but hyp.io.lsl_stream() itself is exactly the same function you’d use with a real EEG amp, eye tracker, or any other LSL-speaking device.

[2]:
import threading
import time

import numpy as np
import pylsl

import hypertools as hyp

%matplotlib inline

A synthetic LSL outlet

We publish a 6-channel signal – each channel a sine wave at a slightly different frequency, plus noise – via a real pylsl.StreamOutlet running on a background thread. This is the exact pattern used by hypertools’ own test suite (tests/test_lsl_streaming.py) to exercise hyp.io.lsl_stream() without any real hardware.

[3]:
N_CHANNELS = 6
STREAM_NAME = 'HypertoolsTutorialStream'


def sample_for_index(i, n_channels=N_CHANNELS, dt=0.02):
    """Sample i: channel c oscillates at frequency (0.5 + 0.1*c) Hz, plus a
    little noise -- distinct-but-related channels, easy to tell apart in
    the resulting plot."""
    t = i * dt
    return [np.sin(2 * np.pi * (0.5 + 0.1 * c) * t) + 0.05 * np.random.randn()
            for c in range(n_channels)]


def start_synthetic_outlet(name=STREAM_NAME, stream_type='EEG',
                           n_channels=N_CHANNELS, rate=100.0,
                           push_interval=0.01):
    """Start a REAL pylsl.StreamOutlet on a background daemon thread,
    continuously pushing synthetic samples until `stop` is set."""
    info = pylsl.StreamInfo(name, stream_type, n_channels, rate, 'float32',
                            f'hypertools-tutorial-{name}')
    outlet = pylsl.StreamOutlet(info)
    stop = threading.Event()

    def _push():
        i = 0
        while not stop.is_set():
            outlet.push_sample(sample_for_index(i, n_channels))
            i += 1
            time.sleep(push_interval)

    thread = threading.Thread(target=_push, daemon=True)
    thread.start()
    return thread, stop


outlet_thread, stop_outlet = start_synthetic_outlet()
print(f'synthetic LSL outlet {STREAM_NAME!r} running on a background thread')

synthetic LSL outlet 'HypertoolsTutorialStream' running on a background thread
2026-07-17 04:26:26.854 (   3.786s) [          957C28]         api_config.cpp:126   INFO| Loaded default config
2026-07-17 04:26:26.854 (   3.786s) [          957C28]             common.cpp:78    INFO| git:64988c6a14b8dc3b3f270ece58eab4f480bfab43/branch:refs/tags/v1.17.7/build:Release/compiler:AppleClang-17.0.0.17000013/link:SHARED

Resolving the stream and plotting it live

hyp.io.lsl_stream(name=...) resolves the outlet above (by its LSL name property) and returns an iterator of per-sample vectors. hyp.plot treats it exactly like the synthetic generator in the streaming data tutorial: stream_init primes the plot with that many initial samples, then stream_chunk samples are pulled and appended per animation frame, up to stream_max total samples retained on screen.

[4]:
stream = hyp.io.lsl_stream(name=STREAM_NAME, timeout=5.0)

fig = hyp.plot(stream, stream_init=200, stream_chunk=20, stream_max=600,
               title='Live LSL stream (synthetic outlet)',
               save_path='lsl_stream.gif', show=False)
fig.stream_info['n_samples'], fig.stream_info['xform_data'][0].shape

[4]:
(600, (600, 3))

Live LSL stream animation

(The animation above is loaded from ``lsl_stream.gif``, written by the cell above; if your environment does not render it inline, open the file directly.)

Cleaning up the synthetic outlet

Stop the background-thread outlet once we’re done streaming from it (a real device’s outlet is managed by its own acquisition software, so this step is specific to this tutorial’s synthetic stand-in).

We close the stream iterator first (closing the underlying LSL inlet), and only then stop the outlet thread. In that order, the teardown is silent; if the outlet vanishes while the inlet is still open, liblsl logs an alarming (but harmless) ‘stream transmission broke off’ error while it tries to re-connect.

[5]:
stream.close()  # close our LSL inlet before tearing down the outlet
stop_outlet.set()
outlet_thread.join(timeout=5.0)
print('outlet thread stopped:', not outlet_thread.is_alive())
outlet thread stopped: True

Using a real device

Everything above works identically with a real LSL-speaking device or application (an EEG amplifier, eye tracker, motion capture rig, etc.) – just resolve it by type= instead of name= (or both):

# resolve any live EEG-type stream on the local network
stream = hyp.io.lsl_stream(type='EEG', timeout=10.0)
hyp.plot(stream, stream_init=200, stream_chunk=20)

See labstreaminglayer.org for the full list of LSL-speaking acquisition software, and the `hyp.io.lsl_stream API reference <../api.rst>`__ for the full parameter list.