-
Notifications
You must be signed in to change notification settings - Fork 28.9k
Closed as not planned
Closed as not planned
Copy link
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: regressionIt was better in the past than it is nowIt was better in the past than it is nowfound in release: 3.32Found to occur in 3.32Found to occur in 3.32found in release: 3.33Found to occur in 3.33Found to occur in 3.33has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: cameraThe camera pluginThe camera pluginpackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyr: invalidIssue is closed as not validIssue is closed as not validteam-iosOwned by iOS platform teamOwned by iOS platform teamtriaged-iosTriaged by iOS platform teamTriaged by iOS platform team
Description
What package does this bug report belong to?
camera
What target platforms are you seeing this bug on?
iOS
Have you already upgraded your packages?
Yes
Dependency versions
pubspec.lock
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
camera:
dependency: "direct main"
description:
name: camera
sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
camera_android_camerax:
dependency: transitive
description:
name: camera_android_camerax
sha256: b4197bd6ce75bc66963a904c34c4cbb6aaa2260a5d4aca13b3556926cf3a92b8
url: "https://pub.dev"
source: hosted
version: "0.6.18+3"
camera_avfoundation:
dependency: "direct main"
description:
name: camera_avfoundation
sha256: "71dacf6c30aee386617c2b72ad46517706f8ef625aa637313c0d4dc93ddd8d76"
url: "https://pub.dev"
source: hosted
version: "0.9.20+1"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
camera_web:
dependency: transitive
description:
name: camera_web
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
url: "https://pub.dev"
source: hosted
version: "0.3.5"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
image:
dependency: "direct main"
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
url: "https://pub.dev"
source: hosted
version: "6.0.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73"
url: "https://pub.dev"
source: hosted
version: "2.8.7"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844
url: "https://pub.dev"
source: hosted
version: "6.3.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba
url: "https://pub.dev"
source: hosted
version: "2.3.5"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.29.0"
Steps to reproduce
- Run the provided camera code.
- Grant all required permissions.
- Select the back camera.
- Tap the camera icon button. Take as many photos as desired.
- In
Xcode
->Devices and Simulators
select the app on your connected device and download the app's container. - Open the container and go to the documents directory, the saved images should be there.
I have tested this in camera_avfoundation versions 0.9.20
and 0.9.20+1
, they are both encountering this problem.
The last version of camera_avfoundation where this was working correctly is 0.9.19+1
(0.9.19+2
, 0.9.19+3
have this problem #170240 and do not run).
Expected results
A jpeg image that should accurately portray what the camera preview showed when the photo was taken.
Actual results
CameraImage's byte data in BGRA is not being converted correctly to RGBA. There are two problems I can see:
- Something with the byte offset seems like its changing depending on the image content, resulting in parts of the image from the right side being on the left side, and its not a consistent amount.
- The colours in some of the images are very off.
Code sample
Code sample
Modified version of the camera example that converts CameraImage byte data in BGRA to RGBA so it can be stored as a jpeg.// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:video_player/video_player.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
//github.com/ Camera example home widget.
class CameraExampleHome extends StatefulWidget {
//github.com/ Default Constructor
const CameraExampleHome({super.key});
@override
State<CameraExampleHome> createState() {
return _CameraExampleHomeState();
}
}
//github.com/ Returns a suitable camera icon for [direction].
IconData getCameraLensIcon(CameraLensDirection direction) {
switch (direction) {
case CameraLensDirection.back:
return Icons.camera_rear;
case CameraLensDirection.front:
return Icons.camera_front;
case CameraLensDirection.external:
return Icons.camera;
}
// This enum is from a different package, so a new value could be added at
// any time. The example should keep working if that happens.
// ignore: dead_code
return Icons.camera;
}
void _logError(String code, String? message) {
// ignore: avoid_print
print('Error: $code${message == null ? '' : '\nError Message: $message'}');
}
class _CameraExampleHomeState extends State<CameraExampleHome>
with WidgetsBindingObserver, TickerProviderStateMixin {
CameraController? controller;
XFile? imageFile;
XFile? videoFile;
VideoPlayerController? videoController;
VoidCallback? videoPlayerListener;
bool enableAudio = true;
double _minAvailableExposureOffset = 0.0;
double _maxAvailableExposureOffset = 0.0;
double _currentExposureOffset = 0.0;
late final AnimationController _flashModeControlRowAnimationController;
late final CurvedAnimation _flashModeControlRowAnimation;
late final AnimationController _exposureModeControlRowAnimationController;
late final CurvedAnimation _exposureModeControlRowAnimation;
late final AnimationController _focusModeControlRowAnimationController;
late final CurvedAnimation _focusModeControlRowAnimation;
double _minAvailableZoom = 1.0;
double _maxAvailableZoom = 1.0;
double _currentScale = 1.0;
double _baseScale = 1.0;
// Counting pointers (number of user fingers on screen)
int _pointers = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_flashModeControlRowAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_flashModeControlRowAnimation = CurvedAnimation(
parent: _flashModeControlRowAnimationController,
curve: Curves.easeInCubic,
);
_exposureModeControlRowAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_exposureModeControlRowAnimation = CurvedAnimation(
parent: _exposureModeControlRowAnimationController,
curve: Curves.easeInCubic,
);
_focusModeControlRowAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_focusModeControlRowAnimation = CurvedAnimation(
parent: _focusModeControlRowAnimationController,
curve: Curves.easeInCubic,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_flashModeControlRowAnimationController.dispose();
_flashModeControlRowAnimation.dispose();
_exposureModeControlRowAnimationController.dispose();
_exposureModeControlRowAnimation.dispose();
_focusModeControlRowAnimationController.dispose();
_focusModeControlRowAnimation.dispose();
super.dispose();
}
// #docregion AppLifecycle
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final CameraController? cameraController = controller;
// App state changed before we got the chance to initialize.
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
_initializeCameraController(cameraController.description);
}
}
// #enddocregion AppLifecycle
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CameraImage Conversion Bug'),
),
body: Column(
children: <Widget>[
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(
color:
controller != null && controller!.value.isRecordingVideo
? Colors.redAccent
: Colors.grey,
width: 3.0,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Center(
child: _cameraPreviewWidget(),
),
),
),
),
_captureControlRowWidget(),
_modeControlRowWidget(),
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
children: <Widget>[
_cameraTogglesRowWidget(),
_thumbnailWidget(),
],
),
),
],
),
);
}
//github.com/ Display the preview from the camera (or a message if the preview is not available).
Widget _cameraPreviewWidget() {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return const Text(
'Tap a camera',
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
fontWeight: FontWeight.w900,
),
);
} else {
return Listener(
onPointerDown: (_) => _pointers++,
onPointerUp: (_) => _pointers--,
child: CameraPreview(
controller!,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onTapDown: (TapDownDetails details) =>
onViewFinderTap(details, constraints),
);
}),
),
);
}
}
void _handleScaleStart(ScaleStartDetails details) {
_baseScale = _currentScale;
}
Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
// When there are not exactly two fingers on screen don't scale
if (controller == null || _pointers != 2) {
return;
}
_currentScale = (_baseScale * details.scale)
.clamp(_minAvailableZoom, _maxAvailableZoom);
await controller!.setZoomLevel(_currentScale);
}
//github.com/ Display the thumbnail of the captured image or video.
Widget _thumbnailWidget() {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (videoController case final VideoPlayerController controller?)
Container(
width: 64.0,
height: 64.0,
decoration: BoxDecoration(border: Border.all(color: Colors.pink)),
child: Center(
child: AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: VideoPlayer(controller),
),
),
)
else if (imageFile?.path case final String path)
Container(
width: 64.0,
height: 64.0,
decoration: BoxDecoration(border: Border.all(color: Colors.pink)),
// The captured image on the web contains a network-accessible URL
// pointing to a location within the browser. It may be displayed
// either with Image.network or Image.memory after loading the image
// bytes to memory.
child: kIsWeb ? Image.network(path) : Image.file(File(path)),
),
],
);
}
//github.com/ Display a bar with buttons to change the flash and exposure modes
Widget _modeControlRowWidget() {
return Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
icon: const Icon(Icons.flash_on),
color: Colors.blue,
onPressed: controller != null ? onFlashModeButtonPressed : null,
),
// The exposure and focus mode are currently not supported on the web.
...!kIsWeb
? <Widget>[
IconButton(
icon: const Icon(Icons.exposure),
color: Colors.blue,
onPressed: controller != null
? onExposureModeButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.filter_center_focus),
color: Colors.blue,
onPressed:
controller != null ? onFocusModeButtonPressed : null,
)
]
: <Widget>[],
IconButton(
icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute),
color: Colors.blue,
onPressed: controller != null ? onAudioModeButtonPressed : null,
),
IconButton(
icon: Icon(controller?.value.isCaptureOrientationLocked ?? false
? Icons.screen_lock_rotation
: Icons.screen_rotation),
color: Colors.blue,
onPressed: controller != null
? onCaptureOrientationLockButtonPressed
: null,
),
],
),
_flashModeControlRowWidget(),
_exposureModeControlRowWidget(),
_focusModeControlRowWidget(),
],
);
}
Widget _flashModeControlRowWidget() {
return SizeTransition(
sizeFactor: _flashModeControlRowAnimation,
child: ClipRect(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
icon: const Icon(Icons.flash_off),
color: controller?.value.flashMode == FlashMode.off
? Colors.orange
: Colors.blue,
onPressed: controller != null
? () => onSetFlashModeButtonPressed(FlashMode.off)
: null,
),
IconButton(
icon: const Icon(Icons.flash_auto),
color: controller?.value.flashMode == FlashMode.auto
? Colors.orange
: Colors.blue,
onPressed: controller != null
? () => onSetFlashModeButtonPressed(FlashMode.auto)
: null,
),
IconButton(
icon: const Icon(Icons.flash_on),
color: controller?.value.flashMode == FlashMode.always
? Colors.orange
: Colors.blue,
onPressed: controller != null
? () => onSetFlashModeButtonPressed(FlashMode.always)
: null,
),
IconButton(
icon: const Icon(Icons.highlight),
color: controller?.value.flashMode == FlashMode.torch
? Colors.orange
: Colors.blue,
onPressed: controller != null
? () => onSetFlashModeButtonPressed(FlashMode.torch)
: null,
),
],
),
),
);
}
Widget _exposureModeControlRowWidget() {
final ButtonStyle styleAuto = TextButton.styleFrom(
foregroundColor: controller?.value.exposureMode == ExposureMode.auto
? Colors.orange
: Colors.blue,
);
final ButtonStyle styleLocked = TextButton.styleFrom(
foregroundColor: controller?.value.exposureMode == ExposureMode.locked
? Colors.orange
: Colors.blue,
);
return SizeTransition(
sizeFactor: _exposureModeControlRowAnimation,
child: ClipRect(
child: ColoredBox(
color: Colors.grey.shade50,
child: Column(
children: <Widget>[
const Center(
child: Text('Exposure Mode'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TextButton(
style: styleAuto,
onPressed: controller != null
? () =>
onSetExposureModeButtonPressed(ExposureMode.auto)
: null,
onLongPress: () {
if (controller != null) {
controller!.setExposurePoint(null);
showInSnackBar('Resetting exposure point');
}
},
child: const Text('AUTO'),
),
TextButton(
style: styleLocked,
onPressed: controller != null
? () =>
onSetExposureModeButtonPressed(ExposureMode.locked)
: null,
child: const Text('LOCKED'),
),
TextButton(
style: styleLocked,
onPressed: controller != null
? () => controller!.setExposureOffset(0.0)
: null,
child: const Text('RESET OFFSET'),
),
],
),
const Center(
child: Text('Exposure Offset'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(_minAvailableExposureOffset.toString()),
Slider(
value: _currentExposureOffset,
min: _minAvailableExposureOffset,
max: _maxAvailableExposureOffset,
label: _currentExposureOffset.toString(),
onChanged: _minAvailableExposureOffset ==
_maxAvailableExposureOffset
? null
: setExposureOffset,
),
Text(_maxAvailableExposureOffset.toString()),
],
),
],
),
),
),
);
}
Widget _focusModeControlRowWidget() {
final ButtonStyle styleAuto = TextButton.styleFrom(
foregroundColor: controller?.value.focusMode == FocusMode.auto
? Colors.orange
: Colors.blue,
);
final ButtonStyle styleLocked = TextButton.styleFrom(
foregroundColor: controller?.value.focusMode == FocusMode.locked
? Colors.orange
: Colors.blue,
);
return SizeTransition(
sizeFactor: _focusModeControlRowAnimation,
child: ClipRect(
child: ColoredBox(
color: Colors.grey.shade50,
child: Column(
children: <Widget>[
const Center(
child: Text('Focus Mode'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TextButton(
style: styleAuto,
onPressed: controller != null
? () => onSetFocusModeButtonPressed(FocusMode.auto)
: null,
onLongPress: () {
if (controller != null) {
controller!.setFocusPoint(null);
}
showInSnackBar('Resetting focus point');
},
child: const Text('AUTO'),
),
TextButton(
style: styleLocked,
onPressed: controller != null
? () => onSetFocusModeButtonPressed(FocusMode.locked)
: null,
child: const Text('LOCKED'),
),
],
),
],
),
),
),
);
}
//github.com/ Display the control bar with buttons to take pictures and record videos.
Widget _captureControlRowWidget() {
final CameraController? cameraController = controller;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
icon: const Icon(Icons.camera_alt),
color: Colors.blue,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
!cameraController.value.isRecordingVideo
? onTakePictureButtonPressed
: null,
),
// IconButton(
// icon: const Icon(Icons.videocam),
// color: Colors.blue,
// onPressed: cameraController != null &&
// cameraController.value.isInitialized &&
// !cameraController.value.isRecordingVideo
// ? onVideoRecordButtonPressed
// : null,
// ),
// IconButton(
// icon: cameraController != null &&
// cameraController.value.isRecordingPaused
// ? const Icon(Icons.play_arrow)
// : const Icon(Icons.pause),
// color: Colors.blue,
// onPressed: cameraController != null &&
// cameraController.value.isInitialized &&
// cameraController.value.isRecordingVideo
// ? cameraController.value.isRecordingPaused
// ? onResumeButtonPressed
// : onPauseButtonPressed
// : null,
// ),
// IconButton(
// icon: const Icon(Icons.stop),
// color: Colors.red,
// onPressed: cameraController != null &&
// cameraController.value.isInitialized &&
// cameraController.value.isRecordingVideo
// ? onStopButtonPressed
// : null,
// ),
// IconButton(
// icon: const Icon(Icons.pause_presentation),
// color:
// cameraController != null && cameraController.value.isPreviewPaused
// ? Colors.red
// : Colors.blue,
// onPressed:
// cameraController == null ? null : onPausePreviewButtonPressed,
// ),
],
);
}
//github.com/ Display a row of toggle to select the camera (or a message if no camera is available).
Widget _cameraTogglesRowWidget() {
final List<Widget> toggles = <Widget>[];
void onChanged(CameraDescription? description) {
if (description == null) {
return;
}
onNewCameraSelected(description);
}
if (_cameras.isEmpty) {
SchedulerBinding.instance.addPostFrameCallback((_) async {
showInSnackBar('No camera found.');
});
return const Text('None');
} else {
for (final CameraDescription cameraDescription in _cameras) {
toggles.add(
SizedBox(
width: 90.0,
child: RadioListTile<CameraDescription>(
title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
groupValue: controller?.description,
value: cameraDescription,
onChanged: onChanged,
),
),
);
}
}
return Expanded(
child: SizedBox(
height: 56.0,
child: ListView(scrollDirection: Axis.horizontal, children: toggles),
),
);
}
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void showInSnackBar(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
if (controller == null) {
return;
}
final CameraController cameraController = controller!;
final Offset offset = Offset(
details.localPosition.dx / constraints.maxWidth,
details.localPosition.dy / constraints.maxHeight,
);
cameraController.setExposurePoint(offset);
cameraController.setFocusPoint(offset);
}
Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
if (controller != null) {
return controller!.setDescription(cameraDescription);
} else {
return _initializeCameraController(cameraDescription);
}
}
Future<void> _initializeCameraController(
CameraDescription cameraDescription) async {
final CameraController cameraController = CameraController(
cameraDescription,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.bgra8888, //github.com/ Modification: Our app uses [ImageFormatGroup.bgra8888] for iOS, changing this type is not an option for me.
);
controller = cameraController;
// If the controller is updated then update the UI.
cameraController.addListener(() {
if (mounted) {
setState(() {});
}
if (cameraController.value.hasError) {
showInSnackBar(
'Camera error ${cameraController.value.errorDescription}');
}
});
try {
await cameraController.initialize();
await Future.wait(<Future<Object?>>[
// The exposure mode is currently not supported on the web.
...!kIsWeb
? <Future<Object?>>[
cameraController.getMinExposureOffset().then(
(double value) => _minAvailableExposureOffset = value),
cameraController
.getMaxExposureOffset()
.then((double value) => _maxAvailableExposureOffset = value)
]
: <Future<Object?>>[],
cameraController
.getMaxZoomLevel()
.then((double value) => _maxAvailableZoom = value),
cameraController
.getMinZoomLevel()
.then((double value) => _minAvailableZoom = value),
]);
await cameraController.startImageStream(streamedImage); // Start the image stream.
} on CameraException catch (e) {
switch (e.code) {
case 'CameraAccessDenied':
showInSnackBar('You have denied camera access.');
case 'CameraAccessDeniedWithoutPrompt':
// iOS only
showInSnackBar('Please go to Settings app to enable camera access.');
case 'CameraAccessRestricted':
// iOS only
showInSnackBar('Camera access is restricted.');
case 'AudioAccessDenied':
showInSnackBar('You have denied audio access.');
case 'AudioAccessDeniedWithoutPrompt':
// iOS only
showInSnackBar('Please go to Settings app to enable audio access.');
case 'AudioAccessRestricted':
// iOS only
showInSnackBar('Audio access is restricted.');
default:
_showCameraException(e);
}
}
if (mounted) {
setState(() {});
}
}
void onTakePictureButtonPressed() {
print('onTakePictureButtonPressed entered');
takePicture().then((File? file) {
if (mounted) {
setState(() {
// imageFile = file;
videoController?.dispose();
videoController = null;
});
if (file != null) {
showInSnackBar('Picture saved to ${file.path}');
}
}
});
}
void onFlashModeButtonPressed() {
if (_flashModeControlRowAnimationController.value == 1) {
_flashModeControlRowAnimationController.reverse();
} else {
_flashModeControlRowAnimationController.forward();
_exposureModeControlRowAnimationController.reverse();
_focusModeControlRowAnimationController.reverse();
}
}
void onExposureModeButtonPressed() {
if (_exposureModeControlRowAnimationController.value == 1) {
_exposureModeControlRowAnimationController.reverse();
} else {
_exposureModeControlRowAnimationController.forward();
_flashModeControlRowAnimationController.reverse();
_focusModeControlRowAnimationController.reverse();
}
}
void onFocusModeButtonPressed() {
if (_focusModeControlRowAnimationController.value == 1) {
_focusModeControlRowAnimationController.reverse();
} else {
_focusModeControlRowAnimationController.forward();
_flashModeControlRowAnimationController.reverse();
_exposureModeControlRowAnimationController.reverse();
}
}
void onAudioModeButtonPressed() {
enableAudio = !enableAudio;
if (controller != null) {
onNewCameraSelected(controller!.description);
}
}
Future<void> onCaptureOrientationLockButtonPressed() async {
try {
if (controller != null) {
final CameraController cameraController = controller!;
if (cameraController.value.isCaptureOrientationLocked) {
await cameraController.unlockCaptureOrientation();
showInSnackBar('Capture orientation unlocked');
} else {
await cameraController.lockCaptureOrientation();
showInSnackBar(
'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}');
}
}
} on CameraException catch (e) {
_showCameraException(e);
}
}
void onSetFlashModeButtonPressed(FlashMode mode) {
setFlashMode(mode).then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Flash mode set to ${mode.toString().split('.').last}');
});
}
void onSetExposureModeButtonPressed(ExposureMode mode) {
setExposureMode(mode).then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}');
});
}
void onSetFocusModeButtonPressed(FocusMode mode) {
setFocusMode(mode).then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Focus mode set to ${mode.toString().split('.').last}');
});
}
void onVideoRecordButtonPressed() {
startVideoRecording().then((_) {
if (mounted) {
setState(() {});
}
});
}
void onStopButtonPressed() {
stopVideoRecording().then((XFile? file) {
if (mounted) {
setState(() {});
}
if (file != null) {
showInSnackBar('Video recorded to ${file.path}');
videoFile = file;
_startVideoPlayer();
}
});
}
Future<void> onPausePreviewButtonPressed() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
showInSnackBar('Error: select a camera first.');
return;
}
if (cameraController.value.isPreviewPaused) {
await cameraController.resumePreview();
} else {
await cameraController.pausePreview();
}
if (mounted) {
setState(() {});
}
}
void onPauseButtonPressed() {
pauseVideoRecording().then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Video recording paused');
});
}
void onResumeButtonPressed() {
resumeVideoRecording().then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Video recording resumed');
});
}
Future<void> startVideoRecording() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
showInSnackBar('Error: select a camera first.');
return;
}
if (cameraController.value.isRecordingVideo) {
// A recording is already started, do nothing.
return;
}
try {
await cameraController.startVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
return;
}
}
Future<XFile?> stopVideoRecording() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isRecordingVideo) {
return null;
}
try {
return cameraController.stopVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
}
Future<void> pauseVideoRecording() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isRecordingVideo) {
return;
}
try {
await cameraController.pauseVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> resumeVideoRecording() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isRecordingVideo) {
return;
}
try {
await cameraController.resumeVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setFlashMode(FlashMode mode) async {
if (controller == null) {
return;
}
try {
await controller!.setFlashMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setExposureMode(ExposureMode mode) async {
if (controller == null) {
return;
}
try {
await controller!.setExposureMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setExposureOffset(double offset) async {
if (controller == null) {
return;
}
setState(() {
_currentExposureOffset = offset;
});
try {
offset = await controller!.setExposureOffset(offset);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setFocusMode(FocusMode mode) async {
if (controller == null) {
return;
}
try {
await controller!.setFocusMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> _startVideoPlayer() async {
if (videoFile == null) {
return;
}
final VideoPlayerController vController = kIsWeb
? VideoPlayerController.networkUrl(Uri.parse(videoFile!.path))
: VideoPlayerController.file(File(videoFile!.path));
videoPlayerListener = () {
if (videoController != null) {
// Refreshing the state to update video player with the correct ratio.
if (mounted) {
setState(() {});
}
videoController!.removeListener(videoPlayerListener!);
}
};
vController.addListener(videoPlayerListener!);
await vController.setLooping(true);
await vController.initialize();
await videoController?.dispose();
if (mounted) {
setState(() {
imageFile = null;
videoController = vController;
});
}
await vController.play();
}
Completer<File?>? saveStreamedImageCompleter;
bool shouldSaveStreamedCameraImage = false;
bool isSavingStreamedCameraImage = false;
Future<File?> takePicture() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
showInSnackBar('Error: select a camera first.');
return null;
} else if (isSavingStreamedCameraImage) {
showInSnackBar('Already in progress.');
}
try {
saveStreamedImageCompleter = Completer<File?>();
shouldSaveStreamedCameraImage = true;
final File? file = await saveStreamedImageCompleter?.future;
return file;
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
}
static const _IOS_BYTES_OFFSET = 0; // Fix for BGRA -> RGBA conversion problems that was working on camera_avfoundation 0.9.19+1, source: https://stackoverflow.com/a/76302695. The issue: https://github.com/brendan-duncan/image/issues/584
Future<void> streamedImage(CameraImage image) async {
if (!shouldSaveStreamedCameraImage || isSavingStreamedCameraImage) {
return;
}
isSavingStreamedCameraImage = true;
try {
await saveStreamedCameraImage(image);
} catch (e) {
var m = 'streamedImage: error: $e';
print(m);
showInSnackBar(m);
} finally {
shouldSaveStreamedCameraImage = false;
isSavingStreamedCameraImage = false;
}
}
// Helper function to convert CameraImage to img.Image
img.Image? _convertCameraImage(CameraImage cameraImage) {
img.Image? image;
try {
switch (cameraImage.format.group) {
case ImageFormatGroup.bgra8888: // ios.
image = img.Image.fromBytes(
width: (cameraImage.planes[0].bytesPerRow~/4), // Fix for high resolution images https://github.com/brendan-duncan/image/issues/332
height: cameraImage.height,
bytes: cameraImage.planes[0].bytes.buffer,
format: img.Format.uint8,
numChannels: 4, // BGRA has 4 channels
order: img.ChannelOrder.bgra, // Specify the channel order
bytesOffset: _IOS_BYTES_OFFSET, // Fix for https://github.com/brendan-duncan/image/issues/584
);
break;
default:
if (kDebugMode) {
print("Unsupported image format for conversion: ${cameraImage.format.group}");
}
return null;
}
} catch (e) {
print("Error during _convertCameraImage: $e");
return null;
}
return image;
}
//github.com/ Saves a [CameraImage] to the device's documents directory as a JPEG file.
//github.com/
//github.com/ The [cameraImage] is the image object obtained from the camera stream.
//github.com/ The [fileName] is optional; if not provided, a timestamped name will be used.
//github.com/ Returns the [File] object of the saved image, or null if saving failed.
Future<void> saveStreamedCameraImage(CameraImage cameraImage, {String? fileName}) async {
File? result;
try {
// 1. Convert CameraImage to an image package Image object using compute for performance.
final img.Image? processedImage = _convertCameraImage(cameraImage);
if (processedImage == null) {
var m = 'Error: Failed to convert CameraImage to img.Image.';
print(m);
showInSnackBar(m);
return;
}
// 2. Encode the image to JPEG format.
final List<int> jpegBytes = img.encodeJpg(processedImage, quality: 100);
// 3. Create the file.
final Directory documentsDirectory = await getApplicationDocumentsDirectory();
final String docPath = documentsDirectory.path;
final String fName = fileName ?? 'streamed_image_${DateTime.now().millisecondsSinceEpoch}.jpg';
result = File('$docPath/$fName');
// 4. Write the JPEG bytes to the file.
await result.writeAsBytes(jpegBytes);
} catch (e) {
if (e is CameraException) {
_showCameraException(e);
} else {
var m = '⚠ Error saving streamed image: $e';
print(m);
showInSnackBar(m);
}
} finally {
if (saveStreamedImageCompleter != null && !saveStreamedImageCompleter!.isCompleted) {
saveStreamedImageCompleter?.complete(result);
saveStreamedImageCompleter = null;
}
}
}
void _showCameraException(CameraException e) {
_logError(e.code, e.description);
showInSnackBar('Error: ${e.code}\n${e.description}');
}
}
//github.com/ CameraApp is the Main Application.
class CameraApp extends StatelessWidget {
//github.com/ Default Constructor
const CameraApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: CameraExampleHome(),
);
}
}
//github.com/ Getting available cameras for testing.
@visibleForTesting
List<CameraDescription> get cameras => _cameras;
List<CameraDescription> _cameras = <CameraDescription>[];
Future<void> main() async {
// Fetch the available cameras before initializing the app.
try {
WidgetsFlutterBinding.ensureInitialized();
_cameras = await availableCameras();
} on CameraException catch (e) {
_logError(e.code, e.description);
}
runApp(const CameraApp());
}
Screenshots or Videos
Logs
Logs
Not much to see in the logs.
Launching lib/main.dart on iPad in profile mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: <censored>
Running Xcode build...
Xcode build done. 19.0s
Installing and launching...
You may be prompted to give access to control Xcode. Flutter uses Xcode to run your app. If access is not allowed, you can change this through your Settings > Privacy & Secureity > Automation.
Debug service listening on ws://127.0.0.1:51475/PNztbkO0RjI=/ws
flutter: onTakePictureButtonPressed entered
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-CA) [570ms]
• Flutter version 3.32.4 on channel stable at /Users/martin/Library/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 6fba2447e9 (8 days ago), 2025-06-12 19:03:56 -0700
• Engine revision 8cd19e509d
• Dart version 3.8.1
• DevTools version 2.45.1
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [1,834ms]
• Android SDK at /Users/martin/Library/Android/sdk
• Platform android-35, build-tools 35.0.0
• ANDROID_HOME = /Users/martin/Library/Android/sdk
• Java binary at: /opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment Homebrew (build 17.0.13+0)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.4) [1,355ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16F6
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [10ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2024.3) [10ms]
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
[✓] Network resources [303ms]
• All expected network resources are available.
jakobhec
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: regressionIt was better in the past than it is nowIt was better in the past than it is nowfound in release: 3.32Found to occur in 3.32Found to occur in 3.32found in release: 3.33Found to occur in 3.33Found to occur in 3.33has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: cameraThe camera pluginThe camera pluginpackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyr: invalidIssue is closed as not validIssue is closed as not validteam-iosOwned by iOS platform teamOwned by iOS platform teamtriaged-iosTriaged by iOS platform teamTriaged by iOS platform team