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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy