Skip to content

Commit

Permalink
[camera] MediaSettings parameter for createCameraWithSettings (flutte…
Browse files Browse the repository at this point in the history
…r#3586)

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 authored and vashworth committed May 6, 2024
1 parent 9ec97b1 commit 6bdd3a7
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 6bdd3a7

Please sign in to comment.