Skip to content

[camera] add heif support image iOS #4586

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 95 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
8c63615
add heic support image iOS
Mairramer Jul 29, 2023
78a86ec
format files
Mairramer Jul 29, 2023
a43d149
Merge branch 'main' into add-heic-support-ios
Mairramer Jul 29, 2023
88c7228
update version camera_platform_interface
Mairramer Jul 29, 2023
a90a81b
fix version
Mairramer Jul 29, 2023
a98c047
new test
Mairramer Jul 29, 2023
19b6ee2
update example
Mairramer Jul 29, 2023
44b7f43
refactor and adjusts code
Mairramer Jul 30, 2023
c67bf61
Update FLTCam_Test.h
Mairramer Jul 30, 2023
5f9a5fc
Update FLTCam.m
Mairramer Jul 30, 2023
88bb20d
Update CameraPlugin.m
Mairramer Jul 30, 2023
1e77287
new adjusts
Mairramer Jul 30, 2023
2abeabe
Update FLTCam.h
Mairramer Jul 30, 2023
d889f8b
Update CHANGELOG.md
Mairramer Jul 30, 2023
53a5838
fix versions
Mairramer Jul 30, 2023
89905f7
fix versions
Mairramer Jul 30, 2023
c748a86
fix versions to NEXT
Mairramer Jul 30, 2023
4d7b623
versions next section
Mairramer Jul 30, 2023
f0a42b2
Merge branch 'main' into add-heic-support-ios
Mairramer Jul 30, 2023
42cdd9d
fix tests
Mairramer Jul 31, 2023
897414f
Merge branch 'main' into add-heic-support-ios
Mairramer Aug 2, 2023
e7c21a1
Merge branch 'main' into add-heic-support-ios
Mairramer Aug 3, 2023
4ee2527
refactor code
Mairramer Aug 3, 2023
a446d01
added default value
Mairramer Aug 4, 2023
d5e261d
Merge remote-tracking branch 'origin/main' into add-heic-support-ios
Mairramer Oct 13, 2023
c5a812a
rebase main
Mairramer Oct 14, 2023
d15112e
fix header
Mairramer Oct 14, 2023
f906e99
format code
Mairramer Oct 14, 2023
2bad48f
Merge branch 'main' into add-heic-support-ios
Mairramer Oct 14, 2023
b6be5b5
add interface
Mairramer Oct 14, 2023
f57a9c2
fixes
Mairramer Oct 14, 2023
729b7a2
add interface
Mairramer Oct 14, 2023
d14f6cb
added test
Mairramer Oct 14, 2023
305c3b4
improves
Mairramer Oct 14, 2023
e62f6d2
rename extension
Mairramer Oct 15, 2023
4fc1da0
format
Mairramer Oct 15, 2023
3df56b3
refactor code
Mairramer Oct 18, 2023
3cb6af3
fix tests
Mairramer Oct 18, 2023
8564f1c
fix test
Mairramer Oct 18, 2023
cf53b1c
remove comment
Mairramer Oct 18, 2023
dc2fb21
code format
Mairramer Oct 18, 2023
c22989e
fix format
Mairramer Oct 18, 2023
a95ccef
format code
Mairramer Oct 18, 2023
97408df
fix format
Mairramer Oct 18, 2023
a5b8f1b
code improve
Mairramer Oct 19, 2023
1a39b9a
fix
Mairramer Oct 19, 2023
2b94bdb
improve code
Mairramer Oct 19, 2023
fb88b94
fix test
Mairramer Oct 19, 2023
59f1a10
format code
Mairramer Oct 20, 2023
383afa2
Merge branch 'main' into add-heic-support-ios
Mairramer Oct 20, 2023
7d9f5f5
fix mistake
Mairramer Oct 20, 2023
4bc9eb8
fix update android
Mairramer Oct 20, 2023
998d2b6
rename file
Mairramer Oct 20, 2023
441eb68
fix export
Mairramer Oct 20, 2023
c5ac5b6
update version
Mairramer Oct 20, 2023
0e55574
Merge branch 'main' into add-heic-support-ios
Mairramer Oct 20, 2023
6ad2422
refactor logic
Mairramer Oct 20, 2023
d728a4f
Merge branch 'main' into add-heic-support-ios
Mairramer Oct 20, 2023
4846d4a
improve code
Mairramer Oct 22, 2023
b010609
format code
Mairramer Oct 22, 2023
369aa1b
clang format
Mairramer Oct 23, 2023
f07bc24
next to camera
Mairramer Oct 23, 2023
c9f4df4
Merge branch 'main' into add-heic-support-ios
Mairramer Oct 23, 2023
b2ea23d
Merge branch 'main' into add-heic-support-ios
Mairramer Oct 23, 2023
eb49544
some changes
Mairramer Oct 31, 2023
73680ff
Merge branch 'main' into add-heic-support-ios
Mairramer Nov 1, 2023
23375ab
fix false positive tests
Mairramer Nov 1, 2023
0bdd96a
fix
Mairramer Nov 1, 2023
a7ede43
fix changelog
Mairramer Nov 2, 2023
7ae8540
fix tests
Mairramer Nov 3, 2023
5ed3a45
Merge branch 'main' into add-heic-support-ios
Mairramer Nov 17, 2023
c95d0f0
rename
Mairramer Nov 17, 2023
572eef1
Merge branch 'main' into add-heic-support-ios
Mairramer Nov 28, 2023
09e8f0a
new changes
Mairramer Nov 28, 2023
e271274
remove changes camera
Mairramer Nov 28, 2023
2338ac7
fix
Mairramer Nov 28, 2023
33b2dde
fix import
Mairramer Nov 28, 2023
df51b0b
new changes
Mairramer Nov 28, 2023
be233cb
fix
Mairramer Nov 28, 2023
f4b77ec
fix
Mairramer Nov 28, 2023
5cbb66b
new fixes
Mairramer Nov 29, 2023
4079924
format code
Mairramer Nov 29, 2023
9410af7
update some wording
bparrishMines Dec 5, 2023
d32d324
Merge branch 'main' of github.com:flutter/packages into add-heic-supp…
bparrishMines Dec 5, 2023
396a1bc
Merge branch 'main' into add-heic-support-ios
Mairramer Dec 22, 2023
344d22e
remove federated
Mairramer Dec 22, 2023
8bf202d
Merge branch 'main' into add-heic-support-ios
Mairramer Dec 28, 2023
0587ba5
Merge branch 'main' into add-heic-support-ios
Mairramer Jan 4, 2024
25b0676
Merge branch 'main' into add-heic-support-ios
Mairramer Jan 6, 2024
ebc79ba
Merge branch 'main' into add-heic-support-ios
Mairramer Jan 13, 2024
570db5f
Merge branch 'main' into add-heic-support-ios
Mairramer Jan 17, 2024
3d26f58
Merge branch 'main' into add-heic-support-ios
Mairramer Jan 19, 2024
feeff1f
Merge branch 'main' into add-heic-support-ios
Mairramer Jan 24, 2024
9588e3e
Update project.pbxproj
bparrishMines Jan 24, 2024
dbf5161
Merge branch 'main' into add-heic-support-ios
bparrishMines Jan 24, 2024
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
4 changes: 4 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.14

* Adds support to HEIF format.

## 0.9.13+11

* Fixes a memory leak of sample buffer when pause and resume the video recording.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,25 @@ void main() {

expect(await completer.future, isNotNull);
});

// Test fileFormat is respected when taking a picture.
testWidgets('Capture specific image output formats',
(WidgetTester tester) async {
final List<CameraDescription> cameras =
await CameraPlatform.instance.availableCameras();
if (cameras.isEmpty) {
return;
}
for (final CameraDescription cameraDescription in cameras) {
for (final ImageFileFormat fileFormat in ImageFileFormat.values) {
final CameraController controller =
CameraController(cameraDescription, ResolutionPreset.low);
await controller.initialize();
await controller.setImageFileFormat(fileFormat);
final XFile file = await controller.takePicture();
await controller.dispose();
expect(file.path.endsWith(fileFormat.name), true);
}
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,12 @@ - (void)testFLTGetStringForUIDeviceOrientation {
XCTAssertEqualObjects(@"portraitUp", FLTGetStringForUIDeviceOrientation(-1));
}

#pragma mark - file format tests

- (void)testFLTGetFileFormatForString {
XCTAssertEqual(FCPFileFormatJPEG, FCPGetFileFormatFromString(@"jpg"));
XCTAssertEqual(FCPFileFormatHEIF, FCPGetFileFormatFromString(@"heif"));
XCTAssertEqual(FCPFileFormatInvalid, FCPGetFileFormatFromString(@"unknown"));
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,90 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testCaptureToFile_mustReportFileExtensionWithHeifWhenHEVCIsAvailableAndFileFormatIsHEIF {
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Test must set extension to heif if availablePhotoCodecTypes contains HEVC."];
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
(void *)FLTCaptureSessionQueueSpecific, NULL);
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
[cam setImageFileFormat:FCPFileFormatHEIF];

AVCapturePhotoSettings *settings =
[AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey : AVVideoCodecTypeHEVC}];

id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettingsWithFormat:OCMOCK_ANY]).andReturn(settings);

id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
NSString *filePath;
[invocation getArgument:&filePath atIndex:2];
XCTAssertEqualObjects([filePath pathExtension], @"heif");
[expectation fulfill];
});

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
// Set availablePhotoCodecTypes to HEVC
NSArray *codecTypes = @[ AVVideoCodecTypeHEVC ];
OCMStub([mockOutput availablePhotoCodecTypes]).andReturn(codecTypes);

OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(delegate.filePath, nil);
});
});
cam.capturePhotoOutput = mockOutput;
// `FLTCam::captureToFile` runs on capture session queue.
dispatch_async(captureSessionQueue, ^{
[cam captureToFile:mockResult];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testCaptureToFile_mustReportFileExtensionWithJpgWhenHEVCNotAvailableAndFileFormatIsHEIF {
XCTestExpectation *expectation = [self
expectationWithDescription:
@"Test must set extension to jpg if availablePhotoCodecTypes does not contain HEVC."];
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
(void *)FLTCaptureSessionQueueSpecific, NULL);
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
[cam setImageFileFormat:FCPFileFormatHEIF];

AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
OCMStub([mockSettings photoSettings]).andReturn(settings);

id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
NSString *filePath;
[invocation getArgument:&filePath atIndex:2];
XCTAssertEqualObjects([filePath pathExtension], @"jpg");
[expectation fulfill];
});

id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);

OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
// Completion runs on IO queue.
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
dispatch_async(ioQueue, ^{
delegate.completionHandler(delegate.filePath, nil);
});
});
cam.capturePhotoOutput = mockOutput;
// `FLTCam::captureToFile` runs on capture session queue.
dispatch_async(captureSessionQueue, ^{
[cam captureToFile:mockResult];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@ class CameraController extends ValueNotifier<CameraValue> {
value = value.copyWith(focusMode: mode);
}

/// Sets the output format for taking pictures.
Future<void> setImageFileFormat(ImageFileFormat format) async {
await CameraPlatform.instance.setImageFileFormat(_cameraId, format);
}

/// Releases the resources of this camera.
@override
Future<void> dispose() async {
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_avfoundation/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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: ../
camera_platform_interface: ^2.4.0
camera_platform_interface: ^2.7.0
flutter:
sdk: flutter
path_provider: ^2.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue;
if ([@"initialize" isEqualToString:call.method]) {
NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]);

[_camera setVideoFormat:FLTGetVideoFormatFromString(videoFormatValue)];

__weak CameraPlugin *weakSelf = self;
Expand Down Expand Up @@ -255,6 +256,9 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
[_camera resumePreviewWithResult:result];
} else if ([@"setDescriptionWhileRecording" isEqualToString:call.method]) {
[_camera setDescriptionWhileRecording:(call.arguments[@"cameraName"]) result:result];
} else if ([@"setImageFileFormat" isEqualToString:call.method]) {
NSString *fileFormat = call.arguments[@"fileFormat"];
[_camera setImageFileFormat:FCPGetFileFormatFromString(fileFormat)];
} else {
[result sendNotImplemented];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,20 @@ extern FLTResolutionPreset FLTGetFLTResolutionPresetForString(NSString *preset);
*/
extern OSType FLTGetVideoFormatFromString(NSString *videoFormatString);

/**
* Represents image format. Mirrors ImageFileFormat in camera.dart.
*/
typedef NS_ENUM(NSInteger, FCPFileFormat) {
FCPFileFormatJPEG,
FCPFileFormatHEIF,
FCPFileFormatInvalid,
};

#pragma mark - image extension

/**
* Gets a string representation of ImageFileFormat.
*/
extern FCPFileFormat FCPGetFileFormatFromString(NSString *fileFormatString);

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,15 @@ OSType FLTGetVideoFormatFromString(NSString *videoFormatString) {
return kCVPixelFormatType_32BGRA;
}
}

#pragma mark - file format

FCPFileFormat FCPGetFileFormatFromString(NSString *fileFormatString) {
if ([fileFormatString isEqualToString:@"jpg"]) {
return FCPFileFormatJPEG;
} else if ([fileFormatString isEqualToString:@"heif"]) {
return FCPFileFormatHEIF;
} else {
return FCPFileFormatInvalid;
}
}
2 changes: 2 additions & 0 deletions packages/camera/camera_avfoundation/ios/Classes/FLTCam.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(assign, nonatomic) FLTFlashMode flashMode;
// Format used for video and image streaming.
@property(assign, nonatomic) FourCharCode videoFormat;
@property(assign, nonatomic) FCPFileFormat fileFormat;

/// Initializes an `FLTCam` instance.
/// @param cameraName a name used to uniquely identify the camera.
Expand All @@ -50,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)captureToFile:(FLTThreadSafeFlutterResult *)result;
- (void)close;
- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result;
- (void)setImageFileFormat:(FCPFileFormat)fileFormat;
/**
* Starts recording a video with an optional streaming messenger.
* If the messenger is non-null then it will be called for each
Expand Down
21 changes: 20 additions & 1 deletion packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName
_deviceOrientation = orientation;
_videoFormat = kCVPixelFormatType_32BGRA;
_inProgressSavePhotoDelegates = [NSMutableDictionary dictionary];
_fileFormat = FCPFileFormatJPEG;

// To limit memory consumption, limit the number of frames pending processing.
// After some testing, 4 was determined to be the best maximum value.
Expand Down Expand Up @@ -218,6 +219,10 @@ - (void)setVideoFormat:(OSType)videoFormat {
@{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)};
}

- (void)setImageFileFormat:(FCPFileFormat)fileFormat {
_fileFormat = fileFormat;
}

- (void)setDeviceOrientation:(UIDeviceOrientation)orientation {
if (_deviceOrientation == orientation) {
return;
Expand Down Expand Up @@ -254,16 +259,30 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation

- (void)captureToFile:(FLTThreadSafeFlutterResult *)result {
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];

if (_resolutionPreset == FLTResolutionPresetMax) {
[settings setHighResolutionPhotoEnabled:YES];
}

NSString *extension;

BOOL isHEVCCodecAvailable =
[self.capturePhotoOutput.availablePhotoCodecTypes containsObject:AVVideoCodecTypeHEVC];

if (_fileFormat == FCPFileFormatHEIF && isHEVCCodecAvailable) {
settings =
[AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey : AVVideoCodecTypeHEVC}];
extension = @"heif";
} else {
extension = @"jpg";
}

AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(_flashMode);
if (avFlashMode != -1) {
[settings setFlashMode:avFlashMode];
}
NSError *error;
NSString *path = [self getTemporaryFilePathWithExtension:@"jpg"
NSString *path = [self getTemporaryFilePathWithExtension:extension
subfolder:@"pictures"
prefix:@"CAP_"
error:error];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output
}];
}

- (NSString *)filePath {
return self.path;
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
/// Exposed for unit tests to manually trigger the completion.
@property(readonly, nonatomic) FLTSavePhotoDelegateCompletionHandler completionHandler;

/// The path for captured photo file.
/// Exposed for unit tests to verify the captured photo file path.
@property(readwrite, nonatomic) NSString *filePath;

/// Handler to write captured photo data into a file.
/// @param error the capture error.
/// @param photoDataProvider a closure that provides photo data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,17 @@ class AVFoundationCamera extends CameraPlatform {
);
}

@override
Future<void> setImageFileFormat(int cameraId, ImageFileFormat format) {
return _channel.invokeMethod<void>(
'setImageFileFormat',
<String, dynamic>{
'cameraId': cameraId,
'fileFormat': format.name,
},
);
}

@override
Widget buildPreview(int cameraId) {
return Texture(textureId: cameraId);
Expand Down
5 changes: 3 additions & 2 deletions packages/camera/camera_avfoundation/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_avfoundation
description: iOS implementation of the camera plugin.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.9.13+11
version: 0.9.14

environment:
sdk: ^3.2.3
Expand All @@ -17,7 +17,7 @@ flutter:
dartPluginClass: AVFoundationCamera

dependencies:
camera_platform_interface: ^2.4.0
camera_platform_interface: ^2.7.0
flutter:
sdk: flutter
stream_transform: ^2.0.0
Expand All @@ -29,3 +29,4 @@ dev_dependencies:

topics:
- camera

Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,46 @@ void main() {
isMethodCall('stopImageStream', arguments: null),
]);
});

test('Should set the ImageFileFormat to heif', () async {
// Arrange
final MethodChannelMock channel = MethodChannelMock(
channelName: _channelName,
methods: <String, dynamic>{'setImageFileFormat': 'heif'},
);

// Act
await camera.setImageFileFormat(cameraId, ImageFileFormat.heif);

// Assert
expect(channel.log, <Matcher>[
isMethodCall('setImageFileFormat', arguments: <String, Object?>{
'cameraId': cameraId,
'fileFormat': 'heif',
}),
]);
});

test('Should set the ImageFileFormat to jpeg', () async {
// Arrange
final MethodChannelMock channel = MethodChannelMock(
channelName: _channelName,
methods: <String, dynamic>{
'setImageFileFormat': 'jpeg',
},
);

// Act
await camera.setImageFileFormat(cameraId, ImageFileFormat.jpeg);

// Assert
expect(channel.log, <Matcher>[
isMethodCall('setImageFileFormat', arguments: <String, Object?>{
'cameraId': cameraId,
'fileFormat': 'jpeg',
}),
]);
});
});
}

Expand Down