-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Closed
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listfound in release: 2.10Found to occur in 2.10Found to occur in 2.10found in release: 2.11Found to occur in 2.11Found to occur in 2.11has 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-androidAndroid applications specificallyAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer version
Description
Videos recorded in landscape right on Android are saved with the wrong orientation.
Might be related to the issue that is fixed by flutter/plugins#4634 although that is a video_player issue and this issue is specific to camera.
Steps to Reproduce
- flutter create myapp
- update the pubspec by adding
camera: ^0.9.4+12
// I added path_provider to make it easier to find the recorded video in the Files app on Android.
// This is purely for the bug report / reproducing of the bug and does not affect the behavior at all.
path_provider: ^2.0.9
- update the Android SDK version contraint in app/build.gradle so that the app compiles
minSdkVersion 21
targetSdkVersion 31
- Update main.dart with the code sample
- Run the app on an Android device Don't start recording yet
- Rotate your device to the right so that the device is in landscape right.
- Start recording a video
- Stop recording the video (the video is saved to disk) Keep in mind where the video was saved for step 10.
- Exit the app
- Find the recorded video on the file system of the device. I used the built in
Filesapp. - Open and play the video
- The video appears to be flipped
Expected results: The video that is saved to disk has the correct orientation
Actual results: The video appears to be flipped
Code sample
// ignore_for_file: public_member_api_docs, avoid_print
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
List<CameraDescription> cameras = <CameraDescription>[];
/// 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;
default:
throw ArgumentError('Unknown lens direction');
}
}
void logError(String code, String? message) {
if (message != null) {
print('Error: $code\nError Message: $message');
} else {
print('Error: $code');
}
}
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());
}
class CameraExampleHome extends StatefulWidget {
const CameraExampleHome({Key? key}) : super(key: key);
@override
_CameraExampleHomeState createState() => _CameraExampleHomeState();
}
class _CameraExampleHomeState extends State<CameraExampleHome>
with WidgetsBindingObserver, TickerProviderStateMixin {
CameraController? controller;
XFile? imageFile;
XFile? videoFile;
bool enableAudio = true;
// Counting pointers (number of user fingers on screen)
int _pointers = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
@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) {
onNewCameraSelected(cameraController.description);
}
}
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Camera example'),
),
body: Column(
children: <Widget>[
Expanded(
child: Container(
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Center(
child: _cameraPreviewWidget(),
),
),
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(
color:
controller != null && controller!.value.isRecordingVideo
? Colors.redAccent
: Colors.grey,
width: 3.0,
),
),
),
),
_captureControlRowWidget(),
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[_cameraTogglesRowWidget()],
),
),
],
),
);
}
/// 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,
onTapDown: (TapDownDetails details) =>
onViewFinderTap(details, constraints),
);
}),
),
);
}
}
Widget _captureControlRowWidget() {
final CameraController? cameraController = controller;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
icon: const Icon(Icons.videocam),
color: Colors.blue,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
!cameraController.value.isRecordingVideo
? onVideoRecordButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.stop),
color: Colors.red,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
cameraController.value.isRecordingVideo
? onStopButtonPressed
: null,
),
],
);
}
/// Display a row of toggle to select the camera (or a message if no camera is available).
Widget _cameraTogglesRowWidget() {
final List<Widget> toggles = <Widget>[];
onChanged(CameraDescription? description) {
if (description == null) {
return;
}
onNewCameraSelected(description);
}
if (cameras.isEmpty) {
return const Text('No camera found');
} 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:
controller != null && controller!.value.isRecordingVideo
? null
: onChanged,
),
),
);
}
}
return Row(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) {
await controller!.dispose();
}
final CameraController cameraController = CameraController(
cameraDescription,
kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
enableAudio: enableAudio,
imageFormatGroup: ImageFormatGroup.jpeg,
);
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();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
void onVideoRecordButtonPressed() {
startVideoRecording().then((_) {
if (mounted) {
setState(() {});
}
});
}
void onStopButtonPressed() {
stopVideoRecording().then((XFile? file) async {
if (mounted) {
setState(() {});
}
if (file != null) {
// Save the video elsewhere so that we can find the video
// using the Files app.
// The cache directory is not visible.
// This is purely to demonstrate the issue with the recorded video,
// it has nothing to do with the bug.
final dir = await getExternalStorageDirectory();
final path = dir!.path + Platform.pathSeparator + file.name;
await file.saveTo(path);
showInSnackBar('Video recorded to $path');
videoFile = file;
}
});
}
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;
}
}
void _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('Error: ${e.code}\n${e.description}');
}
}
class CameraApp extends StatelessWidget {
const CameraApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CameraExampleHome());
}
}
Video recordings
Recording of the device screen
screen_recording.mp4
Video recorder by the camera
REC6871609395713918438.mp4
I didn't upgrade to 2.10 yet, since I'm waiting for the first hotfix release (there have been cherry picks for that release)
I also don't have the iOS toolchain on this machine, but that is irrelevant since the issue was found on Android.
Flutter doctor
[✓] Flutter (Channel stable, 2.8.1, on macOS 12.0.1 21A559 darwin-x64, locale
en-GB)
• Flutter version 2.8.1 at /Users/navaronbracke/Documents/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 77d935af4d (9 weeks ago), 2021-12-16 08:37:33 -0800
• Engine revision 890a5fca2e
• Dart version 2.15.1
[✓] Android toolchain - develop for Android devices (Android SDK version
32.1.0-rc1)
• Android SDK at /Users/navaronbracke/Library/Android/sdk
• Platform android-32, build-tools 32.1.0-rc1
• Java binary at: /Applications/Android
Studio.app/Contents/jre/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 11.0.11+0-b60-7590822)
• All Android licenses accepted.
[✗] Xcode - develop for iOS and macOS
✗ Xcode installation is incomplete; a full installation is necessary for iOS
development.
Download at: https://developer.apple.com/xcode/download/
Or install Xcode via the App Store.
Once installed, run:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
✗ CocoaPods not installed.
CocoaPods is used to retrieve the iOS and macOS platform side's plugin
code that responds to your plugin usage on the Dart side.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins
To install see
https://guides.cocoapods.org/using/getting-started.html#installation for
instructions.
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2021.1)
• 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 11.0.11+0-b60-7590822)
[✓] VS Code (version 1.64.2)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.34.0
[✓] Connected device (2 available)
• motorola one (mobile) • ZL5228NFL7 • android-arm64 • Android 10 (API 29)
• Chrome (web) • chrome • web-javascript • Google Chrome
98.0.4758.80
! Doctor found issues in 1 category.
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listfound in release: 2.10Found to occur in 2.10Found to occur in 2.10found in release: 2.11Found to occur in 2.11Found to occur in 2.11has 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-androidAndroid applications specificallyAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer version