MoQ is under active development. APIs will change, but we keep backwards wire compatibility.

Skip to content

Dart and Flutter ​

pub.dev

The moq package on pub.dev wraps the generated moq_ffi bindings in Dart futures and streams. A Native Assets hook supplies the Rust core for Android (API 24+), iOS (16+), Linux, macOS, and Windows. Flutter web is not supported, since it can't load a native library.

Media frames use keyframe to mark a group start or a video keyframe. For audio, it is true only on the first frame of each group, even when every sample can be decoded independently.

bash
dart pub add moq        # or: flutter pub add moq
dart
import 'package:moq/moq.dart';

final moq = await Moq.connect('https://relay.example.com');

// Subscribe. The stream is live, so listen to it rather than awaiting its end.
moq.announcements(
  options: const AnnounceOptions(prefix: 'live/', filter: '*/camera'),
).listen((event) {
  if (event is AnnounceEventStart) {
    print(event.announce.prefix);
    print(event.announce.captures);
  } else if (event is AnnounceEventLive) {
    print('caught up; what follows is live');
  }
});
final broadcast = await moq.requestBroadcast('live/camera');
final catalog = await broadcast.subscribeCatalog();
print(await catalog.next());
dart
// Publish. bytes comes from your encoder or application source.
final mine = moq.createBroadcast('live/camera');
final track = mine.publishTrack(name: 'video', info: null);
final group = track.appendGroup();
group.writeFrame(frame: Frame(payload: bytes));
group.finish();
mine.announce(route: MoqRoute());
track.finish();
mine.close();

await moq.close();
dart
// Serve. Server.listen binds the socket and streams the sessions that arrive.
final server = await Server.listen(
  options: const ListenOptions(
    bind: '127.0.0.1:4443',
    tlsGenerate: ['localhost'],
  ),
);
final live = server.createBroadcast('live/camera');
live.announce(route: MoqRoute()); // unannounced broadcasts are invisible
await for (final request in server.requests()) {
  final session = await request.accept();
  print(session.epoch());
}

The three advertising operations: moq.createBroadcast(path) (or origin.createBroadcast) returns an unannounced producer, invisible to everyone; broadcast.announce(route:) / broadcast.unannounce() own that exact-path advertisement, and broadcast.close() ends the broadcast for good (a second call is a no-op); origin.dynamic_(prefix:, route:) claims prefix and every path beneath it ('' for everything; Dart spells the origin method dynamic_ because dynamic is reserved). Hold the returned handle while the claim should stay advertised, and reject the requests you will not serve. A route is a capability, not an inventory. announcements(options:) takes a literal prefix plus an optional relative pattern and yields AnnounceEvents: AnnounceEventStart, AnnounceEventUpdate, or AnnounceEventEnd carrying an Announce, whose prefix stays origin-relative and whose captures reports the wildcard matches, or AnnounceEventLive once every route live at subscribe time has been delivered. Paths with a .-prefixed segment below the prefix are hidden unless hidden: true.

Sessions reconnect with backoff when the transport drops and re-announce local broadcasts. Moq.connect and Server.listen take a ConnectOptions / ListenOptions struct, like Rust: reconnect: false makes the dial one-shot and backoff: re-paces the retries. moq.epoch counts the connections, 1 on the first, pairing with session.status() to log each reconnect; maxStreams raises the peer's inbound stream cap for a subscriber to many tracks.

The WebSocket fallback races QUIC after a 200 ms head start. websocketEnabled: false turns it off for a QUIC-only relay, and a websocketDelay Duration changes the head start.

Types are spelled without the Moq prefix (Session, BroadcastProducer, Backoff); the generated names stay valid, since these are aliases rather than wrappers. Container, Route, and the exceptions keep theirs, because Container and Route are Flutter's. Microsecond fields read back as a Duration: stats.rtt, backoff.initial, frame.timestamp.

Cancelling a stream releases the native cursor. The package re-exports moq_ffi, so the full generated API is available without a second import. Generated configuration setters throw if a connect, listen, or accept is in flight, or after cancel(). Incoming requests report a MoqTransport enum. ProtocolMoqException carries a MoqProtocolException as details (scope, verbatim code, kind) when the peer sent a session or stream code. An exception's toString() is the Rust error message.

moq.bandwidth() divides the connection's send estimate; reserve a share for an app-owned encoder so several publishers on one session split the uplink instead of each targeting the whole thing.

Unlike the other bindings, the published Dart binaries carry no codecs: catalog and container types are there, so already-encoded frames flow through MoqMediaProducer/MoqMediaConsumer, but encoding is up to package:camera, platform channels, or another codec package.

MediaProducer.flush(timestampUs: ...) records the handoff of a locally encoded frame on the broadcast media clock. Call it after writeFrame only for live encoder output; file, pipe, and network imports stay clock-free. MediaProducer aliases the generated FFI object, so its method is available directly.

Call media.discontinuity() when the source seeks, pauses, or changes its time base. It publishes a timeline marker and restarts handoff measurement without lowering advertised jitter. Resume with timestamps that continue forward on the broadcast media clock; this does not permit timestamp rewinds. On a video track, resume with a keyframe: a delta frame before it fails.

Connection stats ​

session.stats() returns a ConnectionStats snapshot. Each field is null when the transport backend does not report it (native QUIC reports all of them; browser WebTransport reports few or none) or before it is available, which is not the same as zero. rttUs is microseconds; the rtt extension reads it as a Duration.

FieldUnitMeaning
rttUsmicrosecondsSmoothed round-trip time.
estimatedSendRateBpsbits per secondSend bandwidth from the congestion controller.
estimatedRecvRateBpsbits per secondReceive bandwidth from MoQ PROBE.
bytesSentbytesTotal sent, including retransmissions and overhead.
bytesReceivedbytesTotal received, including duplicates and overhead.
bytesLostbytesTotal lost, detected via retransmission or acknowledgement.
packetsSentdatagramsTotal datagrams sent.
packetsReceiveddatagramsTotal datagrams received.
packetsLostdatagramsTotal datagrams detected as lost.

Raw track publisher metadata has an optional maximum age. Omitting it imposes no publisher age limit; zero keeps the live edge. Local cache limits still apply, and media imports explicitly retain 30 seconds. See publisher retention.

Await session.shutdown() or moq.close() to drain finished tracks before disconnecting. These futures fail if delivery has not completed within one second. session.cancel(code: 0) remains immediate. Finish or abort live tracks before shutdown. IETF media streams are not drained yet.

Licensed under MIT or Apache-2.0