meta_wearables_flutter 0.1.0
meta_wearables_flutter: ^0.1.0 copied to clipboard
A Flutter plugin for Meta Wearables Device Access Toolkit (MWDAT).
// example/lib/main.dart
//
// Example app demonstrating the meta_wearables_flutter plugin.
// Shows registration flow, device listing, camera streaming, and photo capture.
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meta_wearables_flutter/meta_wearables_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ── Step 1: Initialize the SDK ────────────────────────────────────────────
await MetaWearables.initialize();
runApp(const MetaGlassesExampleApp());
}
class MetaGlassesExampleApp extends StatelessWidget {
const MetaGlassesExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Meta Wearables Example',
theme: ThemeData.dark(useMaterial3: true),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
RegistrationState _registrationState = RegistrationState.unregistered;
List<WearableDevice> _devices = [];
CameraStream? _cameraStream;
StreamState _streamState = StreamState.stopped;
Uint8List? _currentFrame;
PhotoData? _lastPhoto;
String? _error;
String? _displaySessionId;
String? _displayId;
@override
void initState() {
super.initState();
_listenToRegistration();
_listenToDevices();
}
void _listenToRegistration() {
// ── Step 2: Observe registration state ──────────────────────────────────
MetaWearables.registrationState.listen((state) {
setState(() => _registrationState = state);
});
}
void _listenToDevices() {
MetaWearables.devices.listen((devices) {
setState(() => _devices = devices);
});
}
Future<void> _register() async {
try {
// ── Step 3: Launch Meta AI registration ─────────────────────────────
await MetaWearables.startRegistration();
} on WearablesError catch (e) {
setState(() => _error = e.description);
}
}
Future<void> _startStreaming() async {
try {
// ── Step 4: Check + request camera permission ────────────────────────
var permission = await MetaWearables.checkCameraPermission();
if (permission != PermissionStatus.granted) {
permission = await MetaWearables.requestCameraPermission();
}
if (permission != PermissionStatus.granted) {
setState(() => _error = 'Camera permission denied');
return;
}
// ── Step 5: Start the camera stream ─────────────────────────────────
final stream = await MetaWearables.startCameraStream(
config: const StreamConfiguration(
videoQuality: VideoQuality.medium,
frameRate: FrameRate.fps24,
),
);
_cameraStream = stream;
// Listen to state changes
MetaWearables.streamStateChanges.listen((state) {
setState(() => _streamState = state);
});
// Listen to video frames — update UI with each frame
stream.frames.listen((frameBytes) {
setState(() => _currentFrame = frameBytes);
});
} on WearablesError catch (e) {
setState(() => _error = e.description);
}
}
Future<void> _stopStreaming() async {
await _cameraStream?.dispose();
setState(() {
_cameraStream = null;
_currentFrame = null;
_streamState = StreamState.stopped;
});
}
Future<void> _capturePhoto() async {
try {
await _cameraStream?.capturePhoto();
} on WearablesError catch (e) {
setState(() => _error = e.description);
}
}
Future<void> _showDisplayScreen() async {
try {
final sessionId = await MetaWearables.createSession();
setState(() => _displaySessionId = sessionId);
await MetaWearables.startSession(sessionId);
final displayId = await MetaWearables.addDisplay(sessionId);
setState(() => _displayId = displayId);
await MetaWearables.sendDisplayContent(
displayId,
"Hello from Flutter",
"This is a test of the Display capability.",
);
} on WearablesError catch (e) {
setState(() => _error = e.description);
}
}
Future<void> _stopDisplay() async {
try {
if (_displaySessionId != null && _displayId != null) {
await MetaWearables.removeDisplay(_displaySessionId!, _displayId!);
}
if (_displaySessionId != null) {
await MetaWearables.stopSession(_displaySessionId!);
}
} finally {
setState(() {
_displaySessionId = null;
_displayId = null;
});
}
}
Future<void> _routeMicHFP() async {
try {
await MetaWearables.routeAudioToBluetooth(true);
if (mounted) setState(() => _error = 'Routed Audio to HFP');
} catch (e) {
if (mounted) setState(() => _error = e.toString());
}
}
Future<void> _routeA2DP() async {
try {
await MetaWearables.routeAudioToBluetooth(false);
if (mounted) setState(() => _error = 'Routed Audio to A2DP');
} catch (e) {
if (mounted) setState(() => _error = e.toString());
}
}
@override
void dispose() {
_cameraStream?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Meta Glasses')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Error banner
if (_error != null)
Card(
color: Colors.red.shade900,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 8),
Expanded(
child: Text(_error!,
style: const TextStyle(color: Colors.white))),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => setState(() => _error = null),
)
]),
),
),
// ── Registration ──────────────────────────────────────────────────
_SectionCard(
title: 'Registration',
child: Column(children: [
_StatusRow('State', _registrationState.name),
const SizedBox(height: 8),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed:
_registrationState != RegistrationState.registered
? _register
: null,
icon: const Icon(Icons.link),
label: const Text('Register with Meta AI'),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _registrationState == RegistrationState.registered
? () => MetaWearables.startUnregistration()
: null,
icon: const Icon(Icons.link_off),
label: const Text('Unregister'),
),
]),
]),
),
// ── Devices ───────────────────────────────────────────────────────
_SectionCard(
title: 'Connected Devices (${_devices.length})',
child: _devices.isEmpty
? const Text(
'No devices found. Make sure glasses are paired and Meta AI app is open.')
: Column(
children: _devices
.map((d) => ListTile(
leading: Icon(
Icons.remove_red_eye,
color:
d.isConnected ? Colors.green : Colors.grey,
),
title: Text(d.name),
subtitle: Text('ID: ${d.id}'),
trailing: Text(
d.isConnected ? 'Connected' : 'Disconnected'),
))
.toList(),
),
),
// ── Camera Stream ─────────────────────────────────────────────────
_SectionCard(
title: 'Camera Stream',
child: Column(children: [
_StatusRow('Stream state', _streamState.name),
const SizedBox(height: 8),
// Live camera preview
AspectRatio(
aspectRatio: 9 / 16,
child: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
),
child: _currentFrame != null
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(
_currentFrame!,
gaplessPlayback: true,
fit: BoxFit.cover,
),
)
: const Center(
child: Text(
'No stream active',
style: TextStyle(color: Colors.grey),
),
),
),
),
const SizedBox(height: 12),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _cameraStream == null &&
_registrationState == RegistrationState.registered
? _startStreaming
: null,
icon: const Icon(Icons.videocam),
label: const Text('Start Stream'),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _cameraStream != null ? _stopStreaming : null,
icon: const Icon(Icons.stop),
label: const Text('Stop'),
),
]),
]),
),
// ── Photo Capture ─────────────────────────────────────────────────
_SectionCard(
title: 'Photo Capture',
child: Column(children: [
ElevatedButton.icon(
onPressed: _cameraStream != null ? _capturePhoto : null,
icon: const Icon(Icons.camera_alt),
label: const Text('Capture Photo'),
),
if (_lastPhoto != null) ...[
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(_lastPhoto!.bytes),
),
Text(
'Captured at ${DateTime.fromMillisecondsSinceEpoch(_lastPhoto!.timestampMs)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
]),
),
// ── Display Integration ───────────────────────────────────────────
_SectionCard(
title: 'Display Integration',
child: Column(children: [
_StatusRow('Display Status', _displayId != null ? 'Active' : 'Inactive'),
const SizedBox(height: 8),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _displayId == null && _registrationState == RegistrationState.registered
? _showDisplayScreen
: null,
icon: const Icon(Icons.monitor),
label: const Text('Show Display'),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _displayId != null ? _stopDisplay : null,
icon: const Icon(Icons.close),
label: const Text('Stop'),
),
]),
]),
),
// ── Audio Routing ────────────────────────────────────────────────
_SectionCard(
title: 'Audio Routing',
child: Column(children: [
const Text('HFP provides microphone, A2DP provides HQ audio out.'),
const SizedBox(height: 8),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _routeMicHFP,
icon: const Icon(Icons.mic),
label: const Text('Route HFP (Mic)'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _routeA2DP,
icon: const Icon(Icons.speaker),
label: const Text('Route A2DP (HQ)'),
),
),
]),
]),
),
],
),
);
}
}
// ── Small reusable widgets ────────────────────────────────────────────────────
class _SectionCard extends StatelessWidget {
final String title;
final Widget child;
const _SectionCard({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
const Divider(),
child,
]),
),
);
}
}
class _StatusRow extends StatelessWidget {
final String label;
final String value;
const _StatusRow(this.label, this.value);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.grey)),
Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
],
);
}
}