orbit_voice 0.4.0
orbit_voice: ^0.4.0 copied to clipboard
Orbit Voice Flutter SDK — LiveKit connect, party-room audio (media stream, mic release, headset routing, VAD gate), no ovk_ in the client.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:orbit_voice/orbit_voice.dart';
/// Demo only — mint [OrbitVoiceSession] on your Edge; never ship ovk_ here.
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const OrbitVoiceExampleApp());
}
class OrbitVoiceExampleApp extends StatelessWidget {
const OrbitVoiceExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Orbit Voice Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0B3D2E)),
useMaterial3: true,
),
home: const _Home(),
);
}
}
class _Home extends StatefulWidget {
const _Home();
@override
State<_Home> createState() => _HomeState();
}
class _HomeState extends State<_Home> {
final _engine = OrbitEngine.instance;
final _url = TextEditingController();
final _token = TextEditingController();
final _room = TextEditingController(text: 'demo-room');
final _songUri = TextEditingController(
text: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
);
String _state = 'disconnected';
String _log = '';
bool _songPublishing = false;
@override
void initState() {
super.initState();
_engine.on(_onEvent);
// Bootstrap ADM/session once at startup (before first connect).
OrbitAudioBootstrap.ensureInitialized();
}
@override
void dispose() {
_engine.off(_onEvent);
_url.dispose();
_token.dispose();
_room.dispose();
_songUri.dispose();
super.dispose();
}
void _onEvent(String event, Map<String, dynamic> payload) {
setState(() {
if (event == OrbitVoiceEvents.connectionStateChanged) {
_state = payload['state']?.toString() ?? _state;
}
if (event == OrbitVoiceEvents.songPublishChanged) {
_songPublishing = payload['publishing'] == true;
}
_log = '$event $payload\n$_log';
});
}
Future<void> _connect() async {
final session = OrbitVoiceSession(
token: _token.text.trim(),
livekitUrl: _url.text.trim(),
livekitRoom: _room.text.trim(),
orbitRoomId: _room.text.trim(),
canPublish: true,
role: 'host',
onSeat: true,
);
try {
await _engine.loginRoom(session);
} catch (e) {
setState(() => _log = 'error: $e\n$_log');
}
}
Future<void> _toggleSong() async {
try {
if (_engine.isSongPublishing) {
await _engine.stopSong();
} else {
final raw = _songUri.text.trim();
if (raw.isEmpty) return;
final ok = await _engine.publishSong(Uri.parse(raw));
setState(() {
_songPublishing = _engine.isSongPublishing;
_log = 'publishSong → $ok\n$_log';
});
}
} catch (e) {
setState(() => _log = 'song error: $e\n$_log');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Orbit Voice')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('State: $_state', style: Theme.of(context).textTheme.titleMedium),
Text(
'Song: ${_songPublishing ? "publishing" : "off"}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
TextField(
controller: _url,
decoration: const InputDecoration(
labelText: 'LiveKit URL (wss://…)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _token,
decoration: const InputDecoration(
labelText: 'JWT from your Edge',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _room,
decoration: const InputDecoration(
labelText: 'Room id',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _songUri,
decoration: const InputDecoration(
labelText: 'Song file path or https URL',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton(onPressed: _connect, child: const Text('Connect')),
OutlinedButton(
onPressed: () => _engine.logoutRoom(),
child: const Text('Leave'),
),
OutlinedButton(
onPressed: () => _engine.setMicEnabled(true),
child: const Text('Mic on'),
),
OutlinedButton(
onPressed: () => _engine.setMicEnabled(false),
child: const Text('Mic off'),
),
OutlinedButton(
onPressed: () => _engine.enableSpeaker(true),
child: const Text('Speaker'),
),
FilledButton.tonal(
onPressed: _toggleSong,
child: Text(_songPublishing ? 'Stop song' : 'Publish song'),
),
],
),
const SizedBox(height: 16),
Text(_log, style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
],
),
);
}
}