nativemind_call 0.1.1
nativemind_call: ^0.1.1 copied to clipboard
Native media layer of NativeMind calls on Andrey Bekhtold's handmade WebRTC code (Android PeerConnectionClient, iOS WebRTCManager, the WebRTCClientType configuration, Debug.swift): video through Flutt [...]
example/lib/main.dart
// Harness of nativemind_call: Andrey Bekhtold's loopback call (CallFragment.setupWebRTC: peer A sends the camera,
// peer B receives) in one process, shown through NmNativeCallPort.videoTile — the local tile is A's texture, the
// remote tile is the video B received over WebRTC. `--dart-define=NM_AUTOSTART=true` starts the call at launch.
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
import 'package:flutter/material.dart';
import 'package:nativemind_call/nativemind_call.dart';
import 'package:nativemind_deals/nativemind_deals.dart';
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
void main() => runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: LoopbackPage()));
/// The loopback screen.
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
class LoopbackPage extends StatefulWidget {
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
const LoopbackPage({super.key, this.call});
/// Engine (tests inject one over a fake host).
final NmNativeCall? call;
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
@override
State<LoopbackPage> createState() => _LoopbackPageState();
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
class _LoopbackPageState extends State<LoopbackPage> {
NmLoopbackCall? _loopback;
NmNativeCallPort? _port;
bool _audio = false;
String _status = 'idle';
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
NmNativeCall get _call => widget.call ?? NmNativeCall.instance;
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
@override
void initState() {
super.initState();
if (const bool.fromEnvironment('NM_AUTOSTART')) WidgetsBinding.instance.addPostFrameCallback((_) => _start());
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
Future<void> _start() async {
if (_loopback != null) return;
setState(() => _status = 'permissions…');
final granted = await _call.requestPermissions();
if (!granted) {
if (mounted) setState(() => _status = 'camera / microphone permission denied');
return;
}
final loopback = NmLoopbackCall(call: _call);
await loopback.setupWebRTC();
// Loudspeaker loop: A's microphone would play on B — start muted.
await loopback.peerA.setAudioEnabled(_audio);
loopback.peerB.iceConnectionState.addListener(() {
if (mounted) setState(() => _status = 'B: ${loopback.peerB.iceConnectionState.value}');
});
if (!mounted) return;
setState(() {
_loopback = loopback;
_port = NmNativeCallPort(
client: loopback.peerA,
remoteClient: loopback.peerB,
activeCall: const NmCallInfo(clientName: 'A', expertName: 'B'),
onAccept: () async {},
onHangUp: _stop,
call: _call,
);
_status = 'offer sent';
});
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
Future<void> _stop() async {
final loopback = _loopback;
if (loopback == null) return;
setState(() {
_loopback = null;
_port = null;
_status = 'idle';
});
await loopback.dispose();
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
Future<void> _toggleAudio() async {
setState(() => _audio = !_audio);
await _loopback?.peerA.setAudioEnabled(_audio);
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
@override
void dispose() {
_loopback?.dispose();
super.dispose();
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
Widget _tile(String label, NmCallTile tile) {
final port = _port;
return Expanded(
child: Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.black12, borderRadius: BorderRadius.circular(16)),
clipBehavior: Clip.antiAlias,
child: Stack(
fit: StackFit.expand,
children: [
if (port != null) port.videoTile(context, tile) else const NmNoVideoWidget(),
Positioned(
left: 12,
top: 8,
child: Text(
label,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, shadows: [Shadow(blurRadius: 4)]),
),
),
],
),
),
);
}
// written by Claude Code (generated by Claude Code), 2026-10-13; SDD: flows/sdd-widgets-handmade
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('nativemind_call · loopback')),
body: SafeArea(
child: Column(
children: [
_tile('A — camera (local tile)', NmCallTile.local),
_tile('B — received over WebRTC (remote tile)', NmCallTile.remote),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(_status, key: const Key('status')),
),
Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(key: const Key('start'), onPressed: _loopback == null ? _start : null, child: const Text('Start')),
OutlinedButton(onPressed: _loopback == null ? null : _toggleAudio, child: Text(_audio ? 'Mute mic' : 'Unmute mic')),
OutlinedButton(key: const Key('stop'), onPressed: _loopback == null ? null : () => _port?.hangUp(), child: const Text('Stop')),
],
),
),
],
),
),
);
}