diff --git a/example/aurora/desktop/ru.auroraos.flutter_example_packages.desktop b/example/aurora/desktop/ru.auroraos.flutter_example_packages.desktop index 4ce86e9..0b6e115 100644 --- a/example/aurora/desktop/ru.auroraos.flutter_example_packages.desktop +++ b/example/aurora/desktop/ru.auroraos.flutter_example_packages.desktop @@ -7,6 +7,6 @@ Exec=/usr/bin/ru.auroraos.flutter_example_packages X-Nemo-Application-Type=silica-qt5 [X-Application] -Permissions=DeviceInfo;UserDirs;Sensors +Permissions=DeviceInfo;UserDirs;Sensors;Camera OrganizationName=ru.auroraos ApplicationName=flutter_example_packages diff --git a/example/lib/packages/camera/extension/camera_controller.dart b/example/lib/packages/camera/extension/camera_controller.dart new file mode 100644 index 0000000..9dd8b32 --- /dev/null +++ b/example/lib/packages/camera/extension/camera_controller.dart @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image/image.dart' as img; +import 'package:path_provider/path_provider.dart' as provider; +import 'package:path_provider/path_provider.dart'; + +import 'uint8_list.dart'; + +extension ExtCameraController on CameraController { + /// Get photo + Future takeImageFile() async { + if (!value.isInitialized) { + return null; + } + if (value.isTakingPicture) { + return null; + } + try { + // Get image + final picture = await takePicture(); + // Get bytes + final bytes = await picture.readAsBytes(); + // Get path + final directory = await provider.getExternalStorageDirectories( + type: StorageDirectory.pictures); + // Save to file + final file = await bytes.writeToFile(directory![0], picture); + // Return saved file + return file; + } on CameraException catch (e) { + debugPrint(e.description); + return null; + } + } + + /// Get video + Future takeVideoFileAndStopRecording() async { + if (!value.isInitialized) { + return null; + } + if (!value.isRecordingVideo) { + return null; + } + try { + // Stop recording + final video = await stopVideoRecording(); + // Get bytes + final bytes = await video.readAsBytes(); + // Get path + final directory = await provider.getApplicationDocumentsDirectory(); + // Save to file + final file = await bytes.writeToFile(directory, video); + // Return saved file + return file; + } on CameraException catch (e) { + debugPrint(e.description); + return null; + } + } +} diff --git a/example/lib/packages/camera/extension/camera_description.dart b/example/lib/packages/camera/extension/camera_description.dart new file mode 100644 index 0000000..c79a880 --- /dev/null +++ b/example/lib/packages/camera/extension/camera_description.dart @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +extension ExtCameraDescription on CameraDescription { + /// Get [CameraController] + Future getCameraController() async { + final cameraController = CameraController( + this, + ResolutionPreset.medium, + enableAudio: true, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + try { + await cameraController.initialize(); + return cameraController; + } on CameraException catch (e) { + debugPrint(e.description); + return null; + } + } + + /// Get Icon by direction + IconData getIcon() { + switch (lensDirection) { + case CameraLensDirection.back: + return Icons.camera_rear; + case CameraLensDirection.front: + return Icons.camera_front; + case CameraLensDirection.external: + return Icons.camera; + } + } +} diff --git a/example/lib/packages/camera/extension/export.dart b/example/lib/packages/camera/extension/export.dart new file mode 100644 index 0000000..3d16a2d --- /dev/null +++ b/example/lib/packages/camera/extension/export.dart @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +library camera; + +export './camera_controller.dart'; +export './camera_description.dart'; +export './uint8_list.dart'; diff --git a/example/lib/packages/camera/extension/uint8_list.dart b/example/lib/packages/camera/extension/uint8_list.dart new file mode 100644 index 0000000..f847ce8 --- /dev/null +++ b/example/lib/packages/camera/extension/uint8_list.dart @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:camera/camera.dart'; +import 'package:path/path.dart' as p; + +extension ExtUint8List on Uint8List { + Future writeToFile(Directory directory, XFile file) { + if (file.name.isEmpty) { + /// @todo XFile.fromData not work name file + return File(p.join(directory.path, + '${DateTime.now().millisecondsSinceEpoch}.${file.mimeType == 'video/mp4' ? 'mp4' : 'jpg'}')) + .writeAsBytes(this); + } else { + return File(p.join(directory.path, file.name)).writeAsBytes(this); + } + } +} diff --git a/example/lib/packages/camera/model.dart b/example/lib/packages/camera/model.dart new file mode 100644 index 0000000..912eede --- /dev/null +++ b/example/lib/packages/camera/model.dart @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'package:flutter/widgets.dart'; +import 'package:scoped_model/scoped_model.dart'; + +/// Model for [CameraPage] +class CameraModel extends Model { + /// Get [ScopedModel] + static CameraModel of(BuildContext context) => + ScopedModel.of(context); +} diff --git a/example/lib/packages/camera/package.dart b/example/lib/packages/camera/package.dart new file mode 100644 index 0000000..3e5ba48 --- /dev/null +++ b/example/lib/packages/camera/package.dart @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'package:flutter_example_packages/base/package/package_page.dart'; +import 'package:get_it/get_it.dart'; + +import 'model.dart'; +import 'page.dart'; + +/// Package values +final packageCamera = PackagePage( + key: 'camera', + descEN: ''' + A Flutter plugin for Aurora, iOS, Android and Web allowing access + to the device cameras. + ''', + descRU: ''' + Плагин Flutter для Aurora, iOS, Android и Интернета, обеспечивающий доступ + к камерам устройства. + ''', + version: '0.10.5+2', + isPlatformDependent: true, + page: () => CameraPage(), + init: () { + GetIt.instance.registerFactory(() => CameraModel()); + }, +); diff --git a/example/lib/packages/camera/page.dart b/example/lib/packages/camera/page.dart new file mode 100644 index 0000000..5dc4842 --- /dev/null +++ b/example/lib/packages/camera/page.dart @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_example_packages/base/di/app_di.dart'; +import 'package:flutter_example_packages/base/package/package.dart'; +import 'package:flutter_example_packages/packages/camera/extension/export.dart'; +import 'package:flutter_example_packages/packages/camera/widgets/camera_body.dart'; +import 'package:flutter_example_packages/packages/camera/widgets/camera_control_panel.dart'; +import 'package:flutter_example_packages/packages/camera/widgets/cameras_loading.dart'; +import 'package:flutter_example_packages/packages/camera/widgets/cameras_select.dart'; +import 'package:flutter_example_packages/theme/colors.dart'; +import 'package:flutter_example_packages/widgets/base/export.dart'; +import 'package:flutter_example_packages/widgets/layouts/block_layout.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'model.dart'; +import 'package.dart'; + +class CameraPage extends AppStatefulWidget { + CameraPage({ + super.key, + }); + + final Package package = packageCamera; + + @override + State createState() => _CameraPageState(); +} + +class _CameraPageState extends AppState { + CameraController? _cameraController; + File? _photo; + File? _video; + bool _loading = false; + + @override + Widget buildWide( + BuildContext context, + MediaQueryData media, + AppLocalizations l10n, + ) { + return BlockLayout( + model: getIt(), + title: widget.package.key, + builder: (context, child, model) { + return Padding( + padding: const EdgeInsets.all(20), + child: CamerasLoading( + package: widget.package, + builder: (context, cameras) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 1, + child: Column( + children: [ + Flexible( + flex: 0, + child: CamerasSelect( + disable: _loading, + cameras: cameras, + onChange: (controller) => setState(() { + _photo = null; + _cameraController = controller; + }), + ), + ), + const SizedBox(height: 5), + Flexible( + flex: 1, + child: CameraBody( + loading: _loading, + controller: _cameraController, + photo: _photo, + ), + ), + CameraControlPanel( + disable: _loading, + controller: _cameraController, + photo: _photo, + // Start record video + onRecordingStart: () => _cameraController + ?.startVideoRecording() + .then((picture) { + if (mounted) { + setState(() {}); + } + }), + // Pause record video if record already start + onRecordingPause: () => _cameraController + ?.pauseVideoRecording() + .then((value) { + if (mounted) { + setState(() {}); + } + }), + // Resume record video if record already start and will pause + onRecordingResume: () => _cameraController + ?.resumeVideoRecording() + .then((value) { + if (mounted) { + setState(() {}); + } + }), + // Clear photo + onClearPhoto: () { + if (mounted) { + setState(() { + _photo = null; + }); + } + }, + // Stop record video and save to file (custom extension) + onRecordingStop: () => _cameraController + ?.takeVideoFileAndStopRecording() + .then((video) { + if (mounted) { + showMessage(video); + setState(() { + _video = video; + }); + } + }), + // Take photo and save to file (custom extension) + onTakePhoto: () => setState(() { + _loading = true; + _cameraController?.takeImageFile().then((photo) { + if (mounted) { + showMessage(photo); + setState(() { + _loading = false; + _photo = photo; + }); + } + }); + }), + ), + ], + ), + ), + ], + ); + }, + ), + ); + }, + ); + } + + void showMessage(File? file) { + if (file != null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("File save to: ${file.path}"), + backgroundColor: AppColors.secondary, + )); + } + } +} diff --git a/example/lib/packages/camera/widgets/camera_body.dart b/example/lib/packages/camera/widgets/camera_body.dart new file mode 100644 index 0000000..b5038e3 --- /dev/null +++ b/example/lib/packages/camera/widgets/camera_body.dart @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_example_packages/theme/radius.dart'; +import 'package:flutter_example_packages/widgets/base/export.dart'; +import 'package:flutter_example_packages/widgets/texts/export.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CameraBody extends AppStatefulWidget { + const CameraBody({ + super.key, + required this.loading, + required this.controller, + required this.photo, + }); + + final bool loading; + final CameraController? controller; + final File? photo; + + @override + State createState() => _CameraBodyState(); +} + +class _CameraBodyState extends AppState { + @override + Widget buildWide( + BuildContext context, + MediaQueryData media, + AppLocalizations l10n, + ) { + return ClipRRect( + borderRadius: AppRadius.small, + child: Container( + color: Colors.black87, + child: Stack( + children: [ + if (widget.loading) + const Center( + child: TextTitleLarge( + 'Loading...', + color: Colors.white, + ), + ), + // Show info if not select camera + if (!widget.loading && + widget.controller == null && + widget.photo == null) + const Center( + child: TextTitleLarge( + 'Select camera', + color: Colors.white, + ), + ), + + // Show camera preview + if (!widget.loading && + widget.controller != null && + widget.controller!.value.isInitialized && + widget.photo == null) + Container( + width: double.infinity, + height: double.infinity, + alignment: Alignment.center, + child: CameraPreview(widget.controller!), + ), + + // Show dot when recording is active + if (!widget.loading && + (widget.controller?.value.isRecordingVideo ?? false) && + !(widget.controller?.value.isRecordingPaused ?? false)) + Padding( + padding: const EdgeInsets.all(16), + child: ClipOval( + child: Container( + color: Colors.red, + width: 16, + height: 16, + ), + ), + ), + + // Show take phone + if (!widget.loading && widget.photo != null) + Container( + width: double.infinity, + height: double.infinity, + alignment: Alignment.center, + child: RotationTransition( + turns: const AlwaysStoppedAnimation(90 / 360), + child: Image.file( + widget.photo!, + fit: BoxFit.fill, + filterQuality: FilterQuality.high, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/packages/camera/widgets/camera_control_panel.dart b/example/lib/packages/camera/widgets/camera_control_panel.dart new file mode 100644 index 0000000..9ec4fc7 --- /dev/null +++ b/example/lib/packages/camera/widgets/camera_control_panel.dart @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_example_packages/theme/colors.dart'; +import 'package:flutter_example_packages/widgets/base/export.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CameraControlPanel extends AppStatefulWidget { + const CameraControlPanel({ + super.key, + required this.disable, + required this.controller, + required this.photo, + required this.onRecordingStart, + required this.onRecordingStop, + required this.onRecordingPause, + required this.onRecordingResume, + required this.onTakePhoto, + required this.onClearPhoto, + }); + + final bool disable; + final CameraController? controller; + final File? photo; + final void Function() onRecordingStart; + final void Function() onRecordingStop; + final void Function() onRecordingPause; + final void Function() onRecordingResume; + final void Function() onTakePhoto; + final void Function() onClearPhoto; + + @override + State createState() => _CameraControlPanelState(); +} + +class _CameraControlPanelState extends AppState { + @override + Widget buildWide( + BuildContext context, + MediaQueryData media, + AppLocalizations l10n, + ) { + final isPhoto = widget.photo != null; + final isRecordingVideo = widget.controller?.value.isRecordingVideo ?? false; + final isRecordingPaused = + widget.controller?.value.isRecordingPaused ?? false; + + return Visibility( + visible: widget.controller != null, + child: Column( + children: [ + const SizedBox(height: 10), + Row( + children: [ + ClipOval( + child: Material( + child: IconButton( + icon: Icon( + isRecordingVideo ? Icons.stop_circle : Icons.videocam, + color: AppColors.primary + .withOpacity(isPhoto || widget.disable ? 0.5 : 1), + ), + onPressed: isPhoto || widget.disable + ? null + : () { + if (isRecordingVideo) { + widget.onRecordingStop.call(); + } else { + widget.onRecordingStart.call(); + } + }, + ), + ), + ), + if (isRecordingVideo) + ClipOval( + child: Material( + child: IconButton( + icon: Icon( + isRecordingPaused + ? Icons.play_circle + : Icons.pause_circle, + color: AppColors.primary, + ), + onPressed: () { + if (isRecordingPaused) { + widget.onRecordingResume.call(); + } else { + widget.onRecordingPause.call(); + } + }, + ), + ), + ), + const Spacer(), + ClipOval( + child: Material( + child: IconButton( + icon: Icon( + isPhoto ? Icons.image_not_supported : Icons.photo_camera, + color: AppColors.primary.withOpacity( + isRecordingVideo || widget.disable ? 0.5 : 1), + ), + onPressed: isRecordingVideo || widget.disable + ? null + : () { + if (isPhoto) { + widget.onClearPhoto.call(); + } else { + widget.onTakePhoto.call(); + } + }, + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/example/lib/packages/camera/widgets/cameras_loading.dart b/example/lib/packages/camera/widgets/cameras_loading.dart new file mode 100644 index 0000000..896b13f --- /dev/null +++ b/example/lib/packages/camera/widgets/cameras_loading.dart @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_example_packages/base/package/package.dart'; +import 'package:flutter_example_packages/widgets/base/export.dart'; +import 'package:flutter_example_packages/widgets/blocks/block_alert.dart'; +import 'package:flutter_example_packages/widgets/blocks/block_info_package.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CamerasLoading extends AppStatefulWidget { + const CamerasLoading({ + super.key, + required this.package, + required this.builder, + }); + + final Package package; + final Widget Function(BuildContext context, List cameras) + builder; + + @override + State createState() => _CamerasLoadingState(); +} + +class _CamerasLoadingState extends AppState { + List? _cameras; + String? _error; + + @override + void initState() { + super.initState(); + // Get list cameras + try { + availableCameras().then((cameras) { + if (mounted) { + setState(() { + _cameras = cameras; + }); + } + }).catchError((e) { + setState(() { + _error = e.toString(); + }); + }); + } catch (e) { + setState(() { + _error = e.toString(); + }); + } + } + + @override + Widget buildWide( + BuildContext context, + MediaQueryData media, + AppLocalizations l10n, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 0, + child: Column( + children: [ + BlockInfoPackage( + widget.package, + ), + ], + ), + ), + Visibility( + visible: _cameras == null && _error == null, + child: const Flexible( + flex: 1, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ), + Visibility( + visible: _cameras?.isEmpty ?? false || _error != null, + child: Flexible( + flex: 0, + child: BlockAlert(_error ?? 'No camera found.'), + ), + ), + if (_cameras?.isNotEmpty ?? false) + Flexible( + flex: 1, + child: widget.builder.call(context, _cameras!), + ), + ], + ); + } +} diff --git a/example/lib/packages/camera/widgets/cameras_select.dart b/example/lib/packages/camera/widgets/cameras_select.dart new file mode 100644 index 0000000..45e84f5 --- /dev/null +++ b/example/lib/packages/camera/widgets/cameras_select.dart @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_example_packages/packages/camera/extension/export.dart'; +import 'package:flutter_example_packages/theme/colors.dart'; +import 'package:flutter_example_packages/widgets/base/export.dart'; +import 'package:flutter_example_packages/widgets/texts/export.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CamerasSelect extends AppStatefulWidget { + const CamerasSelect({ + super.key, + required this.disable, + required this.cameras, + required this.onChange, + }); + + final bool disable; + final List cameras; + final void Function(CameraController controller) onChange; + + @override + State createState() => _CamerasSelectState(); +} + +class _CamerasSelectState extends AppState { + CameraController? _cameraController; + + Future initCamera(CameraDescription? camera) async { + if (camera != null) { + if (_cameraController != null) { + // Check and stop if need image stream + if (_cameraController!.value.isStreamingImages) { + await _cameraController!.stopImageStream(); + } + // Check and stop if need video recording + if (_cameraController!.value.isRecordingVideo) { + await _cameraController!.stopVideoRecording(); + } + // Change camera + await _cameraController!.setDescription(camera); + } else { + _cameraController = await camera.getCameraController(); + } + // Send signal about change camera + if (mounted) { + widget.onChange(_cameraController!); + } + } + } + + @override + Widget buildWide( + BuildContext context, + MediaQueryData media, + AppLocalizations l10n, + ) { + return Row( + children: [ + const TextTitleLarge('Cameras:'), + const SizedBox(width: 8), + for (final CameraDescription camera in widget.cameras) + ClipOval( + child: Material( + child: IconButton( + icon: Icon( + camera.getIcon(), + color: _cameraController?.description == camera + ? AppColors.secondary + : AppColors.primary, + ), + onPressed: + _cameraController?.description == camera || widget.disable + ? null + : () => initCamera(camera), + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/packages/packages.dart b/example/lib/packages/packages.dart index 3e56812..2e24efb 100644 --- a/example/lib/packages/packages.dart +++ b/example/lib/packages/packages.dart @@ -4,6 +4,7 @@ import 'package:flutter_example_packages/base/package/package.dart'; import 'package:flutter_example_packages/packages/battery_plus/package.dart'; import 'package:flutter_example_packages/packages/build_runner/package.dart'; import 'package:flutter_example_packages/packages/cached_network_image/package.dart'; +import 'package:flutter_example_packages/packages/camera/package.dart'; import 'package:flutter_example_packages/packages/crypto/package.dart'; import 'package:flutter_example_packages/packages/cupertino_icons/package.dart'; import 'package:flutter_example_packages/packages/dartz/package.dart'; @@ -41,6 +42,7 @@ final packages = [ packageBatteryPlus, packageBuildRunner, packageCachedNetworkImage, + packageCamera, packageCrypto, packageCupertinoIcons, packageDartz, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f2b8800..96593b3 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -173,6 +173,18 @@ dependencies: # ref: master # path: packages/sensors_plus/sensors_plus_aurora + ## https://pub.dev/packages/camera + camera: ^0.10.5+5 + ## https://pub.dev/packages/camera_android + camera_android: ^0.10.8+13 + ## https://os-git.omprussia.ru/non-oss/flutter/flutter-plugins/-/tree/master/packages/camera/camera_aurora + camera_aurora: + path: ../packages/camera/camera_aurora +# git: +# url: https://gitlab.com/omprussia/flutter/flutter-plugins.git +# ref: master +# path: packages/camera/camera_aurora + dev_dependencies: flutter_test: sdk: diff --git a/packages/camera/camera_aurora/.gitignore b/packages/camera/camera_aurora/.gitignore new file mode 100644 index 0000000..d920ae6 --- /dev/null +++ b/packages/camera/camera_aurora/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/camera/camera_aurora/README.md b/packages/camera/camera_aurora/README.md new file mode 100644 index 0000000..a3d2449 --- /dev/null +++ b/packages/camera/camera_aurora/README.md @@ -0,0 +1,30 @@ +# camera_aurora + +The Aurora implementation of [camera](https://pub.dev/packages/camera). + +## Usage +This package is not an _endorsed_ implementation of `camera`. +Therefore, you have to include `camera_aurora` alongside `camera` as dependencies in your `pubspec.yaml` file. + +***.desktop** + +```desktop +Permissions=Sensors +``` +***.spec** + +```spec +BuildRequires: pkgconfig(streamcamera) +``` + +**pubspec.yaml** + +```yaml +dependencies: + camera: ^0.10.5+5 + camera_aurora: + git: + url: https://gitlab.com/omprussia/flutter/flutter-plugins.git + ref: master + path: packages/device_info_plus/device_info_plus_aurora +``` diff --git a/packages/camera/camera_aurora/analysis_options.yaml b/packages/camera/camera_aurora/analysis_options.yaml new file mode 100644 index 0000000..8a1fdc6 --- /dev/null +++ b/packages/camera/camera_aurora/analysis_options.yaml @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +# SPDX-License-Identifier: BSD-3-Clause + +include: package:flutter_lints/flutter.yaml diff --git a/packages/camera/camera_aurora/aurora/CMakeLists.txt b/packages/camera/camera_aurora/aurora/CMakeLists.txt new file mode 100644 index 0000000..8a65b15 --- /dev/null +++ b/packages/camera/camera_aurora/aurora/CMakeLists.txt @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.10) + +set(PROJECT_NAME camera_aurora) +set(PLUGIN_NAME camera_aurora_platform_plugin) + +project(${PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wno-psabi") +set(CMAKE_CXX_FLAGS_RELEASE "-O3") + +find_package(PkgConfig REQUIRED) +find_package(Qt5 COMPONENTS Core Multimedia REQUIRED) +pkg_check_modules(StreamCamera REQUIRED IMPORTED_TARGET streamcamera) +pkg_check_modules(FlutterEmbedder REQUIRED IMPORTED_TARGET flutter-embedder) + +add_library(${PLUGIN_NAME} SHARED + camera_aurora_plugin.cpp +) + +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden AUTOMOC ON) + +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::FlutterEmbedder) +target_link_libraries(${PLUGIN_NAME} PUBLIC PkgConfig::StreamCamera) +target_link_libraries(${PLUGIN_NAME} PUBLIC Qt5::Core Qt5::Multimedia) + +target_include_directories(${PLUGIN_NAME} PRIVATE ${FLUTTER_DIR}) +target_include_directories(${PLUGIN_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(${PLUGIN_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include/${PROJECT_NAME}) + +target_compile_definitions(${PLUGIN_NAME} PRIVATE PLUGIN_IMPL) diff --git a/packages/camera/camera_aurora/aurora/camera_aurora_plugin.cpp b/packages/camera/camera_aurora/aurora/camera_aurora_plugin.cpp new file mode 100644 index 0000000..1f91bf5 --- /dev/null +++ b/packages/camera/camera_aurora/aurora/camera_aurora_plugin.cpp @@ -0,0 +1,273 @@ +/* + * SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace CameraAuroraMethods +{ + constexpr auto PluginKey = "camera_aurora"; + + constexpr auto AvailableCameras = "availableCameras"; + constexpr auto CreateCamera = "createCamera"; + constexpr auto Dispose = "dispose"; + constexpr auto InitializeCamera = "initializeCamera"; + constexpr auto TakePicture = "takePicture"; + constexpr auto StartVideoRecording = "startVideoRecording"; + constexpr auto StopVideoRecording = "stopVideoRecording"; + constexpr auto PauseVideoRecording = "pauseVideoRecording"; + constexpr auto ResumeVideoRecording = "resumeVideoRecording"; +} + +namespace CameraAuroraEvents +{ + constexpr auto ReadyForCapture = "cameraAuroraReadyForCapture"; + constexpr auto ImageSaved = "cameraAuroraImageSaved"; + constexpr auto StreamedFrame = "cameraAuroraStreamedFrame"; +} + +void CameraAuroraPlugin::RegisterWithRegistrar(PluginRegistrar ®istrar) +{ + RegisterMethods(registrar); + RegisterEvents(registrar); +} + +void CameraAuroraPlugin::RegisterMethods(PluginRegistrar ®istrar) +{ + auto methods = [this](const MethodCall &call) + { + const auto &method = call.GetMethod(); + + if (method == CameraAuroraMethods::AvailableCameras) + { + onAvailableCameras(call); + return; + } + + if (method == CameraAuroraMethods::CreateCamera) + { + onCreateCamera(call); + return; + } + + if (method == CameraAuroraMethods::Dispose) + { + onDispose(call); + return; + } + + if (method == CameraAuroraMethods::InitializeCamera) + { + onInitializeCamera(call); + return; + } + + if (method == CameraAuroraMethods::TakePicture) + { + onTakePicture(call); + return; + } + + if (method == CameraAuroraMethods::StartVideoRecording) + { + onStartVideoRecording(call); + return; + } + + if (method == CameraAuroraMethods::StopVideoRecording) + { + onStopVideoRecording(call); + return; + } + + if (method == CameraAuroraMethods::PauseVideoRecording) + { + onPauseVideoRecording(call); + return; + } + + if (method == CameraAuroraMethods::ResumeVideoRecording) + { + onResumeVideoRecording(call); + return; + } + + unimplemented(call); + }; + + registrar.RegisterMethodChannel( + CameraAuroraMethods::PluginKey, + MethodCodecType::Standard, + methods); +} + +void CameraAuroraPlugin::RegisterEvents(PluginRegistrar ®istrar) +{ + registrar.RegisterEventChannel( + CameraAuroraEvents::ReadyForCapture, MethodCodecType::Standard, + [this](const Encodable &) + { return EventResponse(); }, + [this](const Encodable &) + { return EventResponse(); }); + + registrar.RegisterEventChannel( + CameraAuroraEvents::ImageSaved, MethodCodecType::Standard, + [this](const Encodable &) + { return EventResponse(); }, + [this](const Encodable &) + { return EventResponse(); }); + + registrar.RegisterEventChannel( + CameraAuroraEvents::StreamedFrame, MethodCodecType::Standard, + [this](const Encodable &) + { return EventResponse(); }, + [this](const Encodable &) + { return EventResponse(); }); +} + +/** + * Methods + */ + +void CameraAuroraPlugin::onAvailableCameras(const MethodCall &call) +{ + std::vector list; + + const QList cameras = QCameraInfo::availableCameras(); + + for (const QCameraInfo &cameraInfo : cameras) + { + list.push_back(std::map{ + {"deviceName", cameraInfo.deviceName().toStdString()}, + {"position", static_cast(cameraInfo.position())}, + {"orientation", cameraInfo.orientation()}, + }); + } + + call.SendSuccessResponse(list); +} + +void CameraAuroraPlugin::onCreateCamera(const MethodCall &call) +{ + QCameraInfo cameraInfo; + + const auto cameraName = call.GetArgument("cameraName"); + + qDebug() << "onCreateCamera"; + + for (const QCameraInfo &item : QCameraInfo::availableCameras()) + { + if (item.deviceName().toStdString() == cameraName) + { + cameraInfo = item; + break; + } + } + + m_camera.reset(new QCamera(cameraInfo)); + m_camera->setCaptureMode(QCamera::CaptureStillImage); + m_camera->start(); + + m_imageCapture.reset(new QCameraImageCapture(m_camera.data())); + + connect(m_imageCapture.data(), &QCameraImageCapture::readyForCaptureChanged, this, &CameraAuroraPlugin::readyForCapture); + connect(m_imageCapture.data(), &QCameraImageCapture::imageSaved, this, &CameraAuroraPlugin::imageSaved); + + call.SendSuccessResponse(stoi(cameraName)); + + qDebug() << "onCreateCamera"; +} + +void CameraAuroraPlugin::onDispose(const MethodCall &call) +{ + const auto cameraId = call.GetArgument("cameraId"); + + if (m_camera) { + qDebug() << "onDispose"; + } + + unimplemented(call); +} + +void CameraAuroraPlugin::onInitializeCamera(const MethodCall &call) +{ + qDebug() << "onInitializeCamera"; + unimplemented(call); +} + +void CameraAuroraPlugin::onTakePicture(const MethodCall &call) +{ + if (m_imageCapture->isReadyForCapture()) { + m_imageCapture->capture(); + call.SendSuccessResponse(true); + } else { + call.SendSuccessResponse(false); + } +} + +void CameraAuroraPlugin::onStartVideoRecording(const MethodCall &call) +{ + qDebug() << "onStartVideoRecording"; + unimplemented(call); +} + +void CameraAuroraPlugin::onStopVideoRecording(const MethodCall &call) +{ + qDebug() << "onStopVideoRecording"; + unimplemented(call); +} + +void CameraAuroraPlugin::onPauseVideoRecording(const MethodCall &call) +{ + qDebug() << "onPauseVideoRecording"; + unimplemented(call); +} + +void CameraAuroraPlugin::onResumeVideoRecording(const MethodCall &call) +{ + qDebug() << "onResumeVideoRecording"; + unimplemented(call); +} + +/** + * Slots + */ +void CameraAuroraPlugin::readyForCapture(bool ready) +{ + qDebug() << "readyForCapture"; + + EventChannel( + CameraAuroraEvents::ReadyForCapture, + MethodCodecType::Standard) + .SendEvent(ready); +} + +void CameraAuroraPlugin::imageSaved(int id, const QString &fileName) +{ + qDebug() << "imageSaved"; + + EventChannel( + CameraAuroraEvents::ImageSaved, + MethodCodecType::Standard) + .SendEvent(std::vector{ + id, + fileName.toStdString()}); +} + +void CameraAuroraPlugin::unimplemented(const MethodCall &call) +{ + call.SendSuccessResponse(nullptr); +} + +#include "moc_camera_aurora_plugin.cpp" diff --git a/packages/camera/camera_aurora/aurora/include/camera_aurora/camera_aurora_plugin.h b/packages/camera/camera_aurora/aurora/include/camera_aurora/camera_aurora_plugin.h new file mode 100644 index 0000000..51c89a0 --- /dev/null +++ b/packages/camera/camera_aurora/aurora/include/camera_aurora/camera_aurora_plugin.h @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_H +#define FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_H + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +class PLUGIN_EXPORT CameraAuroraPlugin final + : public QObject, + public PluginInterface +{ + Q_OBJECT + +public: + void RegisterWithRegistrar(PluginRegistrar ®istrar) override; + +public slots: + void readyForCapture(bool ready); + void imageSaved(int id, const QString &fileName); + +private: + void RegisterMethods(PluginRegistrar ®istrar); + void RegisterEvents(PluginRegistrar ®istrar); + + void onAvailableCameras(const MethodCall &call); + void onCreateCamera(const MethodCall &call); + void onDispose(const MethodCall &call); + void onInitializeCamera(const MethodCall &call); + void onTakePicture(const MethodCall &call); + void onStartVideoRecording(const MethodCall &call); + void onStopVideoRecording(const MethodCall &call); + void onPauseVideoRecording(const MethodCall &call); + void onResumeVideoRecording(const MethodCall &call); + + void unimplemented(const MethodCall &call); + +private: + QScopedPointer m_camera; + QScopedPointer m_imageCapture; +}; + +#endif /* FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_H */ diff --git a/packages/camera/camera_aurora/aurora/include/camera_aurora/globals.h b/packages/camera/camera_aurora/aurora/include/camera_aurora/globals.h new file mode 100644 index 0000000..7997967 --- /dev/null +++ b/packages/camera/camera_aurora/aurora/include/camera_aurora/globals.h @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_GLOBALS_H +#define FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_GLOBALS_H + +#ifdef PLUGIN_IMPL +#define PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define PLUGIN_EXPORT +#endif + +#endif /* FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_GLOBALS_H */ diff --git a/packages/camera/camera_aurora/lib/camera_aurora.dart b/packages/camera/camera_aurora/lib/camera_aurora.dart new file mode 100644 index 0000000..39367d8 --- /dev/null +++ b/packages/camera/camera_aurora/lib/camera_aurora.dart @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:async'; +import 'dart:convert'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'camera_aurora_method_channel.dart'; +import 'camera_aurora_platform_interface.dart'; + +class CameraAurora extends CameraPlatform { + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith() { + CameraPlatform.instance = CameraAurora(); + } + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + + /// Completes with a list of available cameras. + /// + /// This method returns an empty list when no cameras are available. + @override + Future> availableCameras() => + CameraAuroraPlatform.instance.availableCameras(); + + /// Creates an uninitialized camera instance and returns the cameraId. + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) { + EventChannel(CameraAuroraEvents.cameraAuroraStreamedFrame.name) + .receiveBroadcastStream() + .listen((event) { + debugPrint(event); + }); + return CameraAuroraPlatform.instance.createCamera(cameraDescription.name); + } + + /// Initializes the camera on the device. + /// + /// [imageFormatGroup] is used to specify the image formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. + /// On iOS this defaults to kCVPixelFormatType_32BGRA. + /// On Web this parameter is currently not supported. + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + // init + } + + /// Releases the resources of this camera. + @override + Future dispose(int cameraId) { + return CameraAuroraPlatform.instance.dispose(cameraId); + } + + /// Captures an image and returns the file where it was saved. + @override + Future takePicture(int cameraId) => + CameraAuroraPlatform.instance.takePicture(cameraId); + + /// Starts a video recording. + /// + /// The length of the recording can be limited by specifying the [maxVideoDuration]. + /// By default no maximum duration is specified, + /// meaning the recording will continue until manually stopped. + /// With [maxVideoDuration] set the video is returned in a [VideoRecordedEvent] + /// through the [onVideoRecordedEvent] stream when the set duration is reached. + /// + /// This method is deprecated in favour of [startVideoCapturing]. + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) => + CameraAuroraPlatform.instance.startVideoRecording(cameraId); + + /// Stops the video recording and returns the file where it was saved. + @override + Future stopVideoRecording(int cameraId) => + CameraAuroraPlatform.instance.stopVideoRecording(cameraId); + + /// Pause video recording. + @override + Future pauseVideoRecording(int cameraId) => + CameraAuroraPlatform.instance.pauseVideoRecording(cameraId); + + /// Resume video recording after pausing. + @override + Future resumeVideoRecording(int cameraId) => + CameraAuroraPlatform.instance.resumeVideoRecording(cameraId); + + /// The ui orientation changed. + /// + /// Implementations for this: + /// - Should support all 4 orientations. + @override + Stream onDeviceOrientationChanged() async* { + yield const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + } + + /// The camera has been initialized. + @override + Stream onCameraInitialized(int cameraId) async* { + yield CameraInitializedEvent( + cameraId, + // previewWidth + 400, + // previewHeight + 400, + // exposureMode + ExposureMode.auto, + // exposurePointSupported + true, + // focusMode + FocusMode.auto, + // focusPointSupported + true, + ); + } + + @override + Stream onStreamedFrameAvailable( + int cameraId, { + CameraImageStreamOptions? options, + }) { + _frameStreamController = StreamController( + onListen: () => + CameraAuroraPlatform.instance.streamedFrame(cameraId).listen((data) { + _frameStreamController!.add(data); + }), + onPause: () => {}, + onResume: () => {}, + onCancel: () => {}, + ); + return _frameStreamController!.stream; + } + + /// Returns a widget showing a live camera preview. + @override + Widget buildPreview(int cameraId) { + return Center( + child: Text( + 'Camera: $cameraId', + style: + const TextStyle(fontWeight: FontWeight.bold, color: Colors.white), + ), + ); + } +} diff --git a/packages/camera/camera_aurora/lib/camera_aurora_method_channel.dart b/packages/camera/camera_aurora/lib/camera_aurora_method_channel.dart new file mode 100644 index 0000000..ec4eecf --- /dev/null +++ b/packages/camera/camera_aurora/lib/camera_aurora_method_channel.dart @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'dart:convert'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'camera_aurora_platform_interface.dart'; + +enum CameraAuroraMethods { + availableCameras, + createCamera, + dispose, + initializeCamera, + takePicture, + startVideoRecording, + stopVideoRecording, + pauseVideoRecording, + resumeVideoRecording, +} + +enum CameraAuroraEvents { + cameraAuroraReadyForCapture, + cameraAuroraImageSaved, + cameraAuroraStreamedFrame, +} + +/// An implementation of [CameraAuroraPlatform] that uses method channels. +class MethodChannelCameraAurora extends CameraAuroraPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodsChannel = const MethodChannel('camera_aurora'); + + @override + Future> availableCameras() async { + final List result = []; + + final cameras = await methodsChannel.invokeMethod?>( + CameraAuroraMethods.availableCameras.name) ?? + []; + + for (int i = 0; i < cameras.length; i++) { + final camera = cameras[i] as Map; + final pos = camera['position']; + final lensDirection = pos == 1 + ? CameraLensDirection.back + : (pos == 2 + ? CameraLensDirection.front + : CameraLensDirection.external); + + result.add(CameraDescription( + name: camera['deviceName'], + lensDirection: lensDirection, + sensorOrientation: camera['orientation'], + )); + } + + return result; + } + + @override + Future createCamera(String cameraName) async { + return await methodsChannel + .invokeMethod(CameraAuroraMethods.createCamera.name, { + 'cameraName': cameraName, + }) as int; + } + + @override + Future dispose(int cameraId) { + return methodsChannel + .invokeMethod(CameraAuroraMethods.dispose.name, { + 'cameraId': cameraId, + }); + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + await methodsChannel + .invokeMethod(CameraAuroraMethods.initializeCamera.name, { + 'cameraId': cameraId, + }) as int; + } + + @override + Future takePicture(int cameraId) async { + final result = await methodsChannel.invokeMethod( + CameraAuroraMethods.takePicture.name, + {'cameraId': cameraId}, + ); + + if (result == true) { + await for (final data + in EventChannel(CameraAuroraEvents.cameraAuroraImageSaved.name) + .receiveBroadcastStream()) { + final response = + (data as List).map((e) => e.toString()).toList(); + return XFile(response[1], mimeType: 'image/jpeg'); + } + } + + throw "Error take picture"; + } + + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + await methodsChannel + .invokeMethod(CameraAuroraMethods.startVideoRecording.name, { + 'cameraId': cameraId, + }); + } + + @override + Future stopVideoRecording(int cameraId) async { + await methodsChannel + .invokeMethod(CameraAuroraMethods.stopVideoRecording.name, { + 'cameraId': cameraId, + }); + + // @todo Save empty data + return XFile.fromData( + Uint8List(0), + name: 'file_name.mp4', + + /// @todo not set + mimeType: 'video/mp4', + length: 0, + ); + } + + @override + Future pauseVideoRecording(int cameraId) async { + await methodsChannel + .invokeMethod(CameraAuroraMethods.pauseVideoRecording.name, { + 'cameraId': cameraId, + }); + } + + @override + Future resumeVideoRecording(int cameraId) async { + await methodsChannel + .invokeMethod(CameraAuroraMethods.resumeVideoRecording.name, { + 'cameraId': cameraId, + }); + } + + @override + Stream streamedFrame(int cameraId) async* { + await for (final data + in EventChannel(CameraAuroraEvents.cameraAuroraStreamedFrame.name) + .receiveBroadcastStream()) { + debugPrint(data); + + yield const CameraImageData( + format: CameraImageFormat(ImageFormatGroup.yuv420, raw: 0), + planes: [], + height: 100, + width: 100, + ); + } + } +} diff --git a/packages/camera/camera_aurora/lib/camera_aurora_platform_interface.dart b/packages/camera/camera_aurora/lib/camera_aurora_platform_interface.dart new file mode 100644 index 0000000..9661fda --- /dev/null +++ b/packages/camera/camera_aurora/lib/camera_aurora_platform_interface.dart @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +// SPDX-License-Identifier: BSD-3-Clause +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'camera_aurora_method_channel.dart'; + +abstract class CameraAuroraPlatform extends PlatformInterface { + /// Constructs a CameraAuroraPlatform. + CameraAuroraPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CameraAuroraPlatform _instance = MethodChannelCameraAurora(); + + /// The default instance of [CameraAuroraPlatform] to use. + /// + /// Defaults to [MethodChannelCameraAurora]. + static CameraAuroraPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CameraAuroraPlatform] when + /// they register themselves. + static set instance(CameraAuroraPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future> availableCameras() { + throw UnimplementedError('availableCameras() has not been implemented.'); + } + + Future createCamera(String cameraName) { + throw UnimplementedError('createCamera() has not been implemented.'); + } + + Future dispose(int cameraId) { + throw UnimplementedError('dispose() has not been implemented.'); + } + + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) { + throw UnimplementedError('initializeCamera() has not been implemented.'); + } + + Future takePicture(int cameraId) { + throw UnimplementedError('takePicture() has not been implemented.'); + } + + Future startVideoRecording(int cameraId) { + throw UnimplementedError('startVideoRecording() has not been implemented.'); + } + + Future stopVideoRecording(int cameraId) { + throw UnimplementedError('stopVideoRecording() has not been implemented.'); + } + + Future pauseVideoRecording(int cameraId) { + throw UnimplementedError('pauseVideoRecording() has not been implemented.'); + } + + Future resumeVideoRecording(int cameraId) { + throw UnimplementedError( + 'resumeVideoRecording() has not been implemented.'); + } + + Stream streamedFrame(int cameraId) { + throw UnimplementedError('streamedFrame() has not been implemented.'); + } +} diff --git a/packages/camera/camera_aurora/lib/type_conversion.dart b/packages/camera/camera_aurora/lib/type_conversion.dart new file mode 100644 index 0000000..14cc46a --- /dev/null +++ b/packages/camera/camera_aurora/lib/type_conversion.dart @@ -0,0 +1,43 @@ +import 'dart:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +CameraImageData cameraImageFromPlatformData(Map data) { + return CameraImageData( + format: _cameraImageFormatFromPlatformData(data['format']), + height: data['height'] as int, + width: data['width'] as int, + lensAperture: data['lensAperture'] as double?, + sensorExposureTime: data['sensorExposureTime'] as int?, + sensorSensitivity: data['sensorSensitivity'] as double?, + planes: List.unmodifiable( + (data['planes'] as List).map( + (dynamic planeData) => _cameraImagePlaneFromPlatformData( + planeData as Map)))); +} + +CameraImageFormat _cameraImageFormatFromPlatformData(dynamic data) { + return CameraImageFormat(_imageFormatGroupFromPlatformData(data), raw: data); +} + +ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { + switch (data) { + case 35: // android.graphics.ImageFormat.YUV_420_888 + return ImageFormatGroup.yuv420; + case 256: // android.graphics.ImageFormat.JPEG + return ImageFormatGroup.jpeg; + case 17: // android.graphics.ImageFormat.NV21 + return ImageFormatGroup.nv21; + } + + return ImageFormatGroup.unknown; +} + +CameraImagePlane _cameraImagePlaneFromPlatformData(Map data) { + return CameraImagePlane( + bytes: data['bytes'] as Uint8List, + bytesPerPixel: data['bytesPerPixel'] as int?, + bytesPerRow: data['bytesPerRow'] as int, + height: data['height'] as int?, + width: data['width'] as int?); +} diff --git a/packages/camera/camera_aurora/pubspec.yaml b/packages/camera/camera_aurora/pubspec.yaml new file mode 100644 index 0000000..bdeb638 --- /dev/null +++ b/packages/camera/camera_aurora/pubspec.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC +# SPDX-License-Identifier: BSD-3-Clause + +name: camera_aurora +description: Aurora implementation of the camera plugin. +version: 0.0.1 + +environment: + sdk: '>=2.18.6 <4.0.0' + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + camera_platform_interface: ^2.6.0 + image: ^4.1.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + platforms: + aurora: + pluginClass: CameraAuroraPlugin + dartPluginClass: CameraAurora diff --git a/packages/xdga_directories/lib/xdga_directories.dart b/packages/xdga_directories/lib/xdga_directories.dart index 28df215..c6c453b 100644 --- a/packages/xdga_directories/lib/xdga_directories.dart +++ b/packages/xdga_directories/lib/xdga_directories.dart @@ -13,12 +13,18 @@ final DynamicLibrary _dylib = () { final XdgaDirectoriesBindings _bindings = XdgaDirectoriesBindings(_dylib); /// QStandardPaths::CacheLocation -String getCacheLocation() => - _bindings.getCacheLocation().cast().toDartString(); +String getCacheLocation() => _bindings + .getCacheLocation() + .cast() + .toDartString() + .replaceAll("/qsource", ""); /// QStandardPaths::AppDataLocation -String getAppDataLocation() => - _bindings.getAppDataLocation().cast().toDartString(); +String getAppDataLocation() => _bindings + .getAppDataLocation() + .cast() + .toDartString() + .replaceAll("/qsource", ""); /// QStandardPaths::DocumentsLocation String getDocumentsLocation() =>