diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 62a4140aa5222..eaac4bc7ecc80 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -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. diff --git a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart index 6fab99b3d694f..25fc417a9cc40 100644 --- a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart +++ b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart @@ -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'; diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index 14d20fc817b2f..35ece04fdf525 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -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 createCameraWithSettings( + CameraDescription cameraDescription, + MediaSettings mediaSettings, + ) async { try { + final ResolutionPreset? resolutionPreset = mediaSettings.resolutionPreset; final Map? reply = await _channel .invokeMapMethod('create', { '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; diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index b43629d4e0c3f..b74cb4203324f 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -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 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. diff --git a/packages/camera/camera_platform_interface/lib/src/types/media_settings.dart b/packages/camera/camera_platform_interface/lib/src/types/media_settings.dart new file mode 100644 index 0000000000000..95e29fad7d7dd --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/media_settings.dart @@ -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}'; + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index a8a4f8ca5dc4e..f9a81559d6801 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -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'; diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 09caeda73eb85..b0c3a1d8bff0a 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -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" diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index e3b6858e6d25e..6a8ebf6540488 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -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 createCamera( + CameraDescription cameraDescription, ResolutionPreset? resolutionPreset, + {bool enableAudio = false}) { + _onCreateCameraCalled(cameraDescription, resolutionPreset, enableAudio); + return Future.value(0); + } +} diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 1fd6445e7464d..8159cdfe26f59 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -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: { '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() @@ -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() @@ -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 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 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 initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add( diff --git a/packages/camera/camera_platform_interface/test/types/media_settings_test.dart b/packages/camera/camera_platform_interface/test/types/media_settings_test.dart new file mode 100644 index 0000000000000..242a153107baf --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/media_settings_test.dart @@ -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', + ); + }); + }); +}