Skip to content

[camera_android_camerax] Implement enableAudio for video recording #9264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.6.18

* Adds support for the `MediaSettings.enableAudio` setting, which determines whether or not audio is
recorded during video recording.

## 0.6.17+1

* Replaces deprecated `onSurfaceDestroyed` with `onSurfaceCleanup`.
Expand Down
7 changes: 4 additions & 3 deletions packages/camera/camera_android_camerax/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.android.tools.build:gradle:8.6.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down Expand Up @@ -57,16 +57,17 @@ android {
}
}

lintOptions {
lint {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lintOptions is deprecated!

checkAllWarnings true
warningsAsErrors true
disable 'AndroidGradlePluginVersion', 'GradleDependency', 'InvalidPackage', 'NewerVersionAvailable'
baseline = file("lint-baseline.xml")
}
}

dependencies {
// CameraX core library using the camera2 implementation must use same version number.
def camerax_version = "1.4.1"
def camerax_version = "1.5.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
Expand Down
268 changes: 268 additions & 0 deletions packages/camera/camera_android_camerax/android/lint-baseline.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// found in the LICENSE file.
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass", "UnsafeOptInUsageError")
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

package io.flutter.plugins.camerax

Expand Down Expand Up @@ -3643,6 +3643,12 @@ abstract class PigeonApiVideoRecordEventListener(
abstract class PigeonApiPendingRecording(
open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar
) {
/** Enables audio to be recorded for this recording. */
abstract fun withAudioEnabled(
pigeon_instance: androidx.camera.video.PendingRecording,
initialMuted: Boolean
): androidx.camera.video.PendingRecording

/** Starts the recording, making it an active recording. */
abstract fun start(
pigeon_instance: androidx.camera.video.PendingRecording,
Expand All @@ -3653,6 +3659,29 @@ abstract class PigeonApiPendingRecording(
@Suppress("LocalVariableName")
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiPendingRecording?) {
val codec = api?.pigeonRegistrar?.codec ?: CameraXLibraryPigeonCodec()
run {
val channel =
BasicMessageChannel<Any?>(
binaryMessenger,
"dev.flutter.pigeon.camera_android_camerax.PendingRecording.withAudioEnabled",
codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val pigeon_instanceArg = args[0] as androidx.camera.video.PendingRecording
val initialMutedArg = args[1] as Boolean
val wrapped: List<Any?> =
try {
listOf(api.withAudioEnabled(pigeon_instanceArg, initialMutedArg))
} catch (exception: Throwable) {
CameraXLibraryPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel =
BasicMessageChannel<Any?>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package io.flutter.plugins.camerax;

import android.Manifest;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.camera.video.PendingRecording;
import androidx.camera.video.Recording;
Expand All @@ -25,6 +27,19 @@ public ProxyApiRegistrar getPigeonRegistrar() {
return (ProxyApiRegistrar) super.getPigeonRegistrar();
}

@NonNull
@Override
public PendingRecording withAudioEnabled(PendingRecording pigeonInstance, boolean initialMuted) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, withAudioEnabled(true) disables audio and withAudioEnabled(false) enables audio? I'm confused by the parameter name being initialMuted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol yeah it's confusing.

I made this mirror PendingRecording.withAudioEnabled where initialMuted == true means that you want to initialize the recording as being muted/audio disabled, so withAudioEnabled(true) disables audio, withAudioEnabled(false) enables audio.

if (!initialMuted
&& ContextCompat.checkSelfPermission(
getPigeonRegistrar().getContext(), Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {
return pigeonInstance.withAudioEnabled(false);
}

return pigeonInstance.withAudioEnabled(true);
}

@NonNull
@Override
public Recording start(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@

package io.flutter.plugins.camerax;

import android.Manifest;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.video.FileOutputOptions;
import androidx.camera.video.PendingRecording;
import androidx.camera.video.QualitySelector;
import androidx.camera.video.Recorder;
import androidx.core.content.ContextCompat;
import java.io.File;

/**
Expand Down Expand Up @@ -69,11 +66,6 @@ public PendingRecording prepareRecording(Recorder pigeonInstance, @NonNull Strin

final PendingRecording pendingRecording =
pigeonInstance.prepareRecording(getPigeonRegistrar().getContext(), fileOutputOptions);
if (ContextCompat.checkSelfPermission(
getPigeonRegistrar().getContext(), Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {
pendingRecording.withAudioEnabled();
}

return pendingRecording;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
package io.flutter.plugins.camerax;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.camera.video.PendingRecording;
import androidx.camera.video.Recording;
import androidx.core.content.ContextCompat;
Expand All @@ -19,6 +24,65 @@
import org.mockito.stubbing.Answer;

public class PendingRecordingTest {
@Test
public void withAudioEnabled_enablesAudioWhenRequestedAndPermissionGranted() {
final PigeonApiPendingRecording api =
new TestProxyApiRegistrar().getPigeonApiPendingRecording();
final PendingRecording instance = mock(PendingRecording.class);
final PendingRecording newInstance = mock(PendingRecording.class);

try (MockedStatic<ContextCompat> mockedContextCompat =
Mockito.mockStatic(ContextCompat.class)) {
mockedContextCompat
.when(
() ->
ContextCompat.checkSelfPermission(
any(Context.class), eq(Manifest.permission.RECORD_AUDIO)))
.thenAnswer((Answer<Integer>) invocation -> PackageManager.PERMISSION_GRANTED);

when(instance.withAudioEnabled(false)).thenReturn(newInstance);

assertEquals(api.withAudioEnabled(instance, false), newInstance);
verify(instance).withAudioEnabled(false);
}
}

@Test
public void withAudioEnabled_doesNotEnableAudioWhenRequestedAndPermissionNotGranted() {
final PigeonApiPendingRecording api =
new TestProxyApiRegistrar().getPigeonApiPendingRecording();
final PendingRecording instance = mock(PendingRecording.class);
final PendingRecording newInstance = mock(PendingRecording.class);

try (MockedStatic<ContextCompat> mockedContextCompat =
Mockito.mockStatic(ContextCompat.class)) {
mockedContextCompat
.when(
() ->
ContextCompat.checkSelfPermission(
any(Context.class), eq(Manifest.permission.RECORD_AUDIO)))
.thenAnswer((Answer<Integer>) invocation -> PackageManager.PERMISSION_DENIED);

when(instance.withAudioEnabled(true)).thenReturn(newInstance);

assertEquals(api.withAudioEnabled(instance, false), newInstance);
verify(instance).withAudioEnabled(true);
}
}

@Test
public void withAudioEnabled_doesNotEnableAudioWhenAudioNotRequested() {
final PigeonApiPendingRecording api =
new TestProxyApiRegistrar().getPigeonApiPendingRecording();
final PendingRecording instance = mock(PendingRecording.class);
final PendingRecording newInstance = mock(PendingRecording.class);

when(instance.withAudioEnabled(true)).thenReturn(newInstance);

assertEquals(api.withAudioEnabled(instance, true), newInstance);
verify(instance).withAudioEnabled(true);
}

@Test
public void start_callsStartOnInstance() {
final PigeonApiPendingRecording api =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,11 +661,12 @@ class _CameraExampleHomeState extends State<CameraExampleHome>

final CameraController cameraController = CameraController(
cameraDescription,
mediaSettings: const MediaSettings(
mediaSettings: MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 15,
videoBitrate: 200000,
audioBitrate: 32000,
enableAudio: enableAudio,
),
imageFormatGroup: ImageFormatGroup.jpeg,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ class AndroidCameraCameraX extends CameraPlatform {
/// This is expressed in terms of one of the [Surface] rotation constant.
late int _initialDefaultDisplayRotation;

/// Whether or not audio should be enabled for recording video if permission is
/// granted.
@visibleForTesting
late bool enableRecordingAudio;

/// Returns list of all available cameras and their descriptions.
@override
Future<List<CameraDescription>> availableCameras() async {
Expand Down Expand Up @@ -345,8 +350,9 @@ class AndroidCameraCameraX extends CameraPlatform {
CameraDescription cameraDescription,
MediaSettings? mediaSettings,
) async {
enableRecordingAudio = mediaSettings?.enableAudio ?? false;
final CameraPermissionsError? error = await systemServicesManager
.requestCameraPermissions(mediaSettings?.enableAudio ?? false);
.requestCameraPermissions(enableRecordingAudio);

if (error != null) {
throw CameraException(error.errorCode, error.description);
Expand Down Expand Up @@ -1109,6 +1115,13 @@ class AndroidCameraCameraX extends CameraPlatform {
);
pendingRecording = await recorder!.prepareRecording(videoOutputPath!);

// Enable/disable recording audio as requested. If enabling audio is requested
// and permission was not granted when the camera was created, then recording
// audio will be disabled to respect the denied permission.
pendingRecording = await pendingRecording!.withAudioEnabled(
/* initialMuted */ !enableRecordingAudio,
);

recording = await pendingRecording!.start(_videoRecordingEventListener);

if (streamCallback != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4359,6 +4359,42 @@ class PendingRecording extends PigeonInternalProxyApiBaseClass {
}
}

/// Enables audio to be recorded for this recording.
Future<PendingRecording> withAudioEnabled(bool initialMuted) async {
final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec =
_pigeonVar_codecPendingRecording;
final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger;
const String pigeonVar_channelName =
'dev.flutter.pigeon.camera_android_camerax.PendingRecording.withAudioEnabled';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[this, initialMuted],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as PendingRecording?)!;
}
}

/// Starts the recording, making it an active recording.
Future<Recording> start(VideoRecordEventListener listener) async {
final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,9 @@ abstract class VideoRecordEventListener {
),
)
abstract class PendingRecording {
/// Enables/disables audio to be recorded for this recording.
PendingRecording withAudioEnabled(bool initialMuted);

/// Starts the recording, making it an active recording.
Recording start(VideoRecordEventListener listener);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_android_camerax/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_android_camerax
description: Android implementation of the camera plugin using the CameraX library.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.6.17+1
version: 0.6.18

environment:
sdk: ^3.7.0
Expand Down
Loading