peer_rtc 1.1.0
peer_rtc: ^1.1.0 copied to clipboard
P2P WebRTC library for Dart/Flutter with mesh networking and binary optimization for real-time games.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:peer_rtc/peer_rtc.dart';
import 'game/star_pong.dart';
import 'widgets/widgets.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final jsonString = await rootBundle.loadString('assets/peer_option.json');
final json = jsonDecode(jsonString) as Map<String, dynamic>;
runApp(App(json: json));
}
class App extends StatelessWidget {
const App({super.key, required this.json});
final Map<String, dynamic> json;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PeerRTC',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: CyberTheme.bgDark,
),
// home: PeerRTCPage(json: json),
home: const StarPong(),
);
}
}
class PeerRTCPage extends StatefulWidget {
const PeerRTCPage({super.key, required this.json});
final Map<String, dynamic> json;
@override
State<PeerRTCPage> createState() => _PeerRTCPageState();
}
class _PeerRTCPageState extends State<PeerRTCPage> {
// ══════════════════════════════════════════════════════════════
// STATE
// ══════════════════════════════════════════════════════════════
Peer? _peer;
String _status = 'INIT';
String _peerId = '';
bool _isChannelOpen = false;
DataConnection? _dataConnection;
int _retryCount = 0;
final List<String> _logs = [];
final _remoteIdController = TextEditingController();
final _messageController = TextEditingController();
final _scrollController = ScrollController();
// ══════════════════════════════════════════════════════════════
// LIFECYCLE
// ══════════════════════════════════════════════════════════════
@override
void initState() {
super.initState();
_connect();
}
@override
void dispose() {
_peer?.dispose();
_remoteIdController.dispose();
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
// ══════════════════════════════════════════════════════════════
// PEER CONNECTION
// ══════════════════════════════════════════════════════════════
void _connect() {
_log('Connecting to signaling server...');
setState(() => _status = 'CONNECTING');
_peer = Peer(options: PeerOptions.fromJson(widget.json));
_peer!.onOpen.listen((id) {
setState(() {
_peerId = id ?? '';
_status = 'READY';
});
_log('Connected! ID: $id');
});
_peer!.onError.listen((e) => _log('ERROR: $e'));
_peer!.onDisconnected.listen((_) {
setState(() => _status = 'OFFLINE');
_log('Disconnected');
});
_peer!.onReconnecting.listen((n) {
setState(() => _status = 'RETRY');
_log('Reconnecting #$n...');
});
_peer!.onReconnected.listen((_) {
setState(() => _status = 'READY');
_log('Reconnected!');
});
_peer!.onConnection.listen((conn) {
_log('Incoming: ${conn.peer}');
_setupDataConnection(conn);
});
}
// ══════════════════════════════════════════════════════════════
// DATA CONNECTION
// ══════════════════════════════════════════════════════════════
void _setupDataConnection(DataConnection conn) {
_dataConnection = conn;
setState(() => _isChannelOpen = false);
conn.onOpen.listen((_) {
_log('Channel OPEN with ${conn.peer}');
setState(() {
_isChannelOpen = true;
_retryCount = 0; // Reset retry count
});
});
conn.onData.listen((data) => _log('< ${data['message'] ?? data}'));
conn.onError.listen((e) => _log('Channel error: $e'));
conn.onClose.listen((_) {
_log('Channel CLOSED');
setState(() {
_dataConnection = null;
_isChannelOpen = false;
});
// Auto-reconnect logic
if (_peer != null && !_peer!.destroyed) {
final remoteId = conn.peer;
// Exponential Backoff: 2s, 4s, 8s, 16s... (cap at 30s)
final delay = Duration(seconds: (2 * (1 << _retryCount)).clamp(2, 30));
_log(
'Lost connection. Retry #${_retryCount + 1} in ${delay.inSeconds}s...',
);
Future.delayed(delay, () {
if (_peer?.destroyed == false && _dataConnection == null) {
_retryCount++;
_connectToPeerById(remoteId);
}
});
}
});
}
void _connectToPeer() {
final id = _remoteIdController.text.trim();
if (id.isEmpty || _peer == null) return;
_log('Connecting to $id...');
_setupDataConnection(_peer!.connect(id));
_remoteIdController.clear();
}
void _connectToPeerById(String id) {
_log('Re-connecting to $id...');
_setupDataConnection(_peer!.connect(id));
}
void _sendMessage() {
final msg = _messageController.text.trim();
if (msg.isEmpty || !_isChannelOpen) return;
_dataConnection!.send({'message': msg});
_log('> $msg');
_messageController.clear();
}
// ══════════════════════════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════════════════════════
void _log(String msg) {
final ts = DateTime.now().toString().substring(11, 19);
setState(() => _logs.add('[$ts] $msg'));
Future.delayed(const Duration(milliseconds: 50), () {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
void _copyId() {
if (_peerId.isEmpty) return;
Clipboard.setData(ClipboardData(text: _peerId));
_log('Copied to clipboard!');
}
// ══════════════════════════════════════════════════════════════
// UI - Layout: Header | Logs | Input
// ══════════════════════════════════════════════════════════════
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Header: ME | PARTNER | STATUS
HeaderMetrics(
peerId: _peerId,
partnerId: _dataConnection?.peer,
status: _status,
isChannelOpen: _isChannelOpen,
onCopyId: _copyId,
),
// Logs
Expanded(
child: LogsSection(
logs: _logs,
scrollController: _scrollController,
),
),
// Input at bottom
TerminalInput(
isChannelOpen: _isChannelOpen,
isConnected: _peer != null,
remoteIdController: _remoteIdController,
messageController: _messageController,
onConnect: _connectToPeer,
onSend: _sendMessage,
onReconnect: _connect,
),
],
),
),
);
}
}