Skip to content

Commit

Permalink
[camera] MediaSettings parameter for createCameraWithSettings (#3586)
Browse files Browse the repository at this point in the history
This PR is for enabling fps and bitrate control of recorded video.
Allow users to more control over recorded video size.
`CameraPlatform.createCameraWithSettings` is added, leaving original `CameraPlatform.createCamera` commented as deprecated. So this is not breaking change.
Tested on a set of mobile devices.
Web support depends on browser (perfect with Firefox).

Fixes flutter/flutter#54339
  • Loading branch information
PROGrand committed May 6, 2024
1 parent 9a94bfd commit 2dfe645
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 23 deletions.
3 changes: 2 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
51 changes: 39 additions & 12 deletions packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,30 @@ class CameraValue {
/// To show the camera preview on the screen use a [CameraPreview] widget.
class CameraController extends ValueNotifier<CameraValue> {
/// 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;
Expand All @@ -248,10 +266,19 @@ class CameraController extends ValueNotifier<CameraValue> {
/// 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.
///
Expand All @@ -265,6 +292,7 @@ class CameraController extends ValueNotifier<CameraValue> {

bool _isDisposed = false;
StreamSubscription<CameraImageData>? _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.
Expand Down Expand Up @@ -313,10 +341,9 @@ class CameraController extends ValueNotifier<CameraValue> {
);
});

_cameraId = await CameraPlatform.instance.createCamera(
_cameraId = await CameraPlatform.instance.createCameraWithSettings(
description,
resolutionPreset,
enableAudio: enableAudio,
mediaSettings,
);

_unawaited(CameraPlatform.instance
Expand Down Expand Up @@ -372,7 +399,7 @@ class CameraController extends ValueNotifier<CameraValue> {

/// Pauses the current camera preview
Future<void> pausePreview() async {
if (value.isPreviewPaused) {
if (value.isPreviewPaused || !value.isInitialized || _isDisposed) {
return;
}
try {
Expand Down Expand Up @@ -923,7 +950,7 @@ class Optional<T> extends IterableBase<T> {
if (_value == null) {
throw StateError('value called on absent Optional.');
}
return _value!;
return _value;
}

/// Executes a function if the Optional value is present.
Expand Down Expand Up @@ -960,7 +987,7 @@ class Optional<T> extends IterableBase<T> {
Optional<S> transform<S>(S Function(T value) transformer) {
return _value == null
? Optional<S>.absent()
: Optional<S>.of(transformer(_value as T));
: Optional<S>.of(transformer(_value));
}

/// Transforms the Optional value.
Expand All @@ -971,7 +998,7 @@ class Optional<T> extends IterableBase<T> {
Optional<S> transformNullable<S>(S? Function(T value) transformer) {
return _value == null
? Optional<S>.absent()
: Optional<S>.fromNullable(transformer(_value as T));
: Optional<S>.fromNullable(transformer(_value));
}

@override
Expand Down
14 changes: 7 additions & 7 deletions packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions packages/camera/camera/test/camera_preview_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,6 +69,15 @@ class FakeController extends ValueNotifier<CameraValue>
@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<void> resumeVideoRecording() async {}

Expand Down
51 changes: 48 additions & 3 deletions packages/camera/camera/test/camera_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -1429,15 +1469,20 @@ class MockCameraPlatform extends Mock
Future<List<CameraDescription>> availableCameras() =>
Future<List<CameraDescription>>.value(mockAvailableCameras);

@override
Future<int> createCameraWithSettings(
CameraDescription cameraDescription, MediaSettings? mediaSettings) =>
mockPlatformException
? throw PlatformException(code: 'foo', message: 'bar')
: Future<int>.value(mockInitializeCamera);

@override
Future<int> createCamera(
CameraDescription description,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) =>
mockPlatformException
? throw PlatformException(code: 'foo', message: 'bar')
: Future<int>.value(mockInitializeCamera);
createCameraWithSettings(description, null);

@override
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) =>
Expand Down

0 comments on commit 2dfe645

Please sign in to comment.