Vitaliy Zarubin
1 year ago
27 changed files with 1668 additions and 5 deletions
@ -0,0 +1,64 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<File?> 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<File?> 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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// SPDX-License-Identifier: BSD-3-Clause |
||||||
|
import 'package:camera/camera.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
extension ExtCameraDescription on CameraDescription { |
||||||
|
/// Get [CameraController] |
||||||
|
Future<CameraController?> 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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// SPDX-License-Identifier: BSD-3-Clause |
||||||
|
library camera; |
||||||
|
|
||||||
|
export './camera_controller.dart'; |
||||||
|
export './camera_description.dart'; |
||||||
|
export './uint8_list.dart'; |
@ -0,0 +1,21 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<File> 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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraModel>(context); |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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>(() => CameraModel()); |
||||||
|
}, |
||||||
|
); |
@ -0,0 +1,162 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraPage> createState() => _CameraPageState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _CameraPageState extends AppState<CameraPage> { |
||||||
|
CameraController? _cameraController; |
||||||
|
File? _photo; |
||||||
|
File? _video; |
||||||
|
bool _loading = false; |
||||||
|
|
||||||
|
@override |
||||||
|
Widget buildWide( |
||||||
|
BuildContext context, |
||||||
|
MediaQueryData media, |
||||||
|
AppLocalizations l10n, |
||||||
|
) { |
||||||
|
return BlockLayout<CameraModel>( |
||||||
|
model: getIt<CameraModel>(), |
||||||
|
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, |
||||||
|
)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraBody> createState() => _CameraBodyState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _CameraBodyState extends AppState<CameraBody> { |
||||||
|
@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, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraControlPanel> createState() => _CameraControlPanelState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _CameraControlPanelState extends AppState<CameraControlPanel> { |
||||||
|
@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(); |
||||||
|
} |
||||||
|
}, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraDescription> cameras) |
||||||
|
builder; |
||||||
|
|
||||||
|
@override |
||||||
|
State<CamerasLoading> createState() => _CamerasLoadingState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _CamerasLoadingState extends AppState<CamerasLoading> { |
||||||
|
List<CameraDescription>? _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!), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraDescription> cameras; |
||||||
|
final void Function(CameraController controller) onChange; |
||||||
|
|
||||||
|
@override |
||||||
|
State<CamerasSelect> createState() => _CamerasSelectState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _CamerasSelectState extends AppState<CamerasSelect> { |
||||||
|
CameraController? _cameraController; |
||||||
|
|
||||||
|
Future<void> 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), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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/ |
@ -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 |
||||||
|
``` |
@ -0,0 +1,4 @@ |
|||||||
|
# SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
# SPDX-License-Identifier: BSD-3-Clause |
||||||
|
|
||||||
|
include: package:flutter_lints/flutter.yaml |
@ -0,0 +1,36 @@ |
|||||||
|
# SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
# 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) |
@ -0,0 +1,273 @@ |
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
* SPDX-License-Identifier: BSD-3-Clause |
||||||
|
*/ |
||||||
|
#include <camera_aurora/camera_aurora_plugin.h> |
||||||
|
#include <flutter/method-channel.h> |
||||||
|
#include <flutter/platform-methods.h> |
||||||
|
|
||||||
|
#include <QtCore> |
||||||
|
#include <QBuffer> |
||||||
|
#include <QCamera> |
||||||
|
#include <QCameraInfo> |
||||||
|
#include <QMediaRecorder> |
||||||
|
#include <QCameraImageCapture> |
||||||
|
|
||||||
|
#include <unistd.h> |
||||||
|
|
||||||
|
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<Encodable> list; |
||||||
|
|
||||||
|
const QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); |
||||||
|
|
||||||
|
for (const QCameraInfo &cameraInfo : cameras) |
||||||
|
{ |
||||||
|
list.push_back(std::map<Encodable, Encodable>{ |
||||||
|
{"deviceName", cameraInfo.deviceName().toStdString()}, |
||||||
|
{"position", static_cast<int>(cameraInfo.position())}, |
||||||
|
{"orientation", cameraInfo.orientation()}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
call.SendSuccessResponse(list); |
||||||
|
} |
||||||
|
|
||||||
|
void CameraAuroraPlugin::onCreateCamera(const MethodCall &call) |
||||||
|
{ |
||||||
|
QCameraInfo cameraInfo; |
||||||
|
|
||||||
|
const auto cameraName = call.GetArgument<Encodable::String>("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<Encodable::Int>("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<Encodable>{ |
||||||
|
id, |
||||||
|
fileName.toStdString()}); |
||||||
|
} |
||||||
|
|
||||||
|
void CameraAuroraPlugin::unimplemented(const MethodCall &call) |
||||||
|
{ |
||||||
|
call.SendSuccessResponse(nullptr); |
||||||
|
} |
||||||
|
|
||||||
|
#include "moc_camera_aurora_plugin.cpp" |
@ -0,0 +1,54 @@ |
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
* SPDX-License-Identifier: BSD-3-Clause |
||||||
|
*/ |
||||||
|
#ifndef FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_H |
||||||
|
#define FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_H |
||||||
|
|
||||||
|
#include <flutter/plugin-interface.h> |
||||||
|
#include <camera_aurora/globals.h> |
||||||
|
|
||||||
|
#include <streamcamera/streamcamera.h> |
||||||
|
#include <streamcamera/streamcamera-codec.h> |
||||||
|
|
||||||
|
#include <QtCore> |
||||||
|
#include <QCamera> |
||||||
|
#include <QCameraInfo> |
||||||
|
#include <QMediaRecorder> |
||||||
|
#include <QCameraImageCapture> |
||||||
|
|
||||||
|
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<QCamera> m_camera; |
||||||
|
QScopedPointer<QCameraImageCapture> m_imageCapture; |
||||||
|
}; |
||||||
|
|
||||||
|
#endif /* FLUTTER_PLUGIN_CAMERA_AURORA_PLUGIN_H */ |
@ -0,0 +1,14 @@ |
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
* 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 */ |
@ -0,0 +1,155 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<CameraImageData>? _frameStreamController; |
||||||
|
|
||||||
|
/// Completes with a list of available cameras. |
||||||
|
/// |
||||||
|
/// This method returns an empty list when no cameras are available. |
||||||
|
@override |
||||||
|
Future<List<CameraDescription>> availableCameras() => |
||||||
|
CameraAuroraPlatform.instance.availableCameras(); |
||||||
|
|
||||||
|
/// Creates an uninitialized camera instance and returns the cameraId. |
||||||
|
@override |
||||||
|
Future<int> 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<void> initializeCamera( |
||||||
|
int cameraId, { |
||||||
|
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, |
||||||
|
}) async { |
||||||
|
// init |
||||||
|
} |
||||||
|
|
||||||
|
/// Releases the resources of this camera. |
||||||
|
@override |
||||||
|
Future<void> dispose(int cameraId) { |
||||||
|
return CameraAuroraPlatform.instance.dispose(cameraId); |
||||||
|
} |
||||||
|
|
||||||
|
/// Captures an image and returns the file where it was saved. |
||||||
|
@override |
||||||
|
Future<XFile> 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<void> startVideoRecording(int cameraId, |
||||||
|
{Duration? maxVideoDuration}) => |
||||||
|
CameraAuroraPlatform.instance.startVideoRecording(cameraId); |
||||||
|
|
||||||
|
/// Stops the video recording and returns the file where it was saved. |
||||||
|
@override |
||||||
|
Future<XFile> stopVideoRecording(int cameraId) => |
||||||
|
CameraAuroraPlatform.instance.stopVideoRecording(cameraId); |
||||||
|
|
||||||
|
/// Pause video recording. |
||||||
|
@override |
||||||
|
Future<void> pauseVideoRecording(int cameraId) => |
||||||
|
CameraAuroraPlatform.instance.pauseVideoRecording(cameraId); |
||||||
|
|
||||||
|
/// Resume video recording after pausing. |
||||||
|
@override |
||||||
|
Future<void> resumeVideoRecording(int cameraId) => |
||||||
|
CameraAuroraPlatform.instance.resumeVideoRecording(cameraId); |
||||||
|
|
||||||
|
/// The ui orientation changed. |
||||||
|
/// |
||||||
|
/// Implementations for this: |
||||||
|
/// - Should support all 4 orientations. |
||||||
|
@override |
||||||
|
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() async* { |
||||||
|
yield const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); |
||||||
|
} |
||||||
|
|
||||||
|
/// The camera has been initialized. |
||||||
|
@override |
||||||
|
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) async* { |
||||||
|
yield CameraInitializedEvent( |
||||||
|
cameraId, |
||||||
|
// previewWidth |
||||||
|
400, |
||||||
|
// previewHeight |
||||||
|
400, |
||||||
|
// exposureMode |
||||||
|
ExposureMode.auto, |
||||||
|
// exposurePointSupported |
||||||
|
true, |
||||||
|
// focusMode |
||||||
|
FocusMode.auto, |
||||||
|
// focusPointSupported |
||||||
|
true, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Stream<CameraImageData> onStreamedFrameAvailable( |
||||||
|
int cameraId, { |
||||||
|
CameraImageStreamOptions? options, |
||||||
|
}) { |
||||||
|
_frameStreamController = StreamController<CameraImageData>( |
||||||
|
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), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,167 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<List<CameraDescription>> availableCameras() async { |
||||||
|
final List<CameraDescription> result = []; |
||||||
|
|
||||||
|
final cameras = await methodsChannel.invokeMethod<List<dynamic>?>( |
||||||
|
CameraAuroraMethods.availableCameras.name) ?? |
||||||
|
[]; |
||||||
|
|
||||||
|
for (int i = 0; i < cameras.length; i++) { |
||||||
|
final camera = cameras[i] as Map<dynamic, dynamic>; |
||||||
|
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<int> createCamera(String cameraName) async { |
||||||
|
return await methodsChannel |
||||||
|
.invokeMethod<Object?>(CameraAuroraMethods.createCamera.name, { |
||||||
|
'cameraName': cameraName, |
||||||
|
}) as int; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<void> dispose(int cameraId) { |
||||||
|
return methodsChannel |
||||||
|
.invokeMethod<Object?>(CameraAuroraMethods.dispose.name, { |
||||||
|
'cameraId': cameraId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<void> initializeCamera( |
||||||
|
int cameraId, { |
||||||
|
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, |
||||||
|
}) async { |
||||||
|
await methodsChannel |
||||||
|
.invokeMethod<Object?>(CameraAuroraMethods.initializeCamera.name, { |
||||||
|
'cameraId': cameraId, |
||||||
|
}) as int; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<XFile> takePicture(int cameraId) async { |
||||||
|
final result = await methodsChannel.invokeMethod<Object?>( |
||||||
|
CameraAuroraMethods.takePicture.name, |
||||||
|
{'cameraId': cameraId}, |
||||||
|
); |
||||||
|
|
||||||
|
if (result == true) { |
||||||
|
await for (final data |
||||||
|
in EventChannel(CameraAuroraEvents.cameraAuroraImageSaved.name) |
||||||
|
.receiveBroadcastStream()) { |
||||||
|
final response = |
||||||
|
(data as List<Object?>).map((e) => e.toString()).toList(); |
||||||
|
return XFile(response[1], mimeType: 'image/jpeg'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
throw "Error take picture"; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<void> startVideoRecording(int cameraId, |
||||||
|
{Duration? maxVideoDuration}) async { |
||||||
|
await methodsChannel |
||||||
|
.invokeMethod<Object?>(CameraAuroraMethods.startVideoRecording.name, { |
||||||
|
'cameraId': cameraId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<XFile> stopVideoRecording(int cameraId) async { |
||||||
|
await methodsChannel |
||||||
|
.invokeMethod<Object?>(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<void> pauseVideoRecording(int cameraId) async { |
||||||
|
await methodsChannel |
||||||
|
.invokeMethod<Object?>(CameraAuroraMethods.pauseVideoRecording.name, { |
||||||
|
'cameraId': cameraId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<void> resumeVideoRecording(int cameraId) async { |
||||||
|
await methodsChannel |
||||||
|
.invokeMethod<Object?>(CameraAuroraMethods.resumeVideoRecording.name, { |
||||||
|
'cameraId': cameraId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Stream<CameraImageData> 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, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,72 @@ |
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
// 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<List<CameraDescription>> availableCameras() { |
||||||
|
throw UnimplementedError('availableCameras() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<int> createCamera(String cameraName) { |
||||||
|
throw UnimplementedError('createCamera() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> dispose(int cameraId) { |
||||||
|
throw UnimplementedError('dispose() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> initializeCamera( |
||||||
|
int cameraId, { |
||||||
|
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, |
||||||
|
}) { |
||||||
|
throw UnimplementedError('initializeCamera() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<XFile> takePicture(int cameraId) { |
||||||
|
throw UnimplementedError('takePicture() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> startVideoRecording(int cameraId) { |
||||||
|
throw UnimplementedError('startVideoRecording() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<XFile> stopVideoRecording(int cameraId) { |
||||||
|
throw UnimplementedError('stopVideoRecording() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> pauseVideoRecording(int cameraId) { |
||||||
|
throw UnimplementedError('pauseVideoRecording() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> resumeVideoRecording(int cameraId) { |
||||||
|
throw UnimplementedError( |
||||||
|
'resumeVideoRecording() has not been implemented.'); |
||||||
|
} |
||||||
|
|
||||||
|
Stream<CameraImageData> streamedFrame(int cameraId) { |
||||||
|
throw UnimplementedError('streamedFrame() has not been implemented.'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
import 'dart:typed_data'; |
||||||
|
|
||||||
|
import 'package:camera_platform_interface/camera_platform_interface.dart'; |
||||||
|
|
||||||
|
CameraImageData cameraImageFromPlatformData(Map<dynamic, dynamic> 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<CameraImagePlane>.unmodifiable( |
||||||
|
(data['planes'] as List<dynamic>).map<CameraImagePlane>( |
||||||
|
(dynamic planeData) => _cameraImagePlaneFromPlatformData( |
||||||
|
planeData as Map<dynamic, dynamic>)))); |
||||||
|
} |
||||||
|
|
||||||
|
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<dynamic, dynamic> 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?); |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
# SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru> |
||||||
|
# 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 |
Loading…
Reference in new issue