Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0e59dbc
Fixes flutter/flutter#148013: setDescriptionWhileRecording with andro…
blackorbs-dev Aug 24, 2025
fcc11be
Fixes flutter/flutter#148013: updated Version and Changelog
blackorbs-dev Aug 24, 2025
971ebc8
Address review feedback: code fix
blackorbs-dev Aug 25, 2025
87c985e
Address review feedback: implement code fixes and suggestions
blackorbs-dev Aug 25, 2025
80d3c61
Merge branch 'main' into fix-issue-148013
blackorbs-dev Aug 27, 2025
0461e63
Update camera_android_camerax readme, revert changes for unaffected c…
blackorbs-dev Aug 27, 2025
f4a076d
Merge branch 'main' into fix-issue-148013
blackorbs-dev Aug 27, 2025
1a8397e
Merge branch 'main' into fix-issue-148013
blackorbs-dev Aug 28, 2025
a631f15
fix merge issue with main
blackorbs-dev Aug 28, 2025
4eece12
update version and changelog
blackorbs-dev Aug 28, 2025
3ac789c
Implement code fixes and suggestions
blackorbs-dev Aug 29, 2025
53e3aa3
Merge branch 'main' into fix-issue-148013
blackorbs-dev Aug 29, 2025
8fbb01c
Address some nits
blackorbs-dev Sep 3, 2025
41aac10
Merge branch 'main' into fix-issue-148013
blackorbs-dev Sep 3, 2025
b1c2ac1
Reformat merge from main
blackorbs-dev Sep 3, 2025
1ddc209
Merge branch 'main' into fix-issue-148013
blackorbs-dev Sep 9, 2025
3b2164f
checkout camera_avfoundation to upstream main
blackorbs-dev Sep 9, 2025
d73bccb
update camera_android_camerax readme
blackorbs-dev Sep 9, 2025
69eb480
Merge branch 'main' into fix-issue-148013
blackorbs-dev Sep 10, 2025
3e799c2
implement version bump for camera_platform_interface
blackorbs-dev Sep 11, 2025
5520c89
implement changes for platform consistency
blackorbs-dev Sep 11, 2025
8614a35
format code changes
blackorbs-dev Sep 11, 2025
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
3 changes: 2 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.11.2+1

* Fixes flutter/flutter#148013: setDescriptionWhileRecording with android camerax.
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.

## 0.11.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ void main() {
await controller.initialize();
await controller.prepareForVideoRecording();

await controller.startVideoRecording();
await controller.startVideoRecording(
enableAndroidPersistentRecording: true,
);
await controller.setDescription(cameras[1]);

expect(controller.description, cameras[1]);
Expand Down
5 changes: 5 additions & 0 deletions packages/camera/camera/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ dev_dependencies:

flutter:
uses-material-design: true
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
camera_android_camerax: {path: ../../../../packages/camera/camera_android_camerax}
camera_platform_interface: {path: ../../../../packages/camera/camera_platform_interface}
19 changes: 18 additions & 1 deletion packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,12 @@ class CameraController extends ValueNotifier<CameraValue> {

/// Sets the description of the camera.
///
/// By default, if a video recording is in progress, calling this method will cancel the current recording on Android.
///
/// To keep recording active while switching cameras on Android, start the
/// recording with [startVideoRecording] and set
/// `enableAndroidPersistentRecording` to `true`.
///
/// Throws a [CameraException] if setting the description fails.
Future<void> setDescription(CameraDescription description) async {
if (value.isRecordingVideo) {
Expand Down Expand Up @@ -554,8 +560,15 @@ class CameraController extends ValueNotifier<CameraValue> {
///
/// The video is returned as a [XFile] after calling [stopVideoRecording].
/// Throws a [CameraException] if the capture fails.
///
/// The [enableAndroidPersistentRecording] parameter is only available on Android.
/// If set to true, configures the recording to be a persistent recording.
/// A persistent recording will only be stopped by explicitly calling [stopVideoRecording]
/// and will ignore events that would normally cause recording to stop,
/// such as lifecycle events or explicit calls to [setDescription] while recording is in progress.
Future<void> startVideoRecording({
onLatestImageAvailable? onAvailable,
bool enableAndroidPersistentRecording = false,
}) async {
_throwIfNotInitialized('startVideoRecording');
if (value.isRecordingVideo) {
Expand All @@ -574,7 +587,11 @@ class CameraController extends ValueNotifier<CameraValue> {

try {
await CameraPlatform.instance.startVideoCapturing(
VideoCaptureOptions(_cameraId, streamCallback: streamCallback),
VideoCaptureOptions(
_cameraId,
streamCallback: streamCallback,
enableAndroidPersistentRecording: enableAndroidPersistentRecording,
),
);
value = value.copyWith(
isRecordingVideo: true,
Expand Down
7 changes: 6 additions & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing
Dart.
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
version: 0.11.2
version: 0.11.2+1

environment:
sdk: ^3.7.0
Expand Down Expand Up @@ -38,3 +38,8 @@ dev_dependencies:

topics:
- camera
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
camera_android_camerax: {path: ../../../packages/camera/camera_android_camerax}
camera_platform_interface: {path: ../../../packages/camera/camera_platform_interface}
1 change: 1 addition & 0 deletions packages/camera/camera/test/camera_preview_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class FakeController extends ValueNotifier<CameraValue>
@override
Future<void> startVideoRecording({
onLatestImageAvailable? onAvailable,
bool enableAndroidPersistentRecording = false,
}) async {}

@override
Expand Down
4 changes: 4 additions & 0 deletions packages/camera/camera_android/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ dev_dependencies:

flutter:
uses-material-design: true
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
camera_platform_interface: {path: ../../../../packages/camera/camera_platform_interface}
4 changes: 4 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.22

* Implements `setDescriptionWhileRecording`.

## 0.6.21

* Implements NV21 support for image streaming.
Expand Down
4 changes: 0 additions & 4 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ use cases, the plugin behaves according to the following:
video recording and image streaming is supported, but concurrent video recording, image
streaming, and image capture is not supported.

### `setDescriptionWhileRecording` is unimplemented [Issue #148013][148013]
`setDescriptionWhileRecording`, used to switch cameras while recording video, is currently unimplemented
due to this not currently being supported by CameraX.

### 240p resolution configuration for video recording

240p resolution configuration for video recording is unsupported by CameraX, and thus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3652,6 +3652,22 @@ abstract class PigeonApiPendingRecording(
initialMuted: Boolean
): androidx.camera.video.PendingRecording

/**
* Configures the recording to be a persistent recording.
*
* A persistent recording will only be stopped by explicitly calling [Recording.stop] or
* [Recording.close] and will ignore events that would normally cause recording to stop, such as
* lifecycle events or explicit unbinding of a [VideoCapture] use case that the recording's
* Recorder is attached to.
*
* To switch to a different camera stream while a recording is in progress, first create the
* recording as persistent recording, then rebind the [VideoCapture] it's associated with to a
* different camera.
*/
abstract fun asPersistentRecording(
pigeon_instance: androidx.camera.video.PendingRecording
): androidx.camera.video.PendingRecording

/** Starts the recording, making it an active recording. */
abstract fun start(
pigeon_instance: androidx.camera.video.PendingRecording,
Expand Down Expand Up @@ -3685,6 +3701,28 @@ abstract class PigeonApiPendingRecording(
channel.setMessageHandler(null)
}
}
run {
val channel =
BasicMessageChannel<Any?>(
binaryMessenger,
"dev.flutter.pigeon.camera_android_camerax.PendingRecording.asPersistentRecording",
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 wrapped: List<Any?> =
try {
listOf(api.asPersistentRecording(pigeon_instanceArg))
} 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 @@ -7,6 +7,7 @@
import android.Manifest;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.camera.video.ExperimentalPersistentRecording;
import androidx.camera.video.PendingRecording;
import androidx.camera.video.Recording;
import androidx.core.content.ContextCompat;
Expand All @@ -27,6 +28,13 @@ public ProxyApiRegistrar getPigeonRegistrar() {
return (ProxyApiRegistrar) super.getPigeonRegistrar();
}

@ExperimentalPersistentRecording
@NonNull
@Override
public PendingRecording asPersistentRecording(PendingRecording pigeonInstance) {
return pigeonInstance.asPersistentRecording();
}

@NonNull
@Override
public PendingRecording withAudioEnabled(PendingRecording pigeonInstance, boolean initialMuted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ public void withAudioEnabled_doesNotEnableAudioWhenAudioNotRequested() {
verify(instance).withAudioEnabled(true);
}

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

when(instance.asPersistentRecording()).thenReturn(persistentInstance);

assertEquals(persistentInstance, api.asPersistentRecording(instance));
verify(instance).asPersistentRecording();
}

@Test
public void start_callsStartOnInstance() {
final PigeonApiPendingRecording api =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,62 @@ void main() {

expect(duration, lessThan(recordingTime - timePaused));
}, skip: skipFor157181);

testWidgets('Switch camera while recording captures full video', (
WidgetTester tester,
) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}

final CameraController controller = CameraController(
cameras[0],
mediaSettings: const MediaSettings(
resolutionPreset: ResolutionPreset.medium,
),
);
await controller.initialize();
await controller.prepareForVideoRecording();

await controller.startVideoRecording(enablePersistentRecording: true);

sleep(const Duration(seconds: 2));

await controller.setDescription(
cameras.firstWhere(
(CameraDescription description) =>
description != controller.description,
orElse: () => cameras.first,
),
);

sleep(const Duration(seconds: 1));

await controller.setDescription(
cameras.firstWhere(
(CameraDescription description) =>
description != controller.description,
orElse: () => cameras.first,
),
);

sleep(const Duration(seconds: 1));

final XFile file = await controller.stopVideoRecording();

final File videoFile = File(file.path);
final VideoPlayerController videoController = VideoPlayerController.file(
videoFile,
);
await videoController.initialize();
final int duration = videoController.value.duration.inMilliseconds;
await videoController.dispose();

expect(
duration,
greaterThanOrEqualTo(const Duration(seconds: 4).inMilliseconds),
);
await controller.dispose();
}, skip: skipFor157181);
}
Loading