Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/connection-check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minor type="added" "Add ConnectionCheck utility for diagnosing connection issues (port of the client-sdk-js connection helper)"
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,38 @@ These controls are accessible on the `RemoteTrackPublication` object.

For more info, see [Subscribing to tracks](https://docs.livekit.io/home/client/tracks/subscribe/).

### Diagnosing connection issues

`ConnectionCheck` runs a set of connection diagnostics against a LiveKit server — the same checks that power [livekit.io/connection-test](https://livekit.io/connection-test). Run it proactively at app start, or reactively when `room.connect()` fails, to determine why a connection cannot be established (firewall, VPN, blocked TURN, etc.).

```dart
final connectionCheck = ConnectionCheck(url, token);
final listener = connectionCheck.createListener();
listener.on<ConnectionCheckUpdateEvent>((event) {
print('${event.info.name}: ${event.info.status.name}');
for (final log in event.info.logs) {
print(' $log');
}
});

// the recommended minimum set of checks
await connectionCheck.checkWebsocket();
await connectionCheck.checkWebRTC();
await connectionCheck.checkTURN();

// additional checks
await connectionCheck.checkReconnect();
await connectionCheck.checkPublishAudio(); // requires microphone permission
await connectionCheck.checkPublishVideo(); // requires camera permission
await connectionCheck.checkConnectionProtocol();
await connectionCheck.checkCloudRegion(); // LiveKit Cloud only

print('all checks passed: ${connectionCheck.isSuccess}');

await listener.dispose();
await connectionCheck.dispose();
```

## Getting help / Contributing

Please join us on [Slack](https://livekit.io/join-slack) to get help from our devs / community members. We welcome your contributions(PRs) and details can be discussed there.
Expand Down
19 changes: 19 additions & 0 deletions example/lib/pages/connect.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_example/pages/connection_check.dart';
import 'package:livekit_example/pages/prejoin.dart';
import 'package:livekit_example/widgets/text_field.dart';
import 'package:shared_preferences/shared_preferences.dart';
Expand Down Expand Up @@ -160,6 +161,20 @@ class _ConnectPageState extends State<ConnectPage> {
}
}

Future<void> _connectionCheck(BuildContext ctx) async {
// Save URL and Token for convenience
await _writePrefs();
if (!ctx.mounted) return;
await Navigator.push<void>(
ctx,
MaterialPageRoute(
builder: (_) => ConnectionCheckPage(
url: _uriCtrl.text,
token: _tokenCtrl.text,
)),
);
}

void _setSimulcast(bool? value) async {
if (value == null || _simulcast == value) return;
setState(() {
Expand Down Expand Up @@ -356,6 +371,10 @@ class _ConnectPageState extends State<ConnectPage> {
],
),
),
TextButton(
onPressed: _busy ? null : () => unawaited(_connectionCheck(context)),
child: const Text('Connection Check'),
),
],
),
),
Expand Down
187 changes: 187 additions & 0 deletions example/lib/pages/connection_check.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import 'dart:async';

import 'package:flutter/material.dart';

import 'package:livekit_client/livekit_client.dart';

class ConnectionCheckPage extends StatefulWidget {
//
const ConnectionCheckPage({
required this.url,
required this.token,
super.key,
});

final String url;
final String token;

@override
State<StatefulWidget> createState() => _ConnectionCheckPageState();
}

class _ConnectionCheckPageState extends State<ConnectionCheckPage> {
//
ConnectionCheck? _connectionCheck;
EventsListener<ConnectionCheckEvent>? _listener;
final Map<int, CheckInfo> _results = {};
bool _running = false;

@override
void dispose() {
unawaited(_cleanUp());
super.dispose();
}

Future<void> _cleanUp() async {
await _listener?.dispose();
_listener = null;
await _connectionCheck?.dispose();
_connectionCheck = null;
}

Future<void> _runChecks() async {
await _cleanUp();

final connectionCheck = ConnectionCheck(widget.url, widget.token);
final listener = connectionCheck.createListener();
listener.on<ConnectionCheckUpdateEvent>((event) {
if (!mounted) return;
setState(() {
_results[event.checkId] = event.info;
});
});

setState(() {
_connectionCheck = connectionCheck;
_listener = listener;
_results.clear();
_running = true;
});

final checks = <Future<CheckInfo> Function()>[
connectionCheck.checkWebsocket,
connectionCheck.checkWebRTC,
connectionCheck.checkTURN,
connectionCheck.checkReconnect,
connectionCheck.checkPublishAudio,
connectionCheck.checkPublishVideo,
connectionCheck.checkConnectionProtocol,
connectionCheck.checkCloudRegion,
];
try {
for (final check in checks) {
// stop starting new checks once the page is gone
if (!mounted) break;
await check();
}
} catch (error) {
print('Connection check error: $error');
} finally {
if (mounted) {
setState(() {
_running = false;
});
}
}
}

Widget _iconFor(CheckStatus status) {
switch (status) {
case CheckStatus.idle:
return const Icon(Icons.radio_button_unchecked);
case CheckStatus.running:
return const SizedBox(
width: 24,
height: 24,
child: Padding(
padding: EdgeInsets.all(2),
child: CircularProgressIndicator(strokeWidth: 2),
),
);
case CheckStatus.skipped:
return const Icon(Icons.skip_next, color: Colors.grey);
case CheckStatus.success:
return const Icon(Icons.check_circle, color: Colors.green);
case CheckStatus.failed:
return const Icon(Icons.error, color: Colors.red);
}
}

Color _colorFor(CheckLogLevel level) {
switch (level) {
case CheckLogLevel.info:
return Colors.grey;
case CheckLogLevel.warning:
return Colors.orange;
case CheckLogLevel.error:
return Colors.red;
}
}

@override
Widget build(BuildContext context) {
final results = _results.entries.toList()..sort((a, b) => a.key.compareTo(b.key));
return Scaffold(
appBar: AppBar(
title: const Text('Connection Check'),
),
body: Column(
children: [
Expanded(
child: ListView(
children: [
for (final entry in results)
ExpansionTile(
initiallyExpanded: true,
leading: _iconFor(entry.value.status),
title: Text(entry.value.name),
subtitle: Text(entry.value.description),
children: [
for (final log in entry.value.logs)
ListTile(
dense: true,
visualDensity: VisualDensity.compact,
title: Text(
log.message,
style: TextStyle(
fontSize: 13,
color: _colorFor(log.level),
),
),
),
],
),
],
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: ElevatedButton(
onPressed: _running ? null : () => unawaited(_runChecks()),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_running)
const Padding(
padding: EdgeInsets.only(right: 10),
child: SizedBox(
height: 15,
width: 15,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
),
const Text('Run checks'),
],
),
),
),
),
],
),
);
}
}
5 changes: 5 additions & 0 deletions lib/livekit_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

import 'src/types/rpc.dart' show kRpcVersion;

export 'src/connection_check/checks/checker.dart';
export 'src/connection_check/checks/cloud_region.dart' show RegionStats;
export 'src/connection_check/checks/connection_protocol.dart' show ProtocolStats;
export 'src/connection_check/connection_check.dart';
export 'src/connection_check/events.dart';
export 'src/constants.dart';
export 'src/core/room.dart';
export 'src/core/room_preconnect.dart';
Expand Down
Loading
Loading