diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 8ad0b49fe98e..901548e9b6d5 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.10.6 +* Adds support to control video fps and bitrate. See `CameraController` constructor. * Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. * Updates support matrix in README to indicate that iOS 11 is no longer supported. * Clients on versions of Flutter that still support iOS 11 can continue to use this diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 1d402afcc2ed..5f90a0a740a9 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -232,12 +232,30 @@ class CameraValue { /// To show the camera preview on the screen use a [CameraPreview] widget. class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. + /// + /// - [resolutionPreset] affect the quality of video recording and image capture. + /// - [enableAudio] controls audio presence in recorded video. + /// + /// Following parameters (if present) will overwrite [resolutionPreset] settings: + /// - [fps] controls rate at which frames should be captured by the camera in frames per second. + /// - [videoBitrate] controls the video encoding bit rate for recording. + /// - [audioBitrate] controls the audio encoding bit rate for recording. + CameraController( CameraDescription description, - this.resolutionPreset, { - this.enableAudio = true, + ResolutionPreset resolutionPreset, { + bool enableAudio = true, + int? fps, + int? videoBitrate, + int? audioBitrate, this.imageFormatGroup, - }) : super(CameraValue.uninitialized(description)); + }) : mediaSettings = MediaSettings( + resolutionPreset: resolutionPreset, + enableAudio: enableAudio, + fps: fps, + videoBitrate: videoBitrate, + audioBitrate: audioBitrate), + super(CameraValue.uninitialized(description)); /// The properties of the camera device controlled by this controller. CameraDescription get description => value.description; @@ -248,10 +266,19 @@ class CameraController extends ValueNotifier { /// if unavailable a lower resolution will be used. /// /// See also: [ResolutionPreset]. - final ResolutionPreset resolutionPreset; + ResolutionPreset get resolutionPreset => + mediaSettings.resolutionPreset ?? ResolutionPreset.max; /// Whether to include audio when recording a video. - final bool enableAudio; + bool get enableAudio => mediaSettings.enableAudio; + + /// The media settings this controller is targeting. + /// + /// This media settings are not guaranteed to be available on the device, + /// if unavailable a [resolutionPreset] default values will be used. + /// + /// See also: [MediaSettings]. + final MediaSettings mediaSettings; /// The [ImageFormatGroup] describes the output of the raw image format. /// @@ -265,6 +292,7 @@ class CameraController extends ValueNotifier { bool _isDisposed = false; StreamSubscription? _imageStreamSubscription; + // A Future awaiting an attempt to initialize (e.g. after `initialize` was // just called). If the controller has not been initialized at least once, // this value is null. @@ -313,10 +341,9 @@ class CameraController extends ValueNotifier { ); }); - _cameraId = await CameraPlatform.instance.createCamera( + _cameraId = await CameraPlatform.instance.createCameraWithSettings( description, - resolutionPreset, - enableAudio: enableAudio, + mediaSettings, ); _unawaited(CameraPlatform.instance @@ -372,7 +399,7 @@ class CameraController extends ValueNotifier { /// Pauses the current camera preview Future pausePreview() async { - if (value.isPreviewPaused) { + if (value.isPreviewPaused || !value.isInitialized || _isDisposed) { return; } try { @@ -923,7 +950,7 @@ class Optional extends IterableBase { if (_value == null) { throw StateError('value called on absent Optional.'); } - return _value!; + return _value; } /// Executes a function if the Optional value is present. @@ -960,7 +987,7 @@ class Optional extends IterableBase { Optional transform(S Function(T value) transformer) { return _value == null ? Optional.absent() - : Optional.of(transformer(_value as T)); + : Optional.of(transformer(_value)); } /// Transforms the Optional value. @@ -971,7 +998,7 @@ class Optional extends IterableBase { Optional transformNullable(S? Function(T value) transformer) { return _value == null ? Optional.absent() - : Optional.fromNullable(transformer(_value as T)); + : Optional.fromNullable(transformer(_value)); } @override diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index dc5755bb8829..10533970c7ea 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,11 +4,11 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.5+9 +version: 0.10.6 environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.2.3 + flutter: ">=3.16.6" flutter: plugin: @@ -21,10 +21,10 @@ flutter: default_package: camera_web dependencies: - camera_android: ^0.10.7 - camera_avfoundation: ^0.9.13 - camera_platform_interface: ^2.5.0 - camera_web: ^0.3.1 + camera_android: ^0.10.9 + camera_avfoundation: ^0.9.15 + camera_platform_interface: ^2.6.0 + camera_web: ^0.3.3 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index c73e1816445c..4b5a2a5b5163 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -68,6 +69,15 @@ class FakeController extends ValueNotifier @override ResolutionPreset get resolutionPreset => ResolutionPreset.low; + @override + MediaSettings get mediaSettings => const MediaSettings( + resolutionPreset: ResolutionPreset.low, + fps: 15, + videoBitrate: 200000, + audioBitrate: 32000, + enableAudio: true, + ); + @override Future resumeVideoRecording() async {} diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index ec111ed85949..f10015334d52 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -111,6 +111,46 @@ void main() { expect(cameraController.value.isInitialized, isTrue); }); + test('can be initialized with media settings', () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.low, + fps: 15, + videoBitrate: 200000, + audioBitrate: 32000, + enableAudio: false, + ); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, const Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + expect(cameraController.resolutionPreset, ResolutionPreset.low); + expect(cameraController.enableAudio, false); + expect(cameraController.mediaSettings.fps, 15); + expect(cameraController.mediaSettings.videoBitrate, 200000); + expect(cameraController.mediaSettings.audioBitrate, 32000); + }); + + test('default constructor initializes media settings', () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.resolutionPreset, ResolutionPreset.max); + expect(cameraController.enableAudio, true); + expect(cameraController.mediaSettings.fps, isNull); + expect(cameraController.mediaSettings.videoBitrate, isNull); + expect(cameraController.mediaSettings.audioBitrate, isNull); + }); + test('can be disposed', () async { final CameraController cameraController = CameraController( const CameraDescription( @@ -1429,15 +1469,20 @@ class MockCameraPlatform extends Mock Future> availableCameras() => Future>.value(mockAvailableCameras); + @override + Future createCameraWithSettings( + CameraDescription cameraDescription, MediaSettings? mediaSettings) => + mockPlatformException + ? throw PlatformException(code: 'foo', message: 'bar') + : Future.value(mockInitializeCamera); + @override Future createCamera( CameraDescription description, ResolutionPreset? resolutionPreset, { bool enableAudio = false, }) => - mockPlatformException - ? throw PlatformException(code: 'foo', message: 'bar') - : Future.value(mockInitializeCamera); + createCameraWithSettings(description, null); @override Stream onCameraInitialized(int cameraId) =>