Streaming data between two devices
This guide sends a stream of samples from one device and receives it on
another, using liblsl, the Dart interface to Lab
Streaming Layer (LSL). At the end, two programs exchange timestamped samples
over the local network and the receiver reports how long each sample took to
arrive.
Requirements
Both devices need the Dart SDK (3.11 or later) and a C++ compiler, because
liblsl is compiled from source the first time a program that uses it is
run. On Debian-based Linux the compiler is installed with
sudo apt install build-essential clang llvm.
The devices must be on the same local network. LSL finds streams with multicast UDP, which some managed networks and firewalls block; the section When the stream is not found describes what to do in that case.
The examples
The two programs are
send.dart and
receive.dart. To obtain them,
clone the repository with its submodules on each device:
git clone --recurse-submodules https://github.com/NexusDynamic/liblsl.dart.git
cd liblsl.dart
dart pub get
cd packages/liblsl
Procedure
- On the first device, start the sender. It sends a two-channel stream at 10 Hz for 60 seconds.
dart run example/send.dart
- On the second device, start the receiver. It looks for the stream for up to 10 seconds and then prints 50 samples.
dart run example/receive.dart
Both programs can also be run in two terminals on one machine, which is a useful first check because it does not depend on the network.
The receiver prints one line per sample. The following output is from a run with both programs on one machine:
Clock offset to the sender: -0.052 ms
sample 18: -0.588 sent at 854594.2343 s latency 1.11 ms
sample 19: -0.309 sent at 854594.3335 s latency 0.62 ms
sample 20: -0.000 sent at 854594.4339 s latency 0.83 ms
How the sender works
A stream is described by a stream info: its name and type, the number of channels, the sampling rate, the data format, and a source id that identifies the sender. An outlet makes the stream available on the network.
final info = await LSL.createStreamInfo(
streamName: 'GuideStream',
streamType: LSLContentType.custom('Example'),
channelCount: 2,
sampleRate: 10.0,
channelFormat: LSLChannelFormat.double64,
sourceId: 'guide-sender-1',
);
final outlet = await LSL.createOutlet(streamInfo: info);
Each call to pushSample sends one sample, which is a list with one value
per channel. LSL stamps the sample with the sender's clock at the moment it
is pushed.
outlet.pushSample([count.toDouble(), sin(2 * pi * 0.5 * count / 10.0)]);
How the receiver works
The receiver resolves the stream by name and opens an inlet on it.
final streams = await LSL.resolveStreamsByProperty(
property: LSLStreamProperty.name,
value: 'GuideStream',
waitTime: 10.0,
minStreamCount: 1,
);
final inlet = await LSL.createInlet<double>(streamInfo: streams.first);
Every sample arrives with a timestamp, which is a reading of the sender's clock. The clocks of two devices have unrelated origins, so the timestamp has to be mapped onto the receiver's clock before it can be compared with local events. LSL estimates the offset between the two clocks, and the receiver adds it to the timestamp.
final offset = await inlet.getTimeCorrection(timeout: 5.0);
final sample = await inlet.pullSample(timeout: 2.0);
final sentAt = sample.timestamp + offset;
final latency = (LSL.localClock() - sentAt) * 1000;
The latency printed by the example is the time between the push on the sender and the pull on the receiver. The example measures the offset once. Clocks drift apart by tens of microseconds per second, so a program that runs for longer than a few minutes should measure the offset again at intervals.
When the stream is not found
If the receiver reports that no stream was found although the sender is running, multicast traffic is probably not reaching the receiver. The firewall on each device must allow the programs to use the local network. On a network that blocks multicast, the addresses of the devices can be given to LSL directly, before any other LSL call and on every device:
LSL.setConfigContent(LSLApiConfig(knownPeers: ['10.0.0.100', '10.0.0.101']));
Android and iOS applications need additional permissions for local network
access. They are listed in the
liblsl README.
Viewing the stream
While the sender is running, the stream can be inspected in LSL Viewer on any desktop or Android device on the same network, under LSL > View streams….
Use in a project
liblsl is added to a Dart or Flutter project with dart pub add liblsl.
The same code runs on Windows, macOS, Linux, Android and iOS.
Further reading
A coordinated experiment builds on streams to run one procedure on several devices. Sharing LSL streams over the network covers web browsers and devices on different networks.