xcamera 0.1.0
xcamera: ^0.1.0 copied to clipboard
A lightweight Flutter camera plugin for Linux supporting high-performance V4L2 streaming and synchronized H.264/AAC video recording.
// example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:xcamera/xcamera.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'XCamera Basic Preview',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: const ColorScheme.dark(
primary: Color(0xFF7C9E8A),
secondary: Color(0xFFA8C4B8),
surface: Color(0xFF1E1E1E),
surfaceContainer: Color(0xFF2A2A2A),
),
scaffoldBackgroundColor: const Color(0xFF121212),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1A1A1A),
elevation: 0.0,
centerTitle: false,
),
cardTheme: CardThemeData(
color: const Color(0xFF252525),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
elevation: 1.0,
),
textTheme: const TextTheme(
bodyMedium: TextStyle(color: Color(0xFFE0E0E0), fontSize: 13.0),
labelSmall: TextStyle(color: Color(0xFF9E9E9E), fontSize: 11.0),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13.0),
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 0.0,
),
minimumSize: const Size(0.0, 40.0),
),
),
),
home: const BasicPreview(),
);
}
}
class BasicPreview extends StatefulWidget {
const BasicPreview({super.key});
@override
State<BasicPreview> createState() => _BasicPreviewState();
}
class _BasicPreviewState extends State<BasicPreview> {
CameraController? _controller;
List<CameraDescription>? _cameras;
CameraDescription? _selectedCamera;
bool _isInitialized = false;
bool _isSwitching = false;
String _error = '';
@override
void initState() {
super.initState();
// Initialize cameras when the widget is first created.
_initCameras();
}
Future<void> _initCameras() async {
try {
// Retrieve all available camera devices.
final cameras = await availableCameras();
if (cameras.isEmpty) {
setState(() => _error = 'No cameras found');
return;
}
// Update state with the camera list and select the first camera.
setState(() {
_cameras = cameras;
_selectedCamera = cameras[0];
_error = '';
});
// Initialize the camera controller with the selected camera.
await _initController(cameras[0]);
} catch (e) {
setState(() => _error = 'Error: $e');
}
}
Future<void> _initController(CameraDescription camera) async {
// Prevent multiple simultaneous initialization attempts.
if (_isSwitching) return;
// Update state to reflect the switching/initialization process.
setState(() {
_isSwitching = true;
_isInitialized = false;
_selectedCamera = camera;
});
try {
// Dispose the existing controller if any.
await _controller?.dispose();
// Create and initialize the new controller with maximum resolution.
final controller = CameraController(camera, ResolutionPreset.max);
await controller.initialize();
// Update state to reflect successful initialization.
setState(() {
_controller = controller;
_isInitialized = true;
_isSwitching = false;
_error = '';
});
} catch (e) {
// Handle initialization errors.
setState(() {
_error = 'Failed to initialize camera: $e';
_isSwitching = false;
_isInitialized = false;
_controller = null;
});
}
}
@override
void dispose() {
// Release camera resources when the widget is disposed.
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_error.isNotEmpty) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_error,
style: const TextStyle(
color: Colors.redAccent,
),
),
),
),
);
} else if (_cameras == null || _cameras!.isEmpty) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} else if (_isSwitching || !_isInitialized || _controller == null) {
return const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF7C9E8A)),
),
Text(
'Initializing camera...',
style: TextStyle(
color: Color(0xFF7C9E8A),
fontSize: 16.0,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
} else {
return Scaffold(
body: Column(
children: [
Expanded(
child: Container(
color: Colors.black,
child: Center(
child: AspectRatio(
aspectRatio: (() {
if (_controller!.value.previewSize != null) {
return _controller!.value.previewSize!.width / _controller!.value.previewSize!.height;
} else {
return 16 / 9;
}
})(),
child: CameraPreview(_controller!),
),
),
),
),
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
border: Border(
top: BorderSide(
color: Colors.black54,
width: 1.0,
),
),
),
child: Row(
spacing: 16.0,
children: [
Expanded(
child: DropdownButton<CameraDescription>(
value: _selectedCamera,
isExpanded: true,
dropdownColor: const Color(0xFF252525),
style: const TextStyle(
color: Colors.white,
fontSize: 14.0,
),
underline: Container(
height: 2.0,
color: const Color(0xFF7C9E8A),
),
items: _cameras!.map((camera) {
return DropdownMenuItem(
value: camera,
child: Text(
camera.name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
onChanged: (camera) async {
if (camera != null && camera != _selectedCamera) {
await _initController(camera);
}
},
),
),
Container(
decoration: BoxDecoration(
color: _isInitialized ? const Color(0xFF7C9E8A) : Colors.grey[800],
borderRadius: BorderRadius.circular(6.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Text(
_isInitialized ? 'LIVE' : 'OFF',
style: const TextStyle(
fontSize: 11.0,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
),
],
),
);
}
}
}