[camera] CameraPlatform.createCameraWithSettings (#3615)

## Platform interface of federated plugin
This is the `platform-interface` part of `camera` PR #3586.

## App-facing change

Previously, CameraController was unable to setup fps and bitrates, allowing only resolution settings like this:
```dart
controller = CameraController(_cameras[0], ResolutionPreset.max);
```

This PR gives additional functionality to set `fps` and `bitrates` via `withSettings`:

```dart
controller = CameraController.withSettings(
      _cameras[0],
      mediaSettings: const MediaSettings(
        resolutionPreset: ResolutionPreset.low,
        fps: 15,
        videoBitrate: 200000,
        audioBitrate: 32000,
        enableAudio: true,
      ),
    );
```

## Android, iOS, etc.

All platforms must implement `CameraPlatform.createCameraWithSettings` in addition to `CameraPlatform.createCamera`, providing platform specific code for `fps` and `bitrate' platform:
```dart
Future<int> createCamera(
    CameraDescription cameraDescription,
    CameraDescription cameraDescription,
    ResolutionPreset? resolutionPreset, {
    ResolutionPreset? resolutionPreset, {
    bool enableAudio = false,
    bool enableAudio = false,
  }) {
  }) =>
    throw UnimplementedError('createCamera() is not implemented.');
      createCameraWithSettings(
        cameraDescription,
        MediaSettings(
          resolutionPreset: resolutionPreset,
          enableAudio: enableAudio,
        ),
      );

  /// Creates an uninitialized camera instance and returns the cameraId.
  Future<int> createCameraWithSettings(
    CameraDescription cameraDescription,
    MediaSettings? mediaSettings,
  ) {
    throw UnimplementedError('createCameraWithSettings() is not implemented.');
  }
  }
```
This commit is contained in:
Vladimir E. Koltunov
2023-10-23 15:16:36 +03:00
committed by GitHub
parent a2d8672340
commit 4bf51144c2
10 changed files with 471 additions and 20 deletions

View File

@ -1,3 +1,7 @@
## 2.6.0
* Adds support to control video fps and bitrate. See `CameraPlatform.createCameraWithSettings`.
## 2.5.2
* Adds pub topics to package metadata.

View File

@ -8,4 +8,5 @@ export 'package:cross_file/cross_file.dart';
export 'src/events/camera_event.dart';
export 'src/events/device_event.dart';
export 'src/platform_interface/camera_platform.dart';
export 'src/types/media_settings.dart';
export 'src/types/types.dart';

View File

@ -88,15 +88,29 @@ class MethodChannelCamera extends CameraPlatform {
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) async {
}) async =>
createCameraWithSettings(
cameraDescription,
MediaSettings(
resolutionPreset: resolutionPreset, enableAudio: enableAudio));
@override
Future<int> createCameraWithSettings(
CameraDescription cameraDescription,
MediaSettings mediaSettings,
) async {
try {
final ResolutionPreset? resolutionPreset = mediaSettings.resolutionPreset;
final Map<String, dynamic>? reply = await _channel
.invokeMapMethod<String, dynamic>('create', <String, dynamic>{
'cameraName': cameraDescription.name,
'resolutionPreset': resolutionPreset != null
? _serializeResolutionPreset(resolutionPreset)
? _serializeResolutionPreset(mediaSettings.resolutionPreset!)
: null,
'enableAudio': enableAudio,
'fps': mediaSettings.fps,
'videoBitrate': mediaSettings.videoBitrate,
'audioBitrate': mediaSettings.audioBitrate,
'enableAudio': mediaSettings.enableAudio,
});
return reply!['cameraId']! as int;

View File

@ -55,6 +55,20 @@ abstract class CameraPlatform extends PlatformInterface {
throw UnimplementedError('createCamera() is not implemented.');
}
/// Creates an uninitialized camera instance and returns the cameraId.
///
/// Pass MediaSettings() for defaults
Future<int> createCameraWithSettings(
CameraDescription cameraDescription,
MediaSettings mediaSettings,
) {
return createCamera(
cameraDescription,
mediaSettings.resolutionPreset,
enableAudio: mediaSettings.enableAudio,
);
}
/// Initializes the camera on the device.
///
/// [imageFormatGroup] is used to specify the image formatting used.

View File

@ -0,0 +1,77 @@
// 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.
// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes
import 'resolution_preset.dart';
/// Recording media settings.
///
/// Used in [CameraPlatform.createCameraWithSettings].
/// Allows to tune recorded video parameters, such as resolution, frame rate, bitrate.
/// If [fps], [videoBitrate] or [audioBitrate] are passed, they must be greater than zero.
class MediaSettings {
/// Creates a [MediaSettings].
const MediaSettings({
this.resolutionPreset,
this.fps,
this.videoBitrate,
this.audioBitrate,
this.enableAudio = false,
}) : assert(fps == null || fps > 0, 'fps must be null or greater than zero'),
assert(videoBitrate == null || videoBitrate > 0,
'videoBitrate must be null or greater than zero'),
assert(audioBitrate == null || audioBitrate > 0,
'audioBitrate must be null or greater than zero');
/// [ResolutionPreset] affect the quality of video recording and image capture.
final ResolutionPreset? resolutionPreset;
/// Rate at which frames should be captured by the camera in frames per second.
final int? fps;
/// The video encoding bit rate for recording.
final int? videoBitrate;
/// The audio encoding bit rate for recording.
final int? audioBitrate;
/// Controls audio presence in recorded video.
final bool enableAudio;
@override
bool operator ==(Object other) {
if (identical(other, this)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MediaSettings &&
resolutionPreset == other.resolutionPreset &&
fps == other.fps &&
videoBitrate == other.videoBitrate &&
audioBitrate == other.audioBitrate &&
enableAudio == other.enableAudio;
}
@override
int get hashCode => Object.hash(
resolutionPreset,
fps,
videoBitrate,
audioBitrate,
enableAudio,
);
@override
String toString() {
return 'MediaSettings{'
'resolutionPreset: $resolutionPreset, '
'fps: $fps, '
'videoBitrate: $videoBitrate, '
'audioBitrate: $audioBitrate, '
'enableAudio: $enableAudio}';
}
}

View File

@ -9,5 +9,6 @@ export 'exposure_mode.dart';
export 'flash_mode.dart';
export 'focus_mode.dart';
export 'image_format_group.dart';
export 'media_settings.dart';
export 'resolution_preset.dart';
export 'video_capture_options.dart';

View File

@ -4,7 +4,7 @@ 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
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 2.5.2
version: 2.6.0
environment:
sdk: ">=2.19.0 <4.0.0"

View File

@ -163,12 +163,67 @@ void main() {
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
ResolutionPreset.low,
),
throwsUnimplementedError,
);
});
test(
'Default implementation of createCameraWithSettings() should call createCamera() passing parameters',
() {
// Arrange
const CameraDescription cameraDescription = CameraDescription(
name: 'back',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
);
const MediaSettings mediaSettings = MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
);
bool createCameraCalled = false;
final OverriddenCameraPlatform cameraPlatform = OverriddenCameraPlatform((
CameraDescription cameraDescriptionArg,
ResolutionPreset? resolutionPresetArg,
bool enableAudioArg,
) {
expect(
cameraDescriptionArg,
cameraDescription,
reason: 'should pass camera description',
);
expect(
resolutionPresetArg,
mediaSettings.resolutionPreset,
reason: 'should pass resolution preset',
);
expect(
enableAudioArg,
mediaSettings.enableAudio,
reason: 'should pass enableAudio',
);
createCameraCalled = true;
});
// Act & Assert
cameraPlatform.createCameraWithSettings(
cameraDescription,
mediaSettings,
);
expect(createCameraCalled, isTrue,
reason:
'default implementation of createCameraWithSettings should call createCamera passing parameters');
});
test(
'Default implementation of initializeCamera() should throw unimplemented error',
() {
@ -496,3 +551,21 @@ class ImplementsCameraPlatform implements CameraPlatform {
}
class ExtendsCameraPlatform extends CameraPlatform {}
class OverriddenCameraPlatform extends CameraPlatform {
OverriddenCameraPlatform(this._onCreateCameraCalled);
final void Function(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset,
bool enableAudio,
) _onCreateCameraCalled;
@override
Future<int> createCamera(
CameraDescription cameraDescription, ResolutionPreset? resolutionPreset,
{bool enableAudio = false}) {
_onCreateCameraCalled(cameraDescription, resolutionPreset, enableAudio);
return Future<int>.value(0);
}
}

View File

@ -33,12 +33,17 @@ void main() {
final MethodChannelCamera camera = MethodChannelCamera();
// Act
final int cameraId = await camera.createCamera(
final int cameraId = await camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
),
);
// Assert
@ -47,7 +52,10 @@ void main() {
'create',
arguments: <String, Object?>{
'cameraName': 'Test',
'resolutionPreset': 'high',
'resolutionPreset': 'low',
'fps': 15,
'videoBitrate': 200000,
'audioBitrate': 32000,
'enableAudio': false
},
),
@ -71,13 +79,19 @@ void main() {
// Act
expect(
() => camera.createCamera(
() => camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
),
),
throwsA(
isA<CameraException>()
@ -105,13 +119,19 @@ void main() {
// Act
expect(
() => camera.createCamera(
() => camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
),
),
throwsA(
isA<CameraException>()
@ -167,13 +187,19 @@ void main() {
'initialize': null
});
final MethodChannelCamera camera = MethodChannelCamera();
final int cameraId = await camera.createCamera(
final int cameraId = await camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
),
);
// Act
@ -214,13 +240,19 @@ void main() {
});
final MethodChannelCamera camera = MethodChannelCamera();
final int cameraId = await camera.createCamera(
final int cameraId = await camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
),
);
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
camera.cameraEventStreamController.add(CameraInitializedEvent(
@ -262,13 +294,19 @@ void main() {
},
);
camera = MethodChannelCamera();
cameraId = await camera.createCamera(
cameraId = await camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
),
);
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
camera.cameraEventStreamController.add(CameraInitializedEvent(
@ -432,13 +470,19 @@ void main() {
},
);
camera = MethodChannelCamera();
cameraId = await camera.createCamera(
cameraId = await camera.createCameraWithSettings(
const CameraDescription(
name: 'Test',
lensDirection: CameraLensDirection.back,
sensorOrientation: 0,
),
ResolutionPreset.high,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: true,
),
);
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
camera.cameraEventStreamController.add(

View File

@ -0,0 +1,223 @@
// 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.
// ignore_for_file: always_specify_types
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test(
'MediaSettings non-parametrized constructor should have correct initial values',
() {
const MediaSettings settingsWithNoParameters = MediaSettings();
expect(
settingsWithNoParameters.resolutionPreset,
isNull,
reason:
'MediaSettings constructor should have null default resolutionPreset',
);
expect(
settingsWithNoParameters.fps,
isNull,
reason: 'MediaSettings constructor should have null default fps',
);
expect(
settingsWithNoParameters.videoBitrate,
isNull,
reason: 'MediaSettings constructor should have null default videoBitrate',
);
expect(
settingsWithNoParameters.audioBitrate,
isNull,
reason: 'MediaSettings constructor should have null default audioBitrate',
);
expect(
settingsWithNoParameters.enableAudio,
isFalse,
reason: 'MediaSettings constructor should have false default enableAudio',
);
});
test('MediaSettings fps should hold parameters', () {
const MediaSettings settings = MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 20,
videoBitrate: 128000,
audioBitrate: 32000,
enableAudio: true,
);
expect(
settings.resolutionPreset,
ResolutionPreset.low,
reason:
'MediaSettings constructor should hold resolutionPreset parameter',
);
expect(
settings.fps,
20,
reason: 'MediaSettings constructor should hold fps parameter',
);
expect(
settings.videoBitrate,
128000,
reason: 'MediaSettings constructor should hold videoBitrate parameter',
);
expect(
settings.audioBitrate,
32000,
reason: 'MediaSettings constructor should hold audioBitrate parameter',
);
expect(
settings.enableAudio,
true,
reason: 'MediaSettings constructor should hold enableAudio parameter',
);
});
test('MediaSettings hash should be Object.hash of passed parameters', () {
const MediaSettings settings = MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 20,
videoBitrate: 128000,
audioBitrate: 32000,
enableAudio: true,
);
expect(
settings.hashCode,
Object.hash(ResolutionPreset.low, 20, 128000, 32000, true),
reason:
'MediaSettings hash() should be equal to Object.hash of parameters',
);
});
group('MediaSettings == operator', () {
const ResolutionPreset preset1 = ResolutionPreset.low;
const int fps1 = 20;
const int videoBitrate1 = 128000;
const int audioBitrate1 = 32000;
const bool enableAudio1 = true;
const ResolutionPreset preset2 = ResolutionPreset.high;
const int fps2 = fps1 + 10;
const int videoBitrate2 = videoBitrate1 * 2;
const int audioBitrate2 = audioBitrate1 * 2;
const bool enableAudio2 = !enableAudio1;
const MediaSettings settings1 = MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 20,
videoBitrate: 128000,
audioBitrate: 32000,
enableAudio: true,
);
test('should compare resolutionPreset', () {
const MediaSettings settings2 = MediaSettings(
resolutionPreset: preset2,
fps: fps1,
videoBitrate: videoBitrate1,
audioBitrate: audioBitrate1,
enableAudio: enableAudio1,
);
expect(settings1 == settings2, isFalse);
});
test('should compare fps', () {
const MediaSettings settings2 = MediaSettings(
resolutionPreset: preset1,
fps: fps2,
videoBitrate: videoBitrate1,
audioBitrate: audioBitrate1,
enableAudio: enableAudio1,
);
expect(settings1 == settings2, isFalse);
});
test('should compare videoBitrate', () {
const MediaSettings settings2 = MediaSettings(
resolutionPreset: preset1,
fps: fps1,
videoBitrate: videoBitrate2,
audioBitrate: audioBitrate1,
enableAudio: enableAudio1,
);
expect(settings1 == settings2, isFalse);
});
test('should compare audioBitrate', () {
const MediaSettings settings2 = MediaSettings(
resolutionPreset: preset1,
fps: fps1,
videoBitrate: videoBitrate1,
audioBitrate: audioBitrate2,
enableAudio: enableAudio1,
);
expect(settings1 == settings2, isFalse);
});
test('should compare enableAudio', () {
const MediaSettings settings2 = MediaSettings(
resolutionPreset: preset1,
fps: fps1,
videoBitrate: videoBitrate1,
audioBitrate: audioBitrate1,
// ignore: avoid_redundant_argument_values
enableAudio: enableAudio2,
);
expect(settings1 == settings2, isFalse);
});
test('should return true when all parameters are equal', () {
const MediaSettings sameSettings = MediaSettings(
resolutionPreset: preset1,
fps: fps1,
videoBitrate: videoBitrate1,
audioBitrate: audioBitrate1,
enableAudio: enableAudio1,
);
expect(
settings1 == sameSettings,
isTrue,
);
});
test('Identical objects should be equal', () {
const MediaSettings settingsIdentical = settings1;
expect(
settings1 == settingsIdentical,
isTrue,
reason:
'MediaSettings == operator should return true for identical objects',
);
});
test('Objects of different types should be non-equal', () {
expect(
settings1 == Object(),
isFalse,
reason:
'MediaSettings == operator should return false for objects of different types',
);
});
});
}