flutter_rtc_link 0.1.2
flutter_rtc_link: ^0.1.2 copied to clipboard
Flutter plugin for real-time audio/video (WebRTC WHIP/WHEP + LiveKit) and IoT device management: publish, subscribe, live relay, robot teleoperation, provisioning and OTA.
Flutter RTC Link #
English | 简体中文
Real-time audio/video (WebRTC) and IoT device management in one Flutter plugin: publish and subscribe with two interchangeable engines, switch clarity per stream, relay a live stream, teleoperate a robot over a low-latency control link, provision / update / diagnose devices — plus a resilience layer built for overseas, cross-public-network links.
🔔 Upgrade recommended:
0.1.2improves Android build compatibility (lowerpermission_handlerconstraint so the default Flutter template builds without an AGP / Gradle upgrade) and fixes an example app UI overflow on the device page. All users should upgrade to the latest version (^0.1.2).
🌐 Documentation · 📦 View on pub.dev · 🔗 View on GitHub
Table of Contents #
Features #
- Dual Engines, One Abstraction — WHIP/WHEP over
flutter_webrtcfor self-hosted SRS / ZLMediaKit / MediaMTX, LiveKit overlivekit_clientfor interactive rooms. Swap the signaling object, keep every other line of code. - Publishing with Simulcast —
startCamera/startMic/startScreenShare,switchCameraviareplaceTrack(zero renegotiation, RFC 9725-friendly), andsetPublishClaritythat only caps the top layer. - Subscribing and Clarity Switching — WHEP playback, per-stream
setClarity, pure-DartVideoGridController(speaker / pip / grid layouts, max visible, global clarity), and honest failure when a stream has no variant to switch to. - Live Relay —
RoleTransfer.promote/handover(identity handover) andStreamRelay.toRtmp/cascade(server-side Egress or a secondary WHIP push). - Robot Teleoperation — joystick velocity, PTZ on its own channel, telemetry, intercom hookup, and a 500 ms watchdog that stops the machine on command timeout, refused sends or link loss.
- IoT Device Suite — multi-channel
DeviceProvisioning(BLE → DPP/QR → NFC → SoftAP → SmartConfig, auto-degrading),OtaUpdater,DeviceLogCollector(local cache + batch re-upload) andDiagnostics(stats + network probe + logs → JSON/Markdown). - Resilience for Overseas Links — supervised connect, jittered backoff reconnect with automatic re-subscription, TURN relay escalation, Wi-Fi ↔ cellular ICE restart, stutter / congestion / frozen-picture recovery.
- Pause / Resume Confirmation — every operation carries an epoch; pausing invalidates it, and resuming is a two-step ticket handshake, so no command captured before an interruption is ever replayed after it.
- Low-Latency Control Link —
ControlLinkRouter(data channel preferred, MQTT fallback, latency budget),ControlRttTracker,CommandRateLimiter: video degrades first, control commands survive. - Unified Permissions —
RtcPermissions.requestAll(...)covering the Android 13/14+ and iOS pitfalls; denials degrade features and emitRtcEvent.permissionDeniedinstead of crashing. - Native Where It Matters — Android DPP (Wi-Fi Easy Connect) platform channel plus a microphone foreground service; iOS Swift method-channel stub.
- Cross-Platform — Android and iOS.
Installation #
Pub.dev (Recommended) #
dependencies:
flutter_rtc_link: ^0.1.2
GitHub #
dependencies:
flutter_rtc_link:
git:
url: https://github.com/zero-labsco/flutter_rtc_link.git
ref: release/v0.1.2 # replace 0.1.2 with the version you need
Platform Setup #
| Platform | Requirement |
|---|---|
| Flutter | >= 3.35.0 |
| Dart SDK | >= 3.9.0 < 4.0.0 |
| Android | minSdk 24, compileSdk 36+ |
| iOS | 15.0+ |
Android permissions are merged by the plugin. iOS usage strings cannot be injected — add NSCameraUsageDescription, NSMicrophoneUsageDescription, NSBluetoothAlwaysUsageDescription, NSLocalNetworkUsageDescription, NFCReaderUsageDescription and UIBackgroundModes: audio to ios/Runner/Info.plist as needed.
Import #
import 'package:flutter_rtc_link/flutter_rtc_link.dart';
Usage #
Quick Start #
final engine = await RtcEngine.create(
signaling: WhipWhepSignaling(
whipUrl: 'https://srs.example.com/rtc/v1/whip/',
whepUrl: 'https://srs.example.com/rtc/v1/whep/',
iceServers: <IceServer>[
const IceServer(urls: 'stun:stun.example.com:3478'),
IceServer(
urls: 'turn:turn-ap.example.com:3478',
username: await token.turnUser(),
credential: await token.turnPass(),
),
],
),
);
final session = await engine.join(roomId: 'robot-42', role: RtcRole.robotOperator);
final remote = await session.subscribe(streamId: 'robot-42', quality: ClarityLevel.p720);
Publishing #
final publisher = session.localPublisher!;
await publisher.startCamera(facing: CameraFacing.rear, maxClarity: ClarityLevel.p720);
await publisher.startMic();
await publisher.switchCamera(facing: CameraFacing.front); // replaceTrack, no renegotiation
await publisher.setPublishClarity(ClarityLevel.p360); // caps the top layer only
Subscribing #
await remote.setClarity(ClarityLevel.p540); // this stream only
final grid = VideoGridController()..addStream(stream);
grid.setMain('robot-42');
RtcVideoView(subscriber: remote);
:::note
A single-layer stream has no software clarity switch. Either transcode server-side, or publish main/sub streams as separate stream ids (_sd / _hd / _fhd) — the plugin will switch between them (1–2 s blink) and reports claritySwitchUnsupported otherwise.
:::
Resilience (Overseas, Handover, Weak Network, Background) #
final monitor = CallbackNetworkMonitor(); // wired to connectivity_plus
final resilient = await ResilientSession.connect(
roomId: 'robot-42',
role: RtcRole.robotOperator,
factory: engine.join,
config: ResilientSessionConfig(
policy: const LinkPolicy(
connectTimeout: Duration(seconds: 12),
reconnect: ReconnectPolicy(maxAttempts: 8, relayAfterAttempts: 2),
restartIceOnHandover: true,
),
relay: RelayPlanner(servers: iceServers, preferredRegions: <String>['ap']),
monitor: monitor,
),
);
await resilient.setBackground(true); // pauses operations, drops the video track
await resilient.setBackground(false); // issues a resume ticket
await resilient.confirmResume(); // the operator confirms → new epoch
| Concern | Implementation |
|---|---|
| Connection establishment | LinkPolicy.connectTimeout + supervised retries |
| Relay / TURN | RelayPlanner per-region ordering, relayModeForAttempt → iceTransportPolicy: relay |
| Network switching | NetworkPathMonitor (kind + generation) → IceRestartable.restartIce |
| Weak network | MediaHealthMonitor: stutter / congestion / frozen → keyframe → downgrade → re-subscribe |
| Background | AppLifecycleGate → setBackground applies BackgroundPolicy |
| Reconnect | Jittered backoff, re-subscribes every remembered stream |
| Stale operations | PauseResumeGate epoch stamps + two-step resume ticket |
final gate = PauseResumeGate();
final queue = EpochOperationQueue<Velocity>(gate);
queue.submit(const Velocity(linear: 0.6)); // epoch 1
gate.pause(PauseReason.background); // epoch 2 — everything before is stale
final ticket = gate.requestResume(); // bound to epoch 3, expires
if (gate.confirmResume(ticket)) {
queue.take(); // null → dropped, NOT replayed
}
Robot Teleoperation #
final robot = RobotController(channel: mqttChannel, gate: gate);
await robot.connect(deviceId: 'robot-42');
await robot.move(velocityFromJoystick(dx: 0.4, dy: -0.8));
await robot.setPtz(pan: 30, tilt: -10);
await robot.pause(PauseReason.background); // stop + invalidate everything
final ticket = robot.requestResume(); // operator must confirm
await robot.confirmResume(ticket);
Devices #
await DeviceProvisioning().provision(
device,
wifi: WifiCredential(ssid: ssid, password: password),
);
OtaUpdater(channel: bleChannel).upgrade(
deviceId: device.id,
firmwareUrl: signedUrl,
expectedSha256: sha256,
).listen((p) => debugPrint('${p.percent} %'));
await DeviceLogCollector().flush();
final report = await Diagnostics.run(session: session);
Permissions #
final results = await RtcPermissions.requestAll(<RtcPermission>{
RtcPermission.camera,
RtcPermission.microphone,
RtcPermission.nearbyWifiDevices,
});
API Reference #
| Entry point | Purpose |
|---|---|
RtcEngine.create(signaling:) |
Creates the engine matching the signaling object |
RtcSession |
join / subscribe / unsubscribe / leave / events / stats |
LocalPublisher |
startCamera / startMic / startScreenShare / switchCamera / setPublishClarity |
RemoteSubscriber |
setClarity / clarity notifier / videoRenderer |
VideoGridController |
Pure-Dart layout and clarity orchestration |
RoleTransfer / StreamRelay |
Identity handover and media forwarding |
RobotController |
move / setPtz / stop / pause / requestResume / confirmResume |
ResilientSession |
connect / setBackground / confirmResume / resubscribe / restartIce |
PauseResumeGate / EpochOperationQueue<T> |
Epoch discipline and latest-wins buffering |
RelayPlanner / NetworkPathMonitor / MediaHealthMonitor |
Relay, handover and weak-network detection |
ControlLinkRouter / ControlRttTracker / CommandRateLimiter |
Low-latency control path |
DeviceProvisioning / OtaUpdater / DeviceLogCollector / Diagnostics |
IoT device suite |
RtcPermissions |
One-shot capability requests |
Example #
cd example && flutter run
Five tabs: link (publish/subscribe), robot (joystick + watchdog), device (provisioning/OTA/logs), diag (diagnostics report) and resil (resilience lab: pause/resume confirmation, handover, weak network, backoff).
Testing #
flutter analyze lib test example/lib
flutter test
Documentation #
The documentation site lives in website/ (Nextra 3 + Next.js 14) and is published to GitHub Pages: Installation, Quick Start, Publishing, Subscribing, Resilience, Robot Teleoperation, Devices and FAQ — each with a Chinese counterpart under /zh/.
cd website && npm ci && npm run dev
Contributing #
Contributions are welcome! Please read the Contributing Guidelines before submitting issues or pull requests.
License #
Copyright (c) 2026 Zero Labs Co. (AmisKwok). This project is licensed under the Mozilla Public License 2.0 (MPL-2.0) — see the LICENSE file for details. The copyright notice and additional statements (no warranty, no endorsement) live in NOTICE.
- Commercial use is allowed. Use, modification and closed-source distribution are permitted.
- Modified the plugin? The files you modified must be published in source form under MPL-2.0. Your own app does not need to be open-sourced.
- Used it unmodified? No source disclosure is required.
- No endorsement. "Zero Labs Co.", "flutter_rtc_link", the logo / mascot artwork, and the author's name (AmisKwok) may not be used to endorse or promote derived products, or to imply sponsorship or affiliation, without prior written permission.
- No warranty, no liability. The copyright holder provides no warranty and accepts no liability for any modified or derivative version; modified versions must be clearly marked as modified.
This plugin is provided "as is", without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.