Skip to content

[image_picker] Add the ability to pick multiple videos #9775

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 18 commits into from
Aug 16, 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
3 changes: 2 additions & 1 deletion packages/image_picker/image_picker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 1.2.0

* Adds `pickMultiVideo` to allow selecting multiple videos from the gallery.
* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.

## 1.1.2
Expand Down
65 changes: 43 additions & 22 deletions packages/image_picker/image_picker/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class _MyHomePageState extends State<MyHomePage> {
Future<void> _playVideo(XFile? file) async {
if (file != null && mounted) {
await _disposeVideoController();
late VideoPlayerController controller;
final VideoPlayerController controller;
if (kIsWeb) {
controller = VideoPlayerController.networkUrl(Uri.parse(file.path));
} else {
Expand All @@ -85,18 +85,25 @@ class _MyHomePageState extends State<MyHomePage> {
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool allowMultiple = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
if (context.mounted) {
if (isVideo) {
final XFile? file = await _picker.pickVideo(
source: source, maxDuration: const Duration(seconds: 10));
await _playVideo(file);
} else if (isMultiImage) {
final List<XFile> files;
if (allowMultiple) {
files = await _picker.pickMultiVideo();
} else {
final XFile? file = await _picker.pickVideo(
source: source, maxDuration: const Duration(seconds: 10));
files = <XFile>[if (file != null) file];
}
// Just play the first file, to keep the example simple.
await _playVideo(files.firstOrNull);
} else if (allowMultiple) {
await _displayPickImageDialog(context, true, (double? maxWidth,
double? maxHeight, int? quality, int? limit) async {
try {
Expand Down Expand Up @@ -349,7 +356,7 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'image0',
tooltip: 'Pick Image from gallery',
tooltip: 'Pick image from gallery',
child: const Icon(Icons.photo),
),
),
Expand All @@ -361,12 +368,11 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
allowMultiple: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
heroTag: 'image1',
tooltip: 'Pick multiple images',
child: const Icon(Icons.photo_library),
),
),
Expand All @@ -382,8 +388,8 @@ class _MyHomePageState extends State<MyHomePage> {
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
tooltip: 'Pick item from gallery',
child: const Icon(Icons.photo_outlined),
),
),
Padding(
Expand All @@ -394,12 +400,13 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
allowMultiple: true,
isMedia: true,
);
},
heroTag: 'image1',
tooltip: 'Pick Multiple Image from gallery',
child: const Icon(Icons.photo_library),
heroTag: 'multipleMedia',
tooltip: 'Pick multiple items',
child: const Icon(Icons.photo_library_outlined),
),
),
if (_picker.supportsImageSource(ImageSource.camera))
Expand All @@ -411,7 +418,7 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'image2',
tooltip: 'Take a Photo',
tooltip: 'Take a photo',
child: const Icon(Icons.camera_alt),
),
),
Expand All @@ -423,8 +430,22 @@ class _MyHomePageState extends State<MyHomePage> {
isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video0',
tooltip: 'Pick Video from gallery',
heroTag: 'video',
tooltip: 'Pick video from gallery',
child: const Icon(Icons.video_file),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_onImageButtonPressed(ImageSource.gallery,
context: context, allowMultiple: true);
},
heroTag: 'multiVideo',
tooltip: 'Pick multiple videos',
child: const Icon(Icons.video_library),
),
),
Expand All @@ -437,8 +458,8 @@ class _MyHomePageState extends State<MyHomePage> {
isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'video1',
tooltip: 'Take a Video',
heroTag: 'takeVideo',
tooltip: 'Take a video',
child: const Icon(Icons.videocam),
),
),
Expand Down
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
image_picker_platform_interface: ^2.10.0
image_picker_platform_interface: ^2.11.0
mime: ^1.0.4
video_player: ^2.7.0

Expand Down
28 changes: 28 additions & 0 deletions packages/image_picker/image_picker/lib/image_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,34 @@ class ImagePicker {
);
}

/// Returns a [List<XFile>] of the videos that were picked.
///
/// The returned [List<XFile>] is intended to be used within a single app
/// session. Do not save the file path and use it across sessions.
///
/// The videos come from the gallery.
///
/// The [maxDuration] argument specifies the maximum duration of the captured
/// videos. If no [maxDuration] is specified, the maximum duration will be
/// infinite. This value may be ignored by platforms that cannot support it.
///
/// The `limit` parameter modifies the maximum number of videos that can be
/// selected. This value may be ignored by platforms that cannot support it.
///
/// The method can throw a [PlatformException] if the video selection process
/// fails.
Future<List<XFile>> pickMultiVideo({
Duration? maxDuration,
int? limit,
}) {
return platform.getMultiVideoWithOptions(
options: MultiVideoPickerOptions(
maxDuration: maxDuration,
limit: limit,
),
);
}

/// Retrieve the lost [XFile] when [pickImage], [pickMultiImage] or [pickVideo] failed because the MainActivity
/// is destroyed. (Android only)
///
Expand Down
16 changes: 8 additions & 8 deletions packages/image_picker/image_picker/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image
library, and taking new pictures with the camera.
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 1.1.2
version: 1.2.0

environment:
sdk: ^3.6.0
Expand All @@ -28,13 +28,13 @@ flutter:
dependencies:
flutter:
sdk: flutter
image_picker_android: ^0.8.7
image_picker_for_web: ">=2.2.0 <4.0.0"
image_picker_ios: ^0.8.8
image_picker_linux: ^0.2.1
image_picker_macos: ^0.2.1
image_picker_platform_interface: ^2.10.0
image_picker_windows: ^0.2.1
image_picker_android: ^0.8.13
image_picker_for_web: ^3.1.0
image_picker_ios: ^0.8.13
image_picker_linux: ^0.2.2
image_picker_macos: ^0.2.2
image_picker_platform_interface: ^2.11.0
image_picker_windows: ^0.2.2

dev_dependencies:
build_runner: ^2.1.10
Expand Down
39 changes: 39 additions & 0 deletions packages/image_picker/image_picker/test/image_picker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,45 @@ void main() {
expect(response.exception!.message, 'test_error_message');
});
});

group('#pickMultiVideo', () {
setUp(() {
when(mockPlatform.getMultiVideoWithOptions(
options: anyNamed('options'),
)).thenAnswer((Invocation _) async => <XFile>[]);
});

test('passes the arguments correctly', () async {
final ImagePicker picker = ImagePicker();
await picker.pickMultiVideo();
await picker.pickMultiVideo(maxDuration: const Duration(seconds: 10));
await picker.pickMultiVideo(limit: 5);

verifyInOrder(<Object>[
mockPlatform.getMultiVideoWithOptions(
options: argThat(
isInstanceOf<MultiVideoPickerOptions>(),
named: 'options',
)),
mockPlatform.getMultiVideoWithOptions(
options: argThat(
isInstanceOf<MultiVideoPickerOptions>().having(
(MultiVideoPickerOptions options) => options.maxDuration,
'maxDuration',
equals(const Duration(seconds: 10))),
named: 'options',
)),
mockPlatform.getMultiVideoWithOptions(
options: argThat(
isInstanceOf<MultiVideoPickerOptions>().having(
(MultiVideoPickerOptions options) => options.limit,
'limit',
equals(5)),
named: 'options',
)),
]);
});
});
});

group('#Multi images', () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.4.4 from annotations
// Mocks generated by Mockito 5.4.6 from annotations
// in image_picker/test/image_picker_test.dart.
// Do not manually edit this file.

Expand All @@ -19,6 +19,7 @@ import 'package:mockito/mockito.dart' as _i1;
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
Expand Down Expand Up @@ -248,6 +249,19 @@ class MockImagePickerPlatform extends _i1.Mock
returnValue: _i4.Future<List<_i5.XFile>>.value(<_i5.XFile>[]),
) as _i4.Future<List<_i5.XFile>>);

@override
_i4.Future<List<_i5.XFile>> getMultiVideoWithOptions(
{_i2.MultiVideoPickerOptions? options =
const _i2.MultiVideoPickerOptions()}) =>
(super.noSuchMethod(
Invocation.method(
#getMultiVideoWithOptions,
[],
{#options: options},
),
returnValue: _i4.Future<List<_i5.XFile>>.value(<_i5.XFile>[]),
) as _i4.Future<List<_i5.XFile>>);

@override
bool supportsImageSource(_i2.ImageSource? source) => (super.noSuchMethod(
Invocation.method(
Expand Down