flutter_usbcamera 0.0.1 copy "flutter_usbcamera: ^0.0.1" to clipboard
flutter_usbcamera: ^0.0.1 copied to clipboard

PlatformAndroid

Flutter plugin for Android UVC (USB Video Class) camera preview, capture, recording and OpenGL rendering. Based on AndroidUSBCamera (AUSBC).

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_usbcamera/flutter_usbcamera.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AUSBC Flutter',
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: Colors.black,
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF6200EE),
          secondary: Color(0xFF03DAC5),
        ),
      ),
      home: const DemoPage(),
    );
  }
}

// ============================================================
// DemoPage — matches DemoFragment layout (fragment_demo.xml)
// ============================================================
class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  final _controller = UsbCameraController();
  StreamSubscription<CameraEvent>? _eventSub;
  int _textureId = -1; // legacy, kept for compatibility
  bool _isCameraOpened = false;
  bool _isCapturing = false;
  bool _isPlayingMic = false;
  String _frameRateText = '';
  List<PreviewSize> _previewSizes = [];
  int? _currentDeviceId;
  int _brightnessMax = 100;
  int _brightnessValue = 0;

  // Capture mode: 0=photo, 1=video, 2=audio
  int _captureMode = 0;

  // Recording timer
  Timer? _recTimer;
  int _recSeconds = 0;
  bool _recDotVisible = true;

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

  Future<void> _init() async {
    _eventSub = UsbCameraController.events.listen(_handleEvent);
    await _controller.registerUsb();
  }
  void _handleEvent(CameraEvent event) {
    switch (event.type) {
      case CameraEventType.onAttachDev:
        if (event.deviceId != null) {
          _controller.requestPermission(event.deviceId!);
        }
        break;
      case CameraEventType.onDetachDev:
        setState(() {
          _isCameraOpened = false;
          _currentDeviceId = null;
        });
        break;
      case CameraEventType.onConnectDev:
        _openCamera(event.deviceId!);
        break;
      case CameraEventType.onDisConnectDev:
        setState(() {
          _isCameraOpened = false;
        });
        break;
      case CameraEventType.onCameraState:
        if (event.state == 'opened') {
          setState(() => _isCameraOpened = true);
          _loadPreviewSizes();
        } else if (event.state == 'closed') {
          setState(() => _isCameraOpened = false);
        } else if (event.state == 'error') {
          setState(() => _isCameraOpened = false);
          _showSnackBar('Camera error: ${event.message}');
        }
        break;
      case CameraEventType.onCaptureBegin:
        if (event.captureType == 'video' || event.captureType == 'audio') {
          setState(() => _isCapturing = true);
          _startRecTimer();
        }
        break;
      case CameraEventType.onCaptureComplete:
        if (event.captureType == 'video' || event.captureType == 'audio') {
          setState(() => _isCapturing = false);
          _stopRecTimer();
        }
        _showSnackBar('Saved: ${event.path}');
        break;
      case CameraEventType.onCaptureError:
        setState(() => _isCapturing = false);
        _stopRecTimer();
        _showSnackBar('Error: ${event.error}');
        break;
      case CameraEventType.onPlayMicBegin:
        setState(() => _isPlayingMic = true);
        break;
      case CameraEventType.onPlayMicComplete:
      case CameraEventType.onPlayMicError:
        setState(() => _isPlayingMic = false);
        break;
      default:
        break;
    }
  }

  Future<void> _openCamera(int deviceId) async {
    await _controller.openCamera(
      deviceId,
      request: const CameraRequest(
        previewWidth: 640,
        previewHeight: 480,
        renderMode: RenderMode.opengl,
        previewFormat: PreviewFormat.mjpeg,
      ),
    );
    setState(() {
      _currentDeviceId = deviceId;
    });
  }

  Future<void> _loadPreviewSizes() async {
    // Wait a moment for camera to fully initialize
    await Future.delayed(const Duration(milliseconds: 500));
    final sizes = await _controller.getAllPreviewSizes();
    final brightness = await _controller.getBrightness();
    setState(() {
      _previewSizes = sizes;
      _brightnessValue = brightness ?? 0;
    });
  }

  void _startRecTimer() {
    _recSeconds = 0;
    _recDotVisible = true;
    _recTimer = Timer.periodic(const Duration(seconds: 1), (_) {
      setState(() {
        _recSeconds++;
        _recDotVisible = _recSeconds % 2 == 0;
      });
    });
  }

  void _stopRecTimer() {
    _recTimer?.cancel();
    _recTimer = null;
    _recSeconds = 0;
  }

  String _formatTime(int totalSeconds) {
    final m = (totalSeconds ~/ 60).toString().padLeft(2, '0');
    final s = (totalSeconds % 60).toString().padLeft(2, '0');
    return '$m:$s';
  }

  void _showSnackBar(String msg) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
  }

  @override
  void dispose() {
    _recTimer?.cancel();
    _eventSub?.cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Column(
          children: [
            // === Top toolbar (matches toolbarBg, 52dp) ===
            _buildToolbar(),
            // === Camera preview area ===
            Expanded(child: _buildPreviewArea()),
            // === Brightness SeekBar ===
            if (_isCameraOpened) _buildBrightnessBar(),
            // === Mode switch + capture area (matches controlPanelLayout, 180dp) ===
            _buildControlPanel(),
          ],
        ),
      ),
    );
  }

  // --- Top toolbar: settings | effects | type | voice | resolution ---
  Widget _buildToolbar() {
    if (_isCapturing) return const SizedBox.shrink();
    return Container(
      height: 52,
      color: Colors.black,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _toolbarIcon('assets/icons/more.png', Icons.more_horiz, _showMoreMenu),
          _toolbarIcon('assets/icons/filter.png', Icons.auto_awesome, _showEffectsDialog),
          _toolbarIcon('assets/icons/type.png', Icons.camera, null),
          _toolbarIcon(
            'assets/icons/voice.png',
            _isPlayingMic ? Icons.mic : Icons.mic_off,
            _toggleMic,
          ),
          _toolbarIcon('assets/icons/resolution.png', Icons.aspect_ratio, _showResolutionDialog),
        ],
      ),
    );
  }

  Widget _toolbarIcon(String assetPath, IconData fallbackIcon, VoidCallback? onTap) {
    return GestureDetector(
      onTap: onTap,
      child: SizedBox(
        width: 48,
        height: 48,
        child: Padding(
          padding: const EdgeInsets.all(8),
          child: Icon(fallbackIcon, color: Colors.white, size: 28),
        ),
      ),
    );
  }

  // --- Camera preview area with UVC logo placeholder ---
  Widget _buildPreviewArea() {
    return Container(
      color: Colors.black,
      child: Stack(
        children: [
          // Always show the PlatformView — it's a real Android TextureView
          // Camera will render to it when opened, black when not
          const Positioned.fill(child: UsbCameraPreview()),
          // UVC logo overlay when camera not opened
          if (!_isCameraOpened)
            Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.usb, size: 80, color: Colors.white.withValues(alpha: 0.3)),
                  const SizedBox(height: 8),
                  Text(
                    'Waiting for USB camera...',
                    style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14),
                  ),
                ],
              ),
            ),
          // Frame rate text (top-left)
          if (_isCameraOpened && _frameRateText.isNotEmpty)
            Positioned(
              top: 12,
              left: 12,
              child: Text(_frameRateText, style: const TextStyle(color: Colors.white, fontSize: 12)),
            ),
        ],
      ),
    );
  }

  // --- Brightness SeekBar (matches brightnessSb) ---
  Widget _buildBrightnessBar() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 40),
      child: Row(
        children: [
          const Icon(Icons.brightness_low, color: Colors.white54, size: 18),
          Expanded(
            child: Slider(
              value: _brightnessValue.toDouble(),
              min: 0,
              max: _brightnessMax.toDouble(),
              onChanged: (v) {
                setState(() => _brightnessValue = v.toInt());
                _controller.setBrightness(v.toInt());
              },
            ),
          ),
          const Icon(Icons.brightness_high, color: Colors.white54, size: 18),
        ],
      ),
    );
  }

  // --- Control panel: mode switch + capture button + album/switch ---
  Widget _buildControlPanel() {
    return Container(
      height: 180,
      color: Colors.black,
      child: Column(
        children: [
          // Recording timer (shown when capturing)
          if (_isCapturing) _buildRecTimer() else _buildModeSwitch(),
          const Spacer(),
          // Capture button row: album | capture | switch camera
          _buildCaptureRow(),
          const SizedBox(height: 16),
        ],
      ),
    );
  }

  // --- Recording timer (matches recTimerLayout) ---
  Widget _buildRecTimer() {
    return Padding(
      padding: const EdgeInsets.only(top: 6),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            width: 10,
            height: 10,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: _recDotVisible ? Colors.red : Colors.transparent,
            ),
          ),
          const SizedBox(width: 5),
          Text(
            _formatTime(_recSeconds),
            style: const TextStyle(color: Colors.white, fontSize: 20),
          ),
        ],
      ),
    );
  }

  // --- Mode switch: photo | video | audio (matches modeSwitchLayout) ---
  Widget _buildModeSwitch() {
    return Padding(
      padding: const EdgeInsets.only(top: 6),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _modeTab('photo', 0),
          const SizedBox(width: 20),
          _modeTab('video', 1),
          const SizedBox(width: 20),
          _modeTab('audio', 2),
        ],
      ),
    );
  }

  Widget _modeTab(String label, int mode) {
    final isSelected = _captureMode == mode;
    return GestureDetector(
      onTap: () => setState(() => _captureMode = mode),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            label,
            style: TextStyle(
              color: isSelected ? Colors.white : const Color(0xFFD7DAE1),
              fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
              fontSize: 14,
              shadows: const [Shadow(blurRadius: 1, color: Color(0xBF000000))],
            ),
          ),
          const SizedBox(height: 1),
          Container(
            width: 6,
            height: 6,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: isSelected ? const Color(0xFF2E5BFF) : Colors.transparent,
            ),
          ),
        ],
      ),
    );
  }

  // --- Capture row: album preview | capture button | switch camera ---
  Widget _buildCaptureRow() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // Album preview (left, 38x38, matches albumPreviewIv)
        if (!_isCapturing)
          SizedBox(
            width: 80,
            child: Center(
              child: Container(
                width: 38,
                height: 38,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.white, width: 1.5),
                ),
                child: const Icon(Icons.photo_library, color: Colors.white, size: 20),
              ),
            ),
          )
        else
          const SizedBox(width: 80),
        // Capture button (center, 76x76, matches captureBtn)
        _buildCaptureButton(),
        // Switch camera (right, 48x48, matches lensFacingBtn1)
        if (!_isCapturing)
          SizedBox(
            width: 80,
            child: Center(
              child: GestureDetector(
                onTap: _showSwitchDialog,
                child: const SizedBox(
                  width: 48,
                  height: 48,
                  child: Icon(Icons.cameraswitch, color: Colors.white, size: 32),
                ),
              ),
            ),
          )
        else
          const SizedBox(width: 80),
      ],
    );
  }

  // --- Capture button (matches CaptureMediaView) ---
  Widget _buildCaptureButton() {
    final isRecording = _isCapturing;
    // Photo mode: white circle; Video/Audio mode: red square when recording
    final bool isPhotoMode = _captureMode == 0;
    return GestureDetector(
      onTap: _onCapture,
      child: Container(
        width: 76,
        height: 76,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          border: Border.all(color: Colors.white, width: 4),
        ),
        child: Center(
          child: isPhotoMode
              ? Container(
                  width: 60,
                  height: 60,
                  decoration: const BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                  ),
                )
              : isRecording
                  ? Container(
                      width: 28,
                      height: 28,
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(4),
                      ),
                    )
                  : Container(
                      width: 60,
                      height: 60,
                      decoration: const BoxDecoration(
                        color: Colors.red,
                        shape: BoxShape.circle,
                      ),
                    ),
        ),
      ),
    );
  }

  // === Actions ===

  void _onCapture() {
    if (!_isCameraOpened) {
      _showSnackBar('camera not worked!');
      return;
    }
    switch (_captureMode) {
      case 0:
        _controller.captureImage();
        break;
      case 1:
        if (_isCapturing) {
          _controller.captureVideoStop();
        } else {
          _controller.captureVideoStart();
        }
        break;
      case 2:
        if (_isCapturing) {
          _controller.captureAudioStop();
        } else {
          _controller.captureAudioStart();
        }
        break;
    }
  }

  void _toggleMic() {
    if (_isPlayingMic) {
      _controller.stopPlayMic();
    } else {
      _controller.startPlayMic();
    }
  }

  void _showSwitchDialog() async {
    final devices = await _controller.getDeviceList();
    if (!mounted || devices.isEmpty) {
      _showSnackBar('Get usb device failed');
      return;
    }
    showDialog(
      context: context,
      builder: (ctx) {
        return SimpleDialog(
          title: const Text('Switch Camera'),
          children: devices.map((dev) {
            final isCurrent = dev.deviceId == _currentDeviceId;
            final name = dev.productName.isNotEmpty
                ? '${dev.productName}(${dev.deviceId})'
                : dev.deviceName;
            return SimpleDialogOption(
              onPressed: () {
                Navigator.pop(ctx);
                if (!isCurrent) _controller.switchCamera(dev.deviceId);
              },
              child: Text(
                '$name${isCurrent ? " ✓" : ""}',
                style: TextStyle(color: isCurrent ? const Color(0xFF2E5BFF) : null),
              ),
            );
          }).toList(),
        );
      },
    );
  }

  void _showEffectsDialog() {
    showDialog(
      context: context,
      builder: (ctx) => SimpleDialog(
        title: const Text('Effects'),
        children: [
          SimpleDialogOption(
            onPressed: () {
              _controller.updateRenderEffect(EffectClassifyId.filter);
              Navigator.pop(ctx);
            },
            child: const Text('None'),
          ),
          SimpleDialogOption(
            onPressed: () {
              _controller.updateRenderEffect(EffectClassifyId.filter, effectId: EffectId.blackWhite);
              Navigator.pop(ctx);
            },
            child: const Text('BlackWhite'),
          ),
          SimpleDialogOption(
            onPressed: () {
              _controller.updateRenderEffect(EffectClassifyId.animation, effectId: EffectId.zoom);
              Navigator.pop(ctx);
            },
            child: const Text('Zoom'),
          ),
          SimpleDialogOption(
            onPressed: () {
              _controller.updateRenderEffect(EffectClassifyId.animation, effectId: EffectId.soul);
              Navigator.pop(ctx);
            },
            child: const Text('Soul'),
          ),
        ],
      ),
    );
  }

  void _showResolutionDialog() {
    if (_previewSizes.isEmpty) {
      _showSnackBar('Get camera preview size failed');
      return;
    }
    showDialog(
      context: context,
      builder: (ctx) => SimpleDialog(
        title: const Text('Resolution'),
        children: _previewSizes.map((size) {
          return SimpleDialogOption(
            onPressed: () {
              Navigator.pop(ctx);
              _controller.updateResolution(size.width, size.height);
            },
            child: Text('${size.width} x ${size.height}'),
          );
        }).toList(),
      ),
    );
  }

  void _showMoreMenu() {
    showDialog(
      context: context,
      builder: (ctx) => SimpleDialog(
        title: const Text('More'),
        children: [
          SimpleDialogOption(
            onPressed: () {
              Navigator.pop(ctx);
              Navigator.push(context, MaterialPageRoute(builder: (_) => const MultiCameraPage()));
            },
            child: const Text('Multi Camera'),
          ),
          SimpleDialogOption(
            onPressed: () {
              Navigator.pop(ctx);
              _controller.setRotateType(RotateType.angle0);
            },
            child: const Text('Rotate 0°'),
          ),
          SimpleDialogOption(
            onPressed: () {
              Navigator.pop(ctx);
              _controller.setRotateType(RotateType.angle90);
            },
            child: const Text('Rotate 90°'),
          ),
          SimpleDialogOption(
            onPressed: () {
              Navigator.pop(ctx);
              _controller.setRotateType(RotateType.angle180);
            },
            child: const Text('Rotate 180°'),
          ),
          SimpleDialogOption(
            onPressed: () {
              Navigator.pop(ctx);
              _controller.setRotateType(RotateType.angle270);
            },
            child: const Text('Rotate 270°'),
          ),
        ],
      ),
    );
  }
}


// ============================================================
// MultiCameraPage — matches DemoMultiCameraFragment / layout_item_camera.xml
// ============================================================
class MultiCameraPage extends StatefulWidget {
  const MultiCameraPage({super.key});

  @override
  State<MultiCameraPage> createState() => _MultiCameraPageState();
}

class _MultiCameraPageState extends State<MultiCameraPage> {
  final _controller = MultiCameraController();
  StreamSubscription<CameraEvent>? _eventSub;
  final Map<int, int> _cameraTextures = {};
  final Map<int, String> _cameraNames = {};
  final Map<int, bool> _cameraOpened = {};
  final Map<int, bool> _cameraRecording = {};

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

  Future<void> _init() async {
    _eventSub = _controller.events.listen(_handleEvent);
    await _controller.registerUsb();
  }

  void _handleEvent(CameraEvent event) {
    switch (event.type) {
      case CameraEventType.onAttachDev:
        final deviceId = event.deviceId!;
        setState(() {
          _cameraNames[deviceId] = event.raw['deviceName'] as String? ?? '';
          _cameraOpened[deviceId] = false;
          _cameraRecording[deviceId] = false;
        });
        _controller.requestPermission(deviceId);
        break;
      case CameraEventType.onDetachDev:
        final deviceId = event.deviceId!;
        setState(() {
          _cameraTextures.remove(deviceId);
          _cameraNames.remove(deviceId);
          _cameraOpened.remove(deviceId);
          _cameraRecording.remove(deviceId);
        });
        break;
      case CameraEventType.onConnectDev:
        _openMultiCamera(event.deviceId!);
        break;
      case CameraEventType.onMultiCameraState:
        final deviceId = event.deviceId!;
        if (event.state == 'opened') {
          setState(() => _cameraOpened[deviceId] = true);
        } else if (event.state == 'closed' || event.state == 'error') {
          setState(() => _cameraOpened[deviceId] = false);
        }
        break;
      case CameraEventType.onCameraState:
        final deviceId = event.deviceId;
        if (deviceId != null && event.state == 'opened') {
          setState(() => _cameraOpened[deviceId] = true);
        }
        break;
      case CameraEventType.onCaptureBegin:
        final deviceId = event.deviceId;
        if (deviceId != null && event.captureType == 'video') {
          setState(() => _cameraRecording[deviceId] = true);
        }
        break;
      case CameraEventType.onCaptureComplete:
        final deviceId = event.deviceId;
        if (deviceId != null && event.captureType == 'video') {
          setState(() => _cameraRecording[deviceId] = false);
        }
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Saved: ${event.path}')),
          );
        }
        break;
      case CameraEventType.onCaptureError:
        final deviceId = event.deviceId;
        if (deviceId != null) setState(() => _cameraRecording[deviceId] = false);
        break;
      default:
        break;
    }
  }

  Future<void> _openMultiCamera(int deviceId) async {
    // Multi-camera with PlatformView requires separate views per camera
    // For now, use the single camera approach
    setState(() => _cameraTextures[deviceId] = 0);
  }

  @override
  void dispose() {
    _eventSub?.cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final deviceIds = _cameraTextures.keys.toList();
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.black,
        title: const Text('Multi Camera'),
      ),
      body: deviceIds.isEmpty
          ? Center(
              child: Text(
                'NO UVC CAMERAS',
                style: TextStyle(
                  color: Colors.grey,
                  fontSize: 28,
                  fontWeight: FontWeight.bold,
                ),
              ),
            )
          : ListView.builder(
              itemCount: deviceIds.length,
              itemBuilder: (context, index) {
                final deviceId = deviceIds[index];
                return _buildCameraItem(deviceId);
              },
            ),
    );
  }

  // Matches layout_item_camera.xml (300dp height, rounded bg, texture + controls)
  Widget _buildCameraItem(int deviceId) {
    final textureId = _cameraTextures[deviceId] ?? -1;
    final name = _cameraNames[deviceId] ?? '';
    final isOpened = _cameraOpened[deviceId] ?? false;
    final isRecording = _cameraRecording[deviceId] ?? false;

    return Container(
      height: 300,
      margin: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: const Color(0xFF1A1A1A),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Stack(
        children: [
          // Camera preview
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: textureId >= 0
                  ? const UsbCameraPreview()
                  : const Center(
                      child: Icon(Icons.videocam_off, color: Colors.white24, size: 48),
                    ),
            ),
          ),
          // Camera name (top-left)
          Positioned(
            top: 10,
            left: 10,
            child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 12)),
          ),
          // Switch icon (top-right)
          Positioned(
            top: 10,
            right: 10,
            child: Icon(
              isOpened ? Icons.toggle_on : Icons.toggle_off,
              color: isOpened ? Colors.green : Colors.grey,
              size: 32,
            ),
          ),
          // Capture controls (bottom-left)
          Positioned(
            bottom: 10,
            left: 20,
            child: Row(
              children: [
                GestureDetector(
                  onTap: isOpened ? () => _controller.captureImage(deviceId) : null,
                  child: Icon(Icons.camera_alt, color: isOpened ? Colors.white : Colors.white24, size: 22),
                ),
                const SizedBox(width: 10),
                GestureDetector(
                  onTap: isOpened
                      ? () {
                          if (isRecording) {
                            _controller.captureVideoStop(deviceId);
                          } else {
                            _controller.captureVideoStart(deviceId);
                          }
                        }
                      : null,
                  child: Icon(
                    isRecording ? Icons.stop_circle : Icons.videocam,
                    color: isRecording ? Colors.red : (isOpened ? Colors.white : Colors.white24),
                    size: 22,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
155
points
36
downloads

Documentation

API reference

Publisher

verified publisherimin.sg

Weekly Downloads

Flutter plugin for Android UVC (USB Video Class) camera preview, capture, recording and OpenGL rendering. Based on AndroidUSBCamera (AUSBC).

Repository (GitHub)
View/report issues

Topics

#camera #usb #uvc #android #hardware

License

BSD-3-Clause (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_usbcamera

Packages that implement flutter_usbcamera