@mindfield/capacitor-lsl
v1.0.0
Published
Capacitor 7 plugin for Lab Streaming Layer (LSL) — stream biosignal data from mobile devices to LabRecorder or any LSL-compatible receiver
Maintainers
Readme
@mindfield/capacitor-lsl
Capacitor 7 plugin for Lab Streaming Layer (LSL) — stream biosignal data from mobile devices to LabRecorder or any LSL-compatible receiver.
Installation
npm install @mindfield/capacitor-lsl
npx cap syncWarning: Do NOT install
cordova-plugin-lsland@mindfield/capacitor-lslin the same project. They share the same native libraries and will cause duplicate class conflicts.
Platforms
| Platform | Status | Notes | |----------|--------|-------| | Android | Supported | ARM64, ARMv7, x86_64 | | iOS | Supported | ARM64 (device + simulator) | | Web | Not supported | LSL requires native UDP multicast |
Quick Start
import { LSL } from '@mindfield/capacitor-lsl';
// Create an outlet
const { outletId } = await LSL.createOutlet({
name: 'eSense_EDA',
type: 'EDA',
channelCount: 1,
sampleRate: 5.0,
channelFormat: 'float32',
sourceId: 'esense-eda-001',
metadata: {
manufacturer: 'Mindfield Biosystems',
device: 'eSense EDA',
channels: [{ label: 'EDA', unit: 'microsiemens', type: 'EDA' }],
},
});
// Show IP for KnownPeers config
const { ip } = await LSL.getDeviceIP();
console.log(`Add to lsl_api.cfg: KnownPeers = {${ip}}`);
// Push samples
await LSL.pushSample({
outletId,
data: [3.14],
});
// Push chunks (more efficient for high sample rates)
await LSL.pushChunk({
outletId,
data: [[3.14], [3.15], [3.16], [3.17], [3.18]],
});
// Check for consumers (LabRecorder etc.)
const { hasConsumers } = await LSL.hasConsumers({ outletId });
// Wait for a consumer to connect (blocking, with timeout)
const result = await LSL.waitForConsumers({ outletId, timeout: 30.0 });
// Cleanup
await LSL.destroyOutlet({ outletId });
// or: await LSL.destroyAllOutlets();API Reference
Outlet Operations
| Method | Description |
|--------|-------------|
| createOutlet(options) | Create a new LSL outlet. Returns { outletId }. |
| pushSample(options) | Push a single sample to an outlet. |
| pushChunk(options) | Push a chunk of samples (single JNI/C call, more efficient). |
| hasConsumers(options) | Check if any consumer is connected. Returns { hasConsumers }. |
| waitForConsumers(options) | Wait for a consumer (blocking). Returns { hasConsumers }. |
| destroyOutlet(options) | Destroy a specific outlet. |
| destroyAllOutlets() | Destroy all outlets and release resources. |
Utility Operations
| Method | Description |
|--------|-------------|
| getLocalClock() | Get LSL monotonic clock time. Returns { timestamp }. |
| getLibraryVersion() | Get liblsl version. Returns { version }. |
| getProtocolVersion() | Get LSL protocol version. Returns { version }. |
| getDeviceIP() | Get Wi-Fi IP address. Returns { ip }. |
Types
type ChannelFormat = 'float32' | 'double64' | 'int32' | 'int16' | 'int8' | 'string';
interface CreateOutletOptions {
name: string;
type: string;
channelCount: number;
sampleRate: number;
channelFormat: ChannelFormat;
sourceId?: string;
metadata?: StreamMetadata;
}
interface StreamMetadata {
manufacturer?: string;
device?: string;
channels?: ChannelInfo[];
}
interface ChannelInfo {
label: string;
unit: string;
type: string;
}Receiving LSL Streams on PC
- Install LabRecorder
- Get the device IP:
const { ip } = await LSL.getDeviceIP() - Add to
lsl_api.cfg:[lab] KnownPeers = {192.168.1.100} - Start LabRecorder — the stream should appear in the stream list
Migration from cordova-plugin-lsl
| Cordova | Capacitor |
|---------|-----------|
| LSL.createOutlet(options) → string | LSL.createOutlet(options) → { outletId: string } |
| LSL.pushSample(outletId, data, ts) | LSL.pushSample({ outletId, data, timestamp }) |
| LSL.pushChunk(outletId, data) | LSL.pushChunk({ outletId, data }) |
| LSL.hasConsumers(outletId) → boolean | LSL.hasConsumers({ outletId }) → { hasConsumers } |
| LSL.waitForConsumers(outletId, timeout) | LSL.waitForConsumers({ outletId, timeout }) |
| LSL.destroyOutlet(outletId) | LSL.destroyOutlet({ outletId }) |
| LSL.getLocalClock() → number | LSL.getLocalClock() → { timestamp } |
| LSL.getLibraryVersion() → string | LSL.getLibraryVersion() → { version } |
| LSL.getDeviceIP() → string | LSL.getDeviceIP() → { ip } |
Key differences:
- All parameters are passed as options objects (Capacitor convention)
- All return values are wrapped in objects
- Import from
@mindfield/capacitor-lslinstead of globalLSLvariable
Architecture
TypeScript API (definitions.ts)
│
├── Android: Kotlin Plugin (LslPlugin.kt)
│ └── JNI Shim (de.mindfield.cordova.lsl.LSLPlugin.java)
│ └── liblsl_jni.so → liblsl.so (pre-built C library)
│
└── iOS: Objective-C Plugin (LslPlugin.m)
└── liblsl.xcframework (pre-built C library, direct C API calls)License
MIT — Copyright (c) 2026 Mindfield Biosystems Ltd.
