A Flutter plugin for iOS that streams RTSP-over-TLS (rtsps://) video using Apple's
NWConnection (Network.framework) and VideoToolbox. Purpose-built for Bambu Lab printer
cameras, which use self-signed TLS certificates and require RTCP keepalives to sustain streams
beyond ~1.8 seconds.
- Full RTSP state machine: OPTIONS → DESCRIBE → SETUP → PLAY → TEARDOWN
- TCP interleaved RTP/RTCP over TLS (
rtsps://) — the only transport used - H.264 hardware decoding via
VTDecompressionSession - Zero-copy frame delivery to Flutter via
TextureRegistry - Automatic self-signed certificate acceptance
- RTCP Receiver Reports with real jitter, fraction lost, LSR/DLSR (RFC 3550)
- Adaptive RTCP rate: 4x faster RRs during detected stalls
- Stall-resistant jitter computation (clamped to prevent feedback loops)
- SPS/PPS change detection to avoid unnecessary decoder reinit
- FU-A fragmentation reassembly and STAP-A aggregation
- Interleaved RTP frame handling during RTSP handshake
- Single-frame JPEG snapshot capture
- Up to 8 concurrent streams
- Digest authentication with qop=auth support (RFC 2617)
- Comprehensive diagnostic logging via
os_log
┌─────────────────────────────────────────────────────┐
│ RtspStreamManager │
│ (Flutter MethodChannel bridge) │
├─────────────────────────────────────────────────────┤
│ RtspStreamSession │
│ (owns all components per stream) │
├──────────┬──────────┬───────────┬───────────────────┤
│ RtspState│ RtpDemux │ RtcpSender│ H264Decoder │
│ Machine │ er │ │ │
├──────────┤ │ ├───────────────────┤
│ RtspTrans│ │ │FlutterTextureOutput│
│ port(TLS)│ │ │ │
└──────────┘ │ └───────────────────┘
| File | Responsibility |
|---|---|
RtspStreamManager.swift |
Flutter MethodChannel/EventChannel bridge, stream lifecycle |
RtspStreamSession.swift |
Coordinates all components for one stream |
RtspStateMachine.swift |
RTSP handshake, Digest auth, TCP SETUP negotiation |
RtspTransport.swift |
TLS TCP connection via NWConnection |
RtpDemuxer.swift |
TCP interleaved frame extraction, RTP parsing, FU-A/STAP-A |
RtcpSender.swift |
Periodic Receiver Reports, SR processing, stall detection |
H264Decoder.swift |
VideoToolbox H.264 decoding, SPS/PPS management |
FlutterTextureOutput.swift |
CVPixelBuffer → Flutter Texture bridge |
SnapshotCapture.swift |
Single-frame RTSP capture → JPEG |
SdpParser.swift |
SDP parsing for video track, SPS/PPS extraction |
The plugin always negotiates TCP interleaved transport (RTP/AVP/TCP;unicast;interleaved=0-1)
for both live streams and single-frame snapshots. An earlier version attempted UDP transport
first (with TCP fallback) to work around suspected TCP/TLS backpressure on the H2C's ARM CPU,
but UDP was removed entirely after testing showed severe packet loss on WiFi (90+ FU-A sequence
gaps observed) — strictly worse than TCP for that printer, with no benefit on any other model.
This also matches how go2rtc/FFmpeg (used by Home Assistant's Bambu integration) connect to the
same printers.
The RTCP Receiver Report implementation follows RFC 3550 closely:
- Interarrival jitter computed per §6.4.1 / A.8, with a 50ms clamp to prevent stall-recovery bursts from inflating reported jitter (which could trigger server-side rate throttling)
- Jitter baseline resets after stall recovery so the first burst packet starts fresh
- Fraction lost and cumulative lost computed from extended sequence numbers
- LSR/DLSR computed from Sender Report NTP timestamps
- Immediate RR sent in response to each SR (some firmware gates encoder output on timely RR)
- Adaptive rate: switches from 1s to 250ms interval when no RTP packets arrive for >1.5s
- iOS 14.0+
- Swift 5.9+
Add to your pubspec.yaml:
dependencies:
flutter_rtsps_plugin:
git:
url: https://github.com/VIDGuide/flutter_rtsps_plugin.gitThen run:
flutter pub getimport 'package:flutter_rtsps_plugin/flutter_rtsps_plugin.dart';
import 'package:flutter/material.dart';
class CameraView extends StatefulWidget {
const CameraView({super.key});
@override
State<CameraView> createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView> {
final _controller = RtspStreamController();
int? _textureId;
int? _streamId;
@override
void initState() {
super.initState();
_startStream();
}
Future<void> _startStream() async {
try {
final textureId = await _controller.startStream(
'rtsps://bblp:<access_code>@<printer_ip>:322/streaming/live/1',
'bblp',
'<access_code>',
);
setState(() => _textureId = textureId);
} on RtspException catch (e) {
debugPrint('Stream error: ${e.code} — ${e.message}');
}
}
@override
void dispose() {
if (_streamId != null) _controller.stopStream(_streamId!);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final id = _textureId;
if (id == null) return const CircularProgressIndicator();
return AspectRatio(
aspectRatio: 16 / 9,
child: Texture(textureId: id),
);
}
}final controller = RtspStreamController();
try {
final jpeg = await controller.captureSnapshot(
'rtsps://bblp:<access_code>@<printer_ip>:322/streaming/live/1',
'bblp',
'<access_code>',
timeoutSeconds: 10,
);
// jpeg is a Uint8List — pass to Image.memory() or save to disk
} on RtspException catch (e) {
debugPrint('Snapshot failed: ${e.code} — ${e.message}');
} finally {
await controller.dispose();
}Bambu Lab printers expose their camera stream over TLS using a self-signed certificate.
This plugin automatically accepts self-signed certificates on the NWConnection transport
layer via sec_protocol_options_set_verify_block, so no additional configuration is required
in your app.
The TCP transport is configured for low-latency streaming:
noDelay = true— disables Nagle's algorithm so small RTCP packets are sent immediatelyenableKeepalive = truewith 5s idle — keeps the connection alive during stalls- 10-second connection timeout
All errors are thrown as RtspException with a typed code:
| Code | Meaning |
|---|---|
connectionFailed |
TCP/TLS connection could not be established |
authenticationFailed |
RTSP 401 — wrong username or password |
timeout |
No response within the timeout window |
noVideoTrack |
SDP contained no video media section |
decoderError |
VideoToolbox failed to initialise |
tooManyStreams |
8 concurrent streams already active |
The plugin uses os_log with the subsystem com.pandawatch.flutter_rtsps_plugin and
per-component categories (RtspStateMachine, RtpDemuxer, RtcpSender, H264Decoder,
etc.). Key log points:
- Full RTSP handshake headers (OPTIONS, DESCRIBE, SETUP, PLAY)
- SDP body line-by-line
- Periodic RTP statistics (packets/sec, bitrate, sequence numbers)
- Stall detection and recovery
- RTCP SR/RR exchange with jitter and loss metrics
If this plugin saves you some time, a coffee is always appreciated!
MIT — see LICENSE.