Skip to content

Commit ba43d73

Browse files
authored
[camera] add heif support image iOS (#4586)
Add support to heic format on IOS on take picture. Based on flutter/flutter#119795
1 parent 5aa3d35 commit ba43d73

16 files changed

+240
-4
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.14
2+
3+
* Adds support to HEIF format.
4+
15
## 0.9.13+11
26

37
* Fixes a memory leak of sample buffer when pause and resume the video recording.

packages/camera/camera_avfoundation/example/integration_test/camera_test.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,25 @@ void main() {
319319

320320
expect(await completer.future, isNotNull);
321321
});
322+
323+
// Test fileFormat is respected when taking a picture.
324+
testWidgets('Capture specific image output formats',
325+
(WidgetTester tester) async {
326+
final List<CameraDescription> cameras =
327+
await CameraPlatform.instance.availableCameras();
328+
if (cameras.isEmpty) {
329+
return;
330+
}
331+
for (final CameraDescription cameraDescription in cameras) {
332+
for (final ImageFileFormat fileFormat in ImageFileFormat.values) {
333+
final CameraController controller =
334+
CameraController(cameraDescription, ResolutionPreset.low);
335+
await controller.initialize();
336+
await controller.setImageFileFormat(fileFormat);
337+
final XFile file = await controller.takePicture();
338+
await controller.dispose();
339+
expect(file.path.endsWith(fileFormat.name), true);
340+
}
341+
}
342+
});
322343
}

packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,12 @@ - (void)testFLTGetStringForUIDeviceOrientation {
104104
XCTAssertEqualObjects(@"portraitUp", FLTGetStringForUIDeviceOrientation(-1));
105105
}
106106

107+
#pragma mark - file format tests
108+
109+
- (void)testFLTGetFileFormatForString {
110+
XCTAssertEqual(FCPFileFormatJPEG, FCPGetFileFormatFromString(@"jpg"));
111+
XCTAssertEqual(FCPFileFormatHEIF, FCPGetFileFormatFromString(@"heif"));
112+
XCTAssertEqual(FCPFileFormatInvalid, FCPGetFileFormatFromString(@"unknown"));
113+
}
114+
107115
@end

packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,90 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi
9494
[self waitForExpectationsWithTimeout:1 handler:nil];
9595
}
9696

97+
- (void)testCaptureToFile_mustReportFileExtensionWithHeifWhenHEVCIsAvailableAndFileFormatIsHEIF {
98+
XCTestExpectation *expectation =
99+
[self expectationWithDescription:
100+
@"Test must set extension to heif if availablePhotoCodecTypes contains HEVC."];
101+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
102+
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
103+
(void *)FLTCaptureSessionQueueSpecific, NULL);
104+
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
105+
[cam setImageFileFormat:FCPFileFormatHEIF];
106+
107+
AVCapturePhotoSettings *settings =
108+
[AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey : AVVideoCodecTypeHEVC}];
109+
110+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
111+
OCMStub([mockSettings photoSettingsWithFormat:OCMOCK_ANY]).andReturn(settings);
112+
113+
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
114+
OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
115+
NSString *filePath;
116+
[invocation getArgument:&filePath atIndex:2];
117+
XCTAssertEqualObjects([filePath pathExtension], @"heif");
118+
[expectation fulfill];
119+
});
120+
121+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
122+
// Set availablePhotoCodecTypes to HEVC
123+
NSArray *codecTypes = @[ AVVideoCodecTypeHEVC ];
124+
OCMStub([mockOutput availablePhotoCodecTypes]).andReturn(codecTypes);
125+
126+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
127+
.andDo(^(NSInvocation *invocation) {
128+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
129+
// Completion runs on IO queue.
130+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
131+
dispatch_async(ioQueue, ^{
132+
delegate.completionHandler(delegate.filePath, nil);
133+
});
134+
});
135+
cam.capturePhotoOutput = mockOutput;
136+
// `FLTCam::captureToFile` runs on capture session queue.
137+
dispatch_async(captureSessionQueue, ^{
138+
[cam captureToFile:mockResult];
139+
});
140+
[self waitForExpectationsWithTimeout:1 handler:nil];
141+
}
142+
143+
- (void)testCaptureToFile_mustReportFileExtensionWithJpgWhenHEVCNotAvailableAndFileFormatIsHEIF {
144+
XCTestExpectation *expectation = [self
145+
expectationWithDescription:
146+
@"Test must set extension to jpg if availablePhotoCodecTypes does not contain HEVC."];
147+
dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
148+
dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific,
149+
(void *)FLTCaptureSessionQueueSpecific, NULL);
150+
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue);
151+
[cam setImageFileFormat:FCPFileFormatHEIF];
152+
153+
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
154+
id mockSettings = OCMClassMock([AVCapturePhotoSettings class]);
155+
OCMStub([mockSettings photoSettings]).andReturn(settings);
156+
157+
id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
158+
OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
159+
NSString *filePath;
160+
[invocation getArgument:&filePath atIndex:2];
161+
XCTAssertEqualObjects([filePath pathExtension], @"jpg");
162+
[expectation fulfill];
163+
});
164+
165+
id mockOutput = OCMClassMock([AVCapturePhotoOutput class]);
166+
167+
OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY])
168+
.andDo(^(NSInvocation *invocation) {
169+
FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)];
170+
// Completion runs on IO queue.
171+
dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL);
172+
dispatch_async(ioQueue, ^{
173+
delegate.completionHandler(delegate.filePath, nil);
174+
});
175+
});
176+
cam.capturePhotoOutput = mockOutput;
177+
// `FLTCam::captureToFile` runs on capture session queue.
178+
dispatch_async(captureSessionQueue, ^{
179+
[cam captureToFile:mockResult];
180+
});
181+
[self waitForExpectationsWithTimeout:1 handler:nil];
182+
}
97183
@end

packages/camera/camera_avfoundation/example/lib/camera_controller.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,11 @@ class CameraController extends ValueNotifier<CameraValue> {
437437
value = value.copyWith(focusMode: mode);
438438
}
439439

440+
/// Sets the output format for taking pictures.
441+
Future<void> setImageFileFormat(ImageFileFormat format) async {
442+
await CameraPlatform.instance.setImageFileFormat(_cameraId, format);
443+
}
444+
440445
/// Releases the resources of this camera.
441446
@override
442447
Future<void> dispose() async {

packages/camera/camera_avfoundation/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
# The example app is bundled with the plugin so we use a path dependency on
1515
# the parent directory to use the current plugin's version.
1616
path: ../
17-
camera_platform_interface: ^2.4.0
17+
camera_platform_interface: ^2.7.0
1818
flutter:
1919
sdk: flutter
2020
path_provider: ^2.0.0

packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
153153
NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue;
154154
if ([@"initialize" isEqualToString:call.method]) {
155155
NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]);
156+
156157
[_camera setVideoFormat:FLTGetVideoFormatFromString(videoFormatValue)];
157158

158159
__weak CameraPlugin *weakSelf = self;
@@ -255,6 +256,9 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
255256
[_camera resumePreviewWithResult:result];
256257
} else if ([@"setDescriptionWhileRecording" isEqualToString:call.method]) {
257258
[_camera setDescriptionWhileRecording:(call.arguments[@"cameraName"]) result:result];
259+
} else if ([@"setImageFileFormat" isEqualToString:call.method]) {
260+
NSString *fileFormat = call.arguments[@"fileFormat"];
261+
[_camera setImageFileFormat:FCPGetFileFormatFromString(fileFormat)];
258262
} else {
259263
[result sendNotImplemented];
260264
}

packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,20 @@ extern FLTResolutionPreset FLTGetFLTResolutionPresetForString(NSString *preset);
127127
*/
128128
extern OSType FLTGetVideoFormatFromString(NSString *videoFormatString);
129129

130+
/**
131+
* Represents image format. Mirrors ImageFileFormat in camera.dart.
132+
*/
133+
typedef NS_ENUM(NSInteger, FCPFileFormat) {
134+
FCPFileFormatJPEG,
135+
FCPFileFormatHEIF,
136+
FCPFileFormatInvalid,
137+
};
138+
139+
#pragma mark - image extension
140+
141+
/**
142+
* Gets a string representation of ImageFileFormat.
143+
*/
144+
extern FCPFileFormat FCPGetFileFormatFromString(NSString *fileFormatString);
145+
130146
NS_ASSUME_NONNULL_END

packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,15 @@ OSType FLTGetVideoFormatFromString(NSString *videoFormatString) {
148148
return kCVPixelFormatType_32BGRA;
149149
}
150150
}
151+
152+
#pragma mark - file format
153+
154+
FCPFileFormat FCPGetFileFormatFromString(NSString *fileFormatString) {
155+
if ([fileFormatString isEqualToString:@"jpg"]) {
156+
return FCPFileFormatJPEG;
157+
} else if ([fileFormatString isEqualToString:@"heif"]) {
158+
return FCPFileFormatHEIF;
159+
} else {
160+
return FCPFileFormatInvalid;
161+
}
162+
}

packages/camera/camera_avfoundation/ios/Classes/FLTCam.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
3030
@property(assign, nonatomic) FLTFlashMode flashMode;
3131
// Format used for video and image streaming.
3232
@property(assign, nonatomic) FourCharCode videoFormat;
33+
@property(assign, nonatomic) FCPFileFormat fileFormat;
3334

3435
/// Initializes an `FLTCam` instance.
3536
/// @param cameraName a name used to uniquely identify the camera.
@@ -50,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
5051
- (void)captureToFile:(FLTThreadSafeFlutterResult *)result;
5152
- (void)close;
5253
- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result;
54+
- (void)setImageFileFormat:(FCPFileFormat)fileFormat;
5355
/**
5456
* Starts recording a video with an optional streaming messenger.
5557
* If the messenger is non-null then it will be called for each

0 commit comments

Comments
 (0)