Skip to content

Commit 62f38a1

Browse files
authored
[camera_android_camerax] Implement enableAudio for video recording (#9264)
> [!NOTE] > This should land after #9241, which should safely bumps the AGP version of this plugin's example app higher than this PR does. Fixes flutter/flutter#168551 by implementing the `enableAudio` camera setting for video recording. Also: - Bumps CameraX library version to the latest version,`1.5.0-beta01` (to use a new CameraX method in this implementation) - Bumps the plugin AGP version to that which `1.5.0-beta01` requires (`8.6.0`) - Bumps the plugin's example app AGP version to that which `1.5.0-beta01` requires (`8.6.0`) -- will be overridden by #9241 - Corrects the example app to use the `enableAudio` setting just as the app-facing camera widget does - Adds lint errors caused by the **pigeon generated** `CameraXLibrary.g.kt` file caused by the `1.5.0-beta01` bump to a `lint-baseline.xml` file (all the errors are [`UnsageOptInUsage`](https://googlesamples.github.io/android-custom-lint-rules/checks/UnsafeOptInUsageError.md.html) lints caused by the plugin's [`ExperimentalCamera2Interop`](https://developer.android.com/reference/androidx/camera/camera2/interop/ExperimentalCamera2Interop) usage which is unrelated to this PR) ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 9c11e9b commit 62f38a1

File tree

14 files changed

+596
-105
lines changed

14 files changed

+596
-105
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.6.18
2+
3+
* Adds support for the `MediaSettings.enableAudio` setting, which determines whether or not audio is
4+
recorded during video recording.
5+
16
## 0.6.17+1
27

38
* Replaces deprecated `onSurfaceDestroyed` with `onSurfaceCleanup`.

packages/camera/camera_android_camerax/android/build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath 'com.android.tools.build:gradle:8.5.0'
12+
classpath 'com.android.tools.build:gradle:8.6.0'
1313
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1414
}
1515
}
@@ -57,16 +57,17 @@ android {
5757
}
5858
}
5959

60-
lintOptions {
60+
lint {
6161
checkAllWarnings true
6262
warningsAsErrors true
6363
disable 'AndroidGradlePluginVersion', 'GradleDependency', 'InvalidPackage', 'NewerVersionAvailable'
64+
baseline = file("lint-baseline.xml")
6465
}
6566
}
6667

6768
dependencies {
6869
// CameraX core library using the camera2 implementation must use same version number.
69-
def camerax_version = "1.4.1"
70+
def camerax_version = "1.5.0-beta01"
7071
implementation "androidx.camera:camera-core:${camerax_version}"
7172
implementation "androidx.camera:camera-camera2:${camerax_version}"
7273
implementation "androidx.camera:camera-lifecycle:${camerax_version}"

packages/camera/camera_android_camerax/android/lint-baseline.xml

Lines changed: 268 additions & 0 deletions
Large diffs are not rendered by default.

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// found in the LICENSE file.
44
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
6-
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass", "UnsafeOptInUsageError")
6+
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
77

88
package io.flutter.plugins.camerax
99

@@ -3643,6 +3643,12 @@ abstract class PigeonApiVideoRecordEventListener(
36433643
abstract class PigeonApiPendingRecording(
36443644
open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar
36453645
) {
3646+
/** Enables audio to be recorded for this recording. */
3647+
abstract fun withAudioEnabled(
3648+
pigeon_instance: androidx.camera.video.PendingRecording,
3649+
initialMuted: Boolean
3650+
): androidx.camera.video.PendingRecording
3651+
36463652
/** Starts the recording, making it an active recording. */
36473653
abstract fun start(
36483654
pigeon_instance: androidx.camera.video.PendingRecording,
@@ -3653,6 +3659,29 @@ abstract class PigeonApiPendingRecording(
36533659
@Suppress("LocalVariableName")
36543660
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiPendingRecording?) {
36553661
val codec = api?.pigeonRegistrar?.codec ?: CameraXLibraryPigeonCodec()
3662+
run {
3663+
val channel =
3664+
BasicMessageChannel<Any?>(
3665+
binaryMessenger,
3666+
"dev.flutter.pigeon.camera_android_camerax.PendingRecording.withAudioEnabled",
3667+
codec)
3668+
if (api != null) {
3669+
channel.setMessageHandler { message, reply ->
3670+
val args = message as List<Any?>
3671+
val pigeon_instanceArg = args[0] as androidx.camera.video.PendingRecording
3672+
val initialMutedArg = args[1] as Boolean
3673+
val wrapped: List<Any?> =
3674+
try {
3675+
listOf(api.withAudioEnabled(pigeon_instanceArg, initialMutedArg))
3676+
} catch (exception: Throwable) {
3677+
CameraXLibraryPigeonUtils.wrapError(exception)
3678+
}
3679+
reply.reply(wrapped)
3680+
}
3681+
} else {
3682+
channel.setMessageHandler(null)
3683+
}
3684+
}
36563685
run {
36573686
val channel =
36583687
BasicMessageChannel<Any?>(

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingProxyApi.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package io.flutter.plugins.camerax;
66

7+
import android.Manifest;
8+
import android.content.pm.PackageManager;
79
import androidx.annotation.NonNull;
810
import androidx.camera.video.PendingRecording;
911
import androidx.camera.video.Recording;
@@ -25,6 +27,19 @@ public ProxyApiRegistrar getPigeonRegistrar() {
2527
return (ProxyApiRegistrar) super.getPigeonRegistrar();
2628
}
2729

30+
@NonNull
31+
@Override
32+
public PendingRecording withAudioEnabled(PendingRecording pigeonInstance, boolean initialMuted) {
33+
if (!initialMuted
34+
&& ContextCompat.checkSelfPermission(
35+
getPigeonRegistrar().getContext(), Manifest.permission.RECORD_AUDIO)
36+
== PackageManager.PERMISSION_GRANTED) {
37+
return pigeonInstance.withAudioEnabled(false);
38+
}
39+
40+
return pigeonInstance.withAudioEnabled(true);
41+
}
42+
2843
@NonNull
2944
@Override
3045
public Recording start(

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderProxyApi.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44

55
package io.flutter.plugins.camerax;
66

7-
import android.Manifest;
8-
import android.content.pm.PackageManager;
97
import androidx.annotation.NonNull;
108
import androidx.annotation.Nullable;
119
import androidx.camera.video.FileOutputOptions;
1210
import androidx.camera.video.PendingRecording;
1311
import androidx.camera.video.QualitySelector;
1412
import androidx.camera.video.Recorder;
15-
import androidx.core.content.ContextCompat;
1613
import java.io.File;
1714

1815
/**
@@ -69,11 +66,6 @@ public PendingRecording prepareRecording(Recorder pigeonInstance, @NonNull Strin
6966

7067
final PendingRecording pendingRecording =
7168
pigeonInstance.prepareRecording(getPigeonRegistrar().getContext(), fileOutputOptions);
72-
if (ContextCompat.checkSelfPermission(
73-
getPigeonRegistrar().getContext(), Manifest.permission.RECORD_AUDIO)
74-
== PackageManager.PERMISSION_GRANTED) {
75-
pendingRecording.withAudioEnabled();
76-
}
7769

7870
return pendingRecording;
7971
}

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
package io.flutter.plugins.camerax;
66

77
import static org.junit.Assert.assertEquals;
8+
import static org.mockito.ArgumentMatchers.eq;
89
import static org.mockito.Mockito.any;
910
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.verify;
1012
import static org.mockito.Mockito.when;
1113

14+
import android.Manifest;
15+
import android.content.Context;
16+
import android.content.pm.PackageManager;
1217
import androidx.camera.video.PendingRecording;
1318
import androidx.camera.video.Recording;
1419
import androidx.core.content.ContextCompat;
@@ -19,6 +24,65 @@
1924
import org.mockito.stubbing.Answer;
2025

2126
public class PendingRecordingTest {
27+
@Test
28+
public void withAudioEnabled_enablesAudioWhenRequestedAndPermissionGranted() {
29+
final PigeonApiPendingRecording api =
30+
new TestProxyApiRegistrar().getPigeonApiPendingRecording();
31+
final PendingRecording instance = mock(PendingRecording.class);
32+
final PendingRecording newInstance = mock(PendingRecording.class);
33+
34+
try (MockedStatic<ContextCompat> mockedContextCompat =
35+
Mockito.mockStatic(ContextCompat.class)) {
36+
mockedContextCompat
37+
.when(
38+
() ->
39+
ContextCompat.checkSelfPermission(
40+
any(Context.class), eq(Manifest.permission.RECORD_AUDIO)))
41+
.thenAnswer((Answer<Integer>) invocation -> PackageManager.PERMISSION_GRANTED);
42+
43+
when(instance.withAudioEnabled(false)).thenReturn(newInstance);
44+
45+
assertEquals(api.withAudioEnabled(instance, false), newInstance);
46+
verify(instance).withAudioEnabled(false);
47+
}
48+
}
49+
50+
@Test
51+
public void withAudioEnabled_doesNotEnableAudioWhenRequestedAndPermissionNotGranted() {
52+
final PigeonApiPendingRecording api =
53+
new TestProxyApiRegistrar().getPigeonApiPendingRecording();
54+
final PendingRecording instance = mock(PendingRecording.class);
55+
final PendingRecording newInstance = mock(PendingRecording.class);
56+
57+
try (MockedStatic<ContextCompat> mockedContextCompat =
58+
Mockito.mockStatic(ContextCompat.class)) {
59+
mockedContextCompat
60+
.when(
61+
() ->
62+
ContextCompat.checkSelfPermission(
63+
any(Context.class), eq(Manifest.permission.RECORD_AUDIO)))
64+
.thenAnswer((Answer<Integer>) invocation -> PackageManager.PERMISSION_DENIED);
65+
66+
when(instance.withAudioEnabled(true)).thenReturn(newInstance);
67+
68+
assertEquals(api.withAudioEnabled(instance, false), newInstance);
69+
verify(instance).withAudioEnabled(true);
70+
}
71+
}
72+
73+
@Test
74+
public void withAudioEnabled_doesNotEnableAudioWhenAudioNotRequested() {
75+
final PigeonApiPendingRecording api =
76+
new TestProxyApiRegistrar().getPigeonApiPendingRecording();
77+
final PendingRecording instance = mock(PendingRecording.class);
78+
final PendingRecording newInstance = mock(PendingRecording.class);
79+
80+
when(instance.withAudioEnabled(true)).thenReturn(newInstance);
81+
82+
assertEquals(api.withAudioEnabled(instance, true), newInstance);
83+
verify(instance).withAudioEnabled(true);
84+
}
85+
2286
@Test
2387
public void start_callsStartOnInstance() {
2488
final PigeonApiPendingRecording api =

packages/camera/camera_android_camerax/example/lib/main.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,11 +661,12 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
661661

662662
final CameraController cameraController = CameraController(
663663
cameraDescription,
664-
mediaSettings: const MediaSettings(
664+
mediaSettings: MediaSettings(
665665
resolutionPreset: ResolutionPreset.low,
666666
fps: 15,
667667
videoBitrate: 200000,
668668
audioBitrate: 32000,
669+
enableAudio: enableAudio,
669670
),
670671
imageFormatGroup: ImageFormatGroup.jpeg,
671672
);

packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ class AndroidCameraCameraX extends CameraPlatform {
261261
/// This is expressed in terms of one of the [Surface] rotation constant.
262262
late int _initialDefaultDisplayRotation;
263263

264+
/// Whether or not audio should be enabled for recording video if permission is
265+
/// granted.
266+
@visibleForTesting
267+
late bool enableRecordingAudio;
268+
264269
/// Returns list of all available cameras and their descriptions.
265270
@override
266271
Future<List<CameraDescription>> availableCameras() async {
@@ -345,8 +350,9 @@ class AndroidCameraCameraX extends CameraPlatform {
345350
CameraDescription cameraDescription,
346351
MediaSettings? mediaSettings,
347352
) async {
353+
enableRecordingAudio = mediaSettings?.enableAudio ?? false;
348354
final CameraPermissionsError? error = await systemServicesManager
349-
.requestCameraPermissions(mediaSettings?.enableAudio ?? false);
355+
.requestCameraPermissions(enableRecordingAudio);
350356

351357
if (error != null) {
352358
throw CameraException(error.errorCode, error.description);
@@ -1109,6 +1115,13 @@ class AndroidCameraCameraX extends CameraPlatform {
11091115
);
11101116
pendingRecording = await recorder!.prepareRecording(videoOutputPath!);
11111117

1118+
// Enable/disable recording audio as requested. If enabling audio is requested
1119+
// and permission was not granted when the camera was created, then recording
1120+
// audio will be disabled to respect the denied permission.
1121+
pendingRecording = await pendingRecording!.withAudioEnabled(
1122+
/* initialMuted */ !enableRecordingAudio,
1123+
);
1124+
11121125
recording = await pendingRecording!.start(_videoRecordingEventListener);
11131126

11141127
if (streamCallback != null) {

packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4359,6 +4359,42 @@ class PendingRecording extends PigeonInternalProxyApiBaseClass {
43594359
}
43604360
}
43614361

4362+
/// Enables audio to be recorded for this recording.
4363+
Future<PendingRecording> withAudioEnabled(bool initialMuted) async {
4364+
final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec =
4365+
_pigeonVar_codecPendingRecording;
4366+
final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger;
4367+
const String pigeonVar_channelName =
4368+
'dev.flutter.pigeon.camera_android_camerax.PendingRecording.withAudioEnabled';
4369+
final BasicMessageChannel<Object?> pigeonVar_channel =
4370+
BasicMessageChannel<Object?>(
4371+
pigeonVar_channelName,
4372+
pigeonChannelCodec,
4373+
binaryMessenger: pigeonVar_binaryMessenger,
4374+
);
4375+
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
4376+
<Object?>[this, initialMuted],
4377+
);
4378+
final List<Object?>? pigeonVar_replyList =
4379+
await pigeonVar_sendFuture as List<Object?>?;
4380+
if (pigeonVar_replyList == null) {
4381+
throw _createConnectionError(pigeonVar_channelName);
4382+
} else if (pigeonVar_replyList.length > 1) {
4383+
throw PlatformException(
4384+
code: pigeonVar_replyList[0]! as String,
4385+
message: pigeonVar_replyList[1] as String?,
4386+
details: pigeonVar_replyList[2],
4387+
);
4388+
} else if (pigeonVar_replyList[0] == null) {
4389+
throw PlatformException(
4390+
code: 'null-error',
4391+
message: 'Host platform returned null value for non-null return value.',
4392+
);
4393+
} else {
4394+
return (pigeonVar_replyList[0] as PendingRecording?)!;
4395+
}
4396+
}
4397+
43624398
/// Starts the recording, making it an active recording.
43634399
Future<Recording> start(VideoRecordEventListener listener) async {
43644400
final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec =

0 commit comments

Comments
 (0)