xcamera 0.0.2 copy "xcamera: ^0.0.2" to clipboard
xcamera: ^0.0.2 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

// 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 Diagnostic Dashboard',
      theme: ThemeData.dark(useMaterial3: true).copyWith(
        colorScheme: ColorScheme.dark(
          primary: Colors.blueAccent,
          secondary: Colors.tealAccent,
          surface: Colors.grey[900]!,
        ),
      ),
      home: const DashboardScreen(),
    );
  }
}

class DashboardScreen extends StatefulWidget {
  const DashboardScreen({super.key});

  @override
  State<DashboardScreen> createState() => _DashboardScreenState();
}

class _DashboardScreenState extends State<DashboardScreen> {
  /// List of available cameras on the device.
  List<CameraDescription> _cameras = [];

  /// Currently selected camera for detail controls/diagnostics.
  CameraDescription? _selectedCamera;

  /// Selected resolution preset for the camera.
  ResolutionPreset _selectedPreset = ResolutionPreset.medium;

  /// Map of index to active camera controllers.
  final Map<int, CameraController> _controllers = {};

  /// Dynamically get the active controller for the currently selected detail camera.
  CameraController? get _controller => _controllers[_selectedCamera?.index ?? -1];

  /// Status message to display in the UI.
  String _statusMessage = 'System Ready';

  /// Error message to display when an error occurs.
  String _errorMessage = '';

  /// Image streaming statistics
  int _imageStreamFrameCount = 0;
  int _imageStreamWidth = 0;
  int _imageStreamHeight = 0;
  bool _enableAudio = true;

  @override
  void initState() {
    super.initState();
    _loadCamerasAndInitialize();
  }

  @override
  void dispose() {
    for (final CameraController controller in _controllers.values) {
      controller.dispose();
    }
    super.dispose();
  }

  /// Loads available cameras and initializes the first one.
  Future<void> _loadCamerasAndInitialize() async {
    try {
      // Get list of available cameras
      final cameras = await availableCameras();

      // Dispose all existing controllers
      for (final CameraController controller in _controllers.values) {
        await controller.dispose();
      }
      _controllers.clear();

      setState(() {
        _cameras = cameras;
        if (cameras.isNotEmpty) {
          _selectedCamera = cameras.first;
        }
      });

      // Initialize the first camera if available
      if (_selectedCamera != null) {
        await _initializeCamera();
      } else {
        setState(() => _errorMessage = 'No camera devices detected.');
      }
    } catch (e) {
      setState(() => _errorMessage = 'Failed to load cameras: $e');
    }
  }

  /// Initializes the camera controller with the selected camera and preset.
  Future<void> _initializeCamera() async {
    if (_selectedCamera == null) return;

    setState(() {
      _errorMessage = '';
      _statusMessage = 'Initializing camera ${_selectedCamera!.name}...';
    });

    // Dispose existing controller for this camera if any
    final existing = _controllers[_selectedCamera!.index];
    if (existing != null) {
      await existing.dispose();
      _controllers.remove(_selectedCamera!.index);
    }

    try {
      // Create new camera controller
      final controller = CameraController(_selectedCamera!, _selectedPreset);

      // Listen to value changes for UI updates
      controller.addListener(() {
        if (mounted) setState(() {});
      });

      // Initialize the controller
      await controller.initialize();

      setState(() {
        _controllers[_selectedCamera!.index] = controller;
        _statusMessage = 'Camera ${_selectedCamera!.name} initialized successfully';
      });
    } catch (e) {
      setState(() {
        _errorMessage = e is CameraException ? e.message : e.toString();
        _statusMessage = 'Initialization failed';
      });
    }
  }

  // Camera Action Methods

  /// Captures a photo and saves it to the device.
  Future<void> _takePhoto() async {
    if (_controller == null || !_controller!.value.isInitialized) return;

    try {
      setState(() => _statusMessage = 'Capturing snapshot...');
      final file = await _controller!.takePicture();
      setState(() => _statusMessage = 'Photo saved: ${file.path}');
    } catch (e) {
      setState(() => _statusMessage = 'Error taking photo: $e');
    }
  }

  /// Toggles video recording on/off.
  Future<void> _toggleVideoRecording() async {
    if (_controller == null || !_controller!.value.isInitialized) return;

    try {
      if (_controller!.value.isRecordingVideo) {
        // Stop recording
        setState(() => _statusMessage = 'Stopping recording...');
        final file = await _controller!.stopVideoRecording();
        setState(() => _statusMessage = 'Video saved: ${file.path}');
      } else {
        // Start recording
        setState(() => _statusMessage = 'Starting recording...');
        await _controller!.startVideoRecording(enableAudio: _enableAudio);
        setState(() => _statusMessage = 'Recording in progress...');
      }
    } catch (e) {
      setState(() => _statusMessage = 'Recording error: $e');
    }
  }

  /// Toggles pause/resume for video recording.
  Future<void> _togglePauseRecording() async {
    if (_controller == null || !_controller!.value.isInitialized || !_controller!.value.isRecordingVideo) {
      return;
    }

    try {
      if (_controller!.value.isRecordingPaused) {
        // Resume recording
        setState(() => _statusMessage = 'Resuming recording...');
        await _controller!.resumeVideoRecording();
        setState(() => _statusMessage = 'Recording resumed');
      } else {
        // Pause recording
        setState(() => _statusMessage = 'Pausing recording...');
        await _controller!.pauseVideoRecording();
        setState(() => _statusMessage = 'Recording paused');
      }
    } catch (e) {
      setState(() => _statusMessage = 'Recording pause/resume error: $e');
    }
  }

  /// Toggles pause/resume for the camera preview.
  Future<void> _togglePausePreview() async {
    if (_controller == null || !_controller!.value.isInitialized) return;

    try {
      if (_controller!.value.isPreviewPaused) {
        // Resume preview
        setState(() => _statusMessage = 'Resuming preview...');
        await _controller!.resumePreview();
        setState(() => _statusMessage = 'Preview resumed');
      } else {
        // Pause preview
        setState(() => _statusMessage = 'Pausing preview...');
        await _controller!.pausePreview();
        setState(() => _statusMessage = 'Preview paused');
      }
    } catch (e) {
      setState(() => _statusMessage = 'Preview pause/resume error: $e');
    }
  }

  /// Toggles image streaming on/off.
  Future<void> _toggleImageStream() async {
    if (_controller == null || !_controller!.value.isInitialized) return;

    try {
      if (_controller!.value.isImageStreaming) {
        setState(() => _statusMessage = 'Stopping image stream...');
        await _controller!.stopImageStream();
        setState(() => _statusMessage = 'Image stream stopped');
      } else {
        setState(() {
          _statusMessage = 'Starting image stream...';
          _imageStreamFrameCount = 0;
        });
        await _controller!.startImageStream((CameraImage image) {
          if (mounted) {
            setState(() {
              _imageStreamFrameCount++;
              _imageStreamWidth = image.width;
              _imageStreamHeight = image.height;
            });
          }
        });
        setState(() => _statusMessage = 'Image stream active');
      }
    } catch (e) {
      setState(() => _statusMessage = 'Image stream error: $e');
    }
  }

  /// Builds a diagnostic row with label and value.
  Widget _buildDiagnosticRow(String label, String value, {bool isHighlight = false}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.grey),
          ),
          Text(
            value,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              color: isHighlight ? Colors.tealAccent : Colors.white,
            ),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('XCamera Diagnostic Dashboard'),
        backgroundColor: Theme.of(context).colorScheme.surface,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _loadCamerasAndInitialize,
            tooltip: 'Reload cameras',
          ),
        ],
      ),
      body: Row(
        children: [
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.black,
              child: _buildPreviewContent(),
            ),
          ),
          Expanded(
            flex: 2,
            child: Container(
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.surface,
                border: const Border(left: BorderSide(color: Colors.black54, width: 2)),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  _buildStatusBar(),
                  Expanded(
                    child: ListView(
                      padding: const EdgeInsets.all(16),
                      children: [
                        _buildDeviceSettingsSection(),
                        const SizedBox(height: 24),
                        _buildControllerStateSection(),
                        const SizedBox(height: 24),
                        _buildControlsSection(),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  /// Builds the preview content (camera preview grid or error state).
  Widget _buildPreviewContent() {
    if (_errorMessage.isNotEmpty) {
      return Padding(
        padding: const EdgeInsets.all(24.0),
        child: Center(
          child: Text(
            _errorMessage,
            style: const TextStyle(color: Colors.redAccent, fontSize: 16),
            textAlign: TextAlign.center,
          ),
        ),
      );
    }

    if (_cameras.isEmpty) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    return GridView.builder(
      padding: const EdgeInsets.all(16),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 16,
        mainAxisSpacing: 16,
        childAspectRatio: 1.25,
      ),
      itemCount: _cameras.length,
      itemBuilder: (context, index) {
        final camera = _cameras[index];
        final controller = _controllers[camera.index];
        final isSelected = _selectedCamera?.index == camera.index;

        return Card(
          elevation: isSelected ? 8 : 2,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
            side: BorderSide(
              color: isSelected ? Colors.tealAccent : Colors.transparent,
              width: 2,
            ),
          ),
          clipBehavior: Clip.antiAlias,
          child: InkWell(
            onTap: () {
              setState(() {
                _selectedCamera = camera;
              });
            },
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // Header bar
                Container(
                  color: isSelected ? Colors.teal.withAlpha(50) : Colors.blueAccent.withAlpha(20),
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(
                        child: Text(
                          camera.name,
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 13,
                            color: isSelected ? Colors.tealAccent : Colors.white,
                          ),
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      Text(
                        controller == null ? 'OFFLINE' : (controller.value.isRecordingVideo ? 'RECORDING' : 'LIVE'),
                        style: TextStyle(
                          fontSize: 10,
                          fontWeight: FontWeight.bold,
                          color: controller == null ? Colors.grey : (controller.value.isRecordingVideo ? Colors.redAccent : Colors.tealAccent),
                        ),
                      ),
                    ],
                  ),
                ),
                // Camera stream or start button
                Expanded(
                  child: Container(
                    color: Colors.black87,
                    child: (() {
                      if (controller != null && controller.value.isInitialized) {
                        return Center(
                          child: AspectRatio(
                            aspectRatio: controller.value.previewSize != null
                                ? controller.value.previewSize!.width / controller.value.previewSize!.height
                                : 16 / 9,
                            child: CameraPreview(controller),
                          ),
                        );
                      } else {
                        return Center(
                          child: ElevatedButton.icon(
                            onPressed: () {
                              setState(() {
                                _selectedCamera = camera;
                              });
                              _initializeCamera();
                            },
                            icon: const Icon(Icons.videocam),
                            label: const Text('Start Stream'),
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.blueAccent,
                              foregroundColor: Colors.white,
                            ),
                          ),
                        );
                      }
                    })(),
                  ),
                ),
                // Quick info / control footer
                if (controller != null && controller.value.isInitialized) ...[
                  Container(
                    padding: const EdgeInsets.all(8),
                    color: Colors.grey[900],
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          controller.value.previewSize != null
                              ? '${controller.value.previewSize!.width.toInt()}x${controller.value.previewSize!.height.toInt()}'
                              : 'Unknown',
                          style: const TextStyle(fontSize: 11, color: Colors.grey),
                        ),
                        if (controller.value.isRecordingVideo) const Icon(Icons.fiber_manual_record, color: Colors.redAccent, size: 14),
                      ],
                    ),
                  ),
                ],
              ],
            ),
          ),
        );
      },
    );
  }

  /// Builds the status bar showing current status message.
  Widget _buildStatusBar() {
    return Container(
      color: Colors.blueAccent.withAlpha(31),
      padding: const EdgeInsets.all(12),
      child: Row(
        children: [
          const Icon(Icons.info_outline, color: Colors.blueAccent, size: 20),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              _statusMessage,
              style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.blueAccent),
            ),
          ),
        ],
      ),
    );
  }

  /// Builds the device settings section with camera and resolution selection.
  Widget _buildDeviceSettingsSection() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const Text(
          'DEVICE SETTINGS',
          style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blueAccent),
        ),
        const SizedBox(height: 8),
        DropdownButtonFormField<CameraDescription>(
          decoration: const InputDecoration(
            labelText: 'Active Camera',
            border: OutlineInputBorder(),
          ),
          initialValue: _selectedCamera,
          items: _cameras.map((camera) {
            return DropdownMenuItem<CameraDescription>(
              value: camera,
              child: Text(camera.name),
            );
          }).toList(),
          onChanged: (camera) {
            if (camera != null) {
              setState(() => _selectedCamera = camera);
              _initializeCamera();
            }
          },
        ),
        const SizedBox(height: 12),
        DropdownButtonFormField<ResolutionPreset>(
          decoration: const InputDecoration(
            labelText: 'Resolution Preset',
            border: OutlineInputBorder(),
          ),
          initialValue: _selectedPreset,
          items: ResolutionPreset.values.map((preset) {
            return DropdownMenuItem<ResolutionPreset>(
              value: preset,
              child: Text(preset.name.toUpperCase()),
            );
          }).toList(),
          onChanged: (preset) {
            if (preset != null) {
              setState(() => _selectedPreset = preset);
              _initializeCamera();
            }
          },
        ),
      ],
    );
  }

  /// Builds the controller state diagnostics section.
  Widget _buildControllerStateSection() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const Text(
          'CONTROLLER STATE',
          style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blueAccent),
        ),
        const SizedBox(height: 8),
        Container(
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: Colors.black26,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Column(
            children: [
              _buildDiagnosticRow(
                'Initialized',
                _controller?.value.isInitialized.toString() ?? 'false',
              ),
              _buildDiagnosticRow(
                'Streaming Images',
                _controller?.value.isStreamingImages.toString() ?? 'false',
              ),
              _buildDiagnosticRow(
                'Preview Paused',
                _controller?.value.isPreviewPaused.toString() ?? 'false',
                isHighlight: _controller?.value.isPreviewPaused ?? false,
              ),
              _buildDiagnosticRow(
                'Recording Video',
                _controller?.value.isRecordingVideo.toString() ?? 'false',
                isHighlight: _controller?.value.isRecordingVideo ?? false,
              ),
              _buildDiagnosticRow(
                'Recording Paused',
                _controller?.value.isRecordingPaused.toString() ?? 'false',
                isHighlight: _controller?.value.isRecordingPaused ?? false,
              ),
              _buildDiagnosticRow(
                'Resolution Size',
                _controller?.value.previewSize != null
                    ? '${_controller!.value.previewSize!.width.toInt()} x ${_controller!.value.previewSize!.height.toInt()}'
                    : 'Unknown',
              ),
              _buildDiagnosticRow(
                'Image Stream Active',
                _controller?.value.isImageStreaming.toString() ?? 'false',
                isHighlight: _controller?.value.isImageStreaming ?? false,
              ),
              if (_controller?.value.isImageStreaming == true) ...[
                _buildDiagnosticRow(
                  'Stream Frame Count',
                  _imageStreamFrameCount.toString(),
                  isHighlight: true,
                ),
                _buildDiagnosticRow(
                  'Stream Frame Size',
                  '$_imageStreamWidth x $_imageStreamHeight',
                ),
              ],
            ],
          ),
        ),
      ],
    );
  }

  /// Builds the controls section with action buttons.
  Widget _buildControlsSection() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const Text(
          'CONTROLS',
          style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blueAccent),
        ),
        const SizedBox(height: 8),
        ElevatedButton.icon(
          onPressed: _controller?.value.isInitialized == true ? _takePhoto : null,
          icon: const Icon(Icons.camera_alt),
          label: const Text('Capture Snapshot'),
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blueAccent,
            foregroundColor: Colors.white,
            padding: const EdgeInsets.all(16),
          ),
        ),
        const SizedBox(height: 8),
        SwitchListTile(
          title: const Text('Record Audio'),
          value: _enableAudio,
          activeThumbColor: Colors.teal,
          onChanged: _controller?.value.isRecordingVideo == true ? null : (val) => setState(() => _enableAudio = val),
        ),
        const SizedBox(height: 4),
        Row(
          children: [
            Expanded(
              child: ElevatedButton.icon(
                onPressed: _controller?.value.isInitialized == true ? _toggleVideoRecording : null,
                icon: Icon(_controller?.value.isRecordingVideo == true ? Icons.stop : Icons.videocam),
                label: Text(_controller?.value.isRecordingVideo == true ? 'Stop Video' : 'Record Video'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: _controller?.value.isRecordingVideo == true ? Colors.redAccent : Colors.teal,
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.all(16),
                ),
              ),
            ),
            if (_controller?.value.isRecordingVideo == true) ...[
              const SizedBox(width: 8),
              IconButton.filled(
                onPressed: _togglePauseRecording,
                icon: Icon(
                  _controller?.value.isRecordingPaused == true ? Icons.play_arrow : Icons.pause,
                ),
                style: IconButton.styleFrom(
                  backgroundColor: Colors.orange,
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.all(16),
                ),
              ),
            ],
          ],
        ),
        const SizedBox(height: 12),
        ElevatedButton.icon(
          onPressed: _controller?.value.isInitialized == true ? _togglePausePreview : null,
          icon: Icon(_controller?.value.isPreviewPaused == true ? Icons.visibility : Icons.visibility_off),
          label: Text(_controller?.value.isPreviewPaused == true ? 'Resume Preview' : 'Pause Preview'),
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.deepPurple,
            foregroundColor: Colors.white,
            padding: const EdgeInsets.all(16),
          ),
        ),
        const SizedBox(height: 12),
        ElevatedButton.icon(
          onPressed: _controller?.value.isInitialized == true ? _toggleImageStream : null,
          icon: Icon(_controller?.value.isImageStreaming == true ? Icons.stop_circle : Icons.play_circle),
          label: Text(_controller?.value.isImageStreaming == true ? 'Stop Image Stream' : 'Start Image Stream'),
          style: ElevatedButton.styleFrom(
            backgroundColor: _controller?.value.isImageStreaming == true ? Colors.redAccent : Colors.teal[700],
            foregroundColor: Colors.white,
            padding: const EdgeInsets.all(16),
          ),
        ),
      ],
    );
  }
}
1
likes
0
points
168
downloads

Publisher

unverified uploader

Weekly Downloads

A lightweight Flutter camera plugin for Linux supporting high-performance V4L2 streaming and synchronized H.264/AAC video recording.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

cross_file, equatable, flutter, get_it, plugin_platform_interface

More

Packages that depend on xcamera

Packages that implement xcamera