Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ff2a6fe
feat: add video output path support to camera recording APIs across p…
Mairramer May 5, 2026
ab84efa
more work
Mairramer May 6, 2026
445cfa1
style: fix linting and formatting issues across camera packages
Mairramer May 23, 2026
e25ab0e
fix camera tests
Mairramer May 23, 2026
0a7d03d
fix parameter
Mairramer May 24, 2026
91c91c1
fix param
Mairramer May 24, 2026
3d54807
feat: implement video recording with custom path support and add exam…
Mairramer May 24, 2026
22277b4
feat: add video output path parameter to startVideoRecording method a…
Mairramer May 24, 2026
c75fbaa
refactor: improve code formatting and readability in video recording …
Mairramer May 24, 2026
742c63d
feat: add support for CADisableMinimumFrameDurationOnPhone and UIAppl…
Mairramer May 24, 2026
c5691c7
feat: add support for document browsing in Info.plist
Mairramer May 24, 2026
0dc2583
refactor: remove videoOutputPath support from camera_web and update p…
Mairramer May 25, 2026
d7b0b51
Update packages/camera/camera_avfoundation/ios/camera_avfoundation/So…
Mairramer May 25, 2026
17c0bf6
Update packages/camera/camera_android/android/src/main/java/io/flutte…
Mairramer May 25, 2026
525ec56
Update packages/camera/camera_android_camerax/android/src/main/java/i…
Mairramer May 25, 2026
41572cf
refactor: restrict supported video output format to .mp4 across all p…
Mairramer May 25, 2026
b15fc47
format
Mairramer May 25, 2026
24ba3ac
feat: add documentation for custom video recording paths and remove w…
Mairramer May 25, 2026
7d3544e
fix
Mairramer May 25, 2026
9d1342f
refactor: simplify camera example by removing unused web file helpers…
Mairramer May 25, 2026
67f2bb1
fix
Mairramer May 25, 2026
508b2eb
Update packages/camera/camera/example/lib/video_recording_example.dart
Mairramer May 25, 2026
5b1c082
Merge branch 'main' into feat/add-custom-path-ouput-to-recording
Mairramer May 25, 2026
d663c37
rollback
Mairramer May 25, 2026
0cf24ef
Update packages/camera/camera/example/lib/video_recording_example.dart
Mairramer May 25, 2026
128511d
Update packages/camera/camera/example/lib/video_recording_example.dart
Mairramer May 25, 2026
26600d0
Update packages/camera/camera/example/lib/video_recording_example.dart
Mairramer May 25, 2026
409434d
Update packages/camera/camera/example/lib/video_recording_example.dart
Mairramer May 25, 2026
b2a8629
Update packages/camera/camera_windows/windows/capture_controller.cpp
Mairramer May 25, 2026
9aed89f
fix: improve video path handling in example app and resolve AVAssetWr…
Mairramer May 26, 2026
4905720
feat: add support for custom video output path in video recording acr…
Mairramer May 26, 2026
312453e
docs: remove obsolete video recording example and update platform-spe…
Mairramer Jun 4, 2026
a69ddbe
Merge remote-tracking branch 'origin/main' into feat/add-custom-path-…
Mairramer Jun 4, 2026
ce40316
rebase
Mairramer Jun 4, 2026
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.13.0

* Adds `videoOutputPath` support to `startVideoRecording`.
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.

## 0.12.0+1
Expand Down
24 changes: 21 additions & 3 deletions packages/camera/camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

A Flutter plugin for iOS, Android and Web allowing access to the device cameras.

| | Android | iOS | Web |
|----------------|---------|-----------|------------------------|
| **Support** | SDK 24+ | iOS 13.0+ | [See `camera_web `][1] |
| | Android | iOS | Web |
| ----------- | ------- | --------- | ---------------------- |
| **Support** | SDK 24+ | iOS 13.0+ | [See `camera_web `][1] |

## Features

Expand Down Expand Up @@ -92,6 +92,24 @@ Here is a list of all permission error codes that can be thrown:

- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control).

### Custom Video Recording Path

You can optionally specify a `videoOutputPath` when calling `startVideoRecording()` to save the recorded video directly to a custom absolute file path on the device.

```dart
// Always ensure the path ends with the .mp4 extension
await controller.startVideoRecording(
videoOutputPath: '/path/to/your/custom_video.mp4',
);
```

#### Platform-Specific Considerations

For platform-specific considerations when saving videos, please see the READMEs on pub.dev for their respective platforms:
- [Android](https://pub.dev/packages/camera_android)
- [iOS](https://pub.dev/packages/camera_avfoundation)
- [Windows](https://pub.dev/packages/camera_windows)

### Example

Here is a small example flutter app displaying a full screen camera preview.
Expand Down
12 changes: 8 additions & 4 deletions packages/camera/camera/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
Expand All @@ -28,6 +34,8 @@
<string>Can I use the camera please? Only for demo purpose of the app</string>
<key>NSMicrophoneUsageDescription</key>
<string>Only for demo purpose of the app</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand All @@ -46,9 +54,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
7 changes: 7 additions & 0 deletions packages/camera/camera/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ 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_avfoundation: {path: ../../../../packages/camera/camera_avfoundation}
camera_platform_interface: {path: ../../../../packages/camera/camera_platform_interface}
camera_web: {path: ../../../../packages/camera/camera_web}
9 changes: 9 additions & 0 deletions packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ class CameraController extends ValueNotifier<CameraValue> {
Future<void> startVideoRecording({
onLatestImageAvailable? onAvailable,
bool enablePersistentRecording = true,
String? videoOutputPath,
}) async {
_throwIfNotInitialized('startVideoRecording');
if (value.isRecordingVideo) {
Expand All @@ -578,6 +579,13 @@ class CameraController extends ValueNotifier<CameraValue> {
);
}

if (videoOutputPath != null) {
final String lowerPath = videoOutputPath.toLowerCase();
if (!lowerPath.endsWith('.mp4')) {
throw CameraException('InvalidFilePath', 'Invalid video extension. Supported: .mp4');
}
}

void Function(CameraImageData image)? streamCallback;
if (onAvailable != null) {
streamCallback = (CameraImageData imageData) {
Expand All @@ -591,6 +599,7 @@ class CameraController extends ValueNotifier<CameraValue> {
_cameraId,
streamCallback: streamCallback,
enablePersistentRecording: enablePersistentRecording,
videoOutputPath: videoOutputPath,
),
);
value = value.copyWith(
Expand Down
11 changes: 9 additions & 2 deletions 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.12.0+1
version: 0.13.0

environment:
sdk: ^3.10.0
Expand All @@ -23,7 +23,7 @@ flutter:
dependencies:
camera_android_camerax: ^0.7.0
camera_avfoundation: ^0.10.0
camera_platform_interface: ^2.12.0
camera_platform_interface: ^2.14.0
camera_web: ^0.3.3
flutter:
sdk: flutter
Expand All @@ -38,3 +38,10 @@ 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_avfoundation: {path: ../../../packages/camera/camera_avfoundation}
camera_platform_interface: {path: ../../../packages/camera/camera_platform_interface}
camera_web: {path: ../../../packages/camera/camera_web}
8 changes: 6 additions & 2 deletions packages/camera/camera/test/camera_image_stream_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,14 @@ class MockStreamingCameraPlatform extends MockCameraPlatform {
}

@override
Future<void> startVideoRecording(int cameraId, {Duration? maxVideoDuration}) {
Future<void> startVideoRecording(
int cameraId, {
Duration? maxVideoDuration,
String? videoOutputPath,
}) {
streamCallLog.add('startVideoRecording');
// Ignore maxVideoDuration, as it is unimplemented and deprecated.
return super.startVideoRecording(cameraId);
return super.startVideoRecording(cameraId, videoOutputPath: videoOutputPath);
}

@override
Expand Down
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 @@ -112,6 +112,7 @@ class FakeController extends ValueNotifier<CameraValue> implements CameraControl
Future<void> startVideoRecording({
onLatestImageAvailable? onAvailable,
bool enablePersistentRecording = true,
String? videoOutputPath,
}) async {}

@override
Expand Down
8 changes: 6 additions & 2 deletions packages/camera/camera/test/camera_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3493,9 +3493,13 @@ class MockCameraPlatform extends Mock with MockPlatformInterfaceMixin implements
super.noSuchMethod(Invocation.method(#prepareForVideoRecording, null));

@override
Future<void> startVideoRecording(int cameraId, {Duration? maxVideoDuration}) {
Future<void> startVideoRecording(
int cameraId, {
Duration? maxVideoDuration,
String? videoOutputPath,
}) {
// Ignore maxVideoDuration, as it is unimplemented and deprecated.
return startVideoCapturing(VideoCaptureOptions(cameraId));
return startVideoCapturing(VideoCaptureOptions(cameraId, videoOutputPath: videoOutputPath));
}

@override
Expand Down
4 changes: 4 additions & 0 deletions packages/camera/camera_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.10.11

* Adds support for custom video output path in video recording.

## 0.10.10+18

* Bumps the androidx group across 10 directories with 1 update.
Expand Down
17 changes: 17 additions & 0 deletions packages/camera/camera_android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ $ flutter pub add camera_android
when recording a video with sound enabled and trying to play it back, the duration won't be correct and
you will only see the first frame.

## Custom Video Recording Path

Although it is possible to use an absolute path like `/storage/emulated/0/Download/video.mp4`, this is a fragile practice and may fail on many devices or Android versions due to **Scoped Storage** restrictions.

- **Best Practice:** Always use the [path_provider](https://pub.dev/packages/path_provider) package to fetch a safe, writable directory.
- **Recommended Directory:** Use `getTemporaryDirectory()` or `getApplicationDocumentsDirectory()`.

```dart
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

final directory = await getTemporaryDirectory();
final videoPath = p.join(directory.path, 'my_video.mp4');

await controller.startVideoRecording(videoOutputPath: videoPath);
```

[1]: https://pub.dev/packages/camera
[2]: https://flutter.dev/to/endorsed-federated-plugin
[3]: https://pub.dev/packages/camera_android_camerax
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -869,8 +869,9 @@ void unlockAutoFocus() {
dartMessenger.error(flutterResult, errorCode, errorMessage, null));
}

public void startVideoRecording(@Nullable EventChannel imageStreamChannel) {
prepareRecording();
public void startVideoRecording(
@Nullable EventChannel imageStreamChannel, @Nullable String videoOutputPath) {
prepareRecording(videoOutputPath);

if (imageStreamChannel != null) {
setStreamHandler(imageStreamChannel);
Expand Down Expand Up @@ -1296,13 +1297,19 @@ public void onError(@NonNull String errorCode, @NonNull String errorMessage) {
}

@VisibleForTesting
void prepareRecording() {
final File outputDir = applicationContext.getCacheDir();
try {
captureFile = File.createTempFile("REC", ".mp4", outputDir);
} catch (IOException | SecurityException e) {
throw new Messages.FlutterError("cannotCreateFile", e.getMessage(), null);
void prepareRecording(@Nullable String videoOutputPath) {
if (videoOutputPath != null) {
validateOutputPath(videoOutputPath);
captureFile = new File(videoOutputPath);
} else {
final File outputDir = applicationContext.getCacheDir();
try {
captureFile = File.createTempFile("REC", ".mp4", outputDir);
} catch (IOException | SecurityException e) {
throw new Messages.FlutterError("cannotCreateFile", e.getMessage(), null);
}
}

try {
prepareMediaRecorder(captureFile.getAbsolutePath());
} catch (IOException e) {
Expand All @@ -1317,6 +1324,23 @@ void prepareRecording() {
setFpsCameraFeatureForRecording(cameraProperties);
}

private void validateOutputPath(String path) {
File file = new File(path);
if (file.isDirectory()) {
throw new Messages.FlutterError("IOError", "The output path is a directory: " + path, null);
}
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
throw new Messages.FlutterError(
"IOError", "The parent directory does not exist: " + parent.getAbsolutePath(), null);
}

String lowerPath = path.toLowerCase(Locale.ROOT);
if (!lowerPath.endsWith(".mp4")) {
throw new Messages.FlutterError("IOError", "Invalid video extension. Supported: .mp4", null);
}
}

private void setStreamHandler(EventChannel imageStreamChannel) {
imageStreamChannel.setStreamHandler(
new EventChannel.StreamHandler() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ public void takePicture(@NonNull Messages.Result<String> result) {
}

@Override
public void startVideoRecording(@NonNull Boolean enableStream) {
camera.startVideoRecording(enableStream ? imageStreamChannel : null);
public void startVideoRecording(@NonNull Boolean enableStream, @Nullable String videoOutputPath) {
camera.startVideoRecording(enableStream ? imageStreamChannel : null, videoOutputPath);
}

@NonNull
Expand Down
Loading