Skip to content
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

✨ Add iOS video options #327

Merged
merged 6 commits into from
Apr 27, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,6 @@ class CameraAwesomeX : CameraInterface, FlutterPlugin, ActivityAware {
ContextCompat.getMainExecutor(activity!!),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
Log.d(
"CameraX___", "Photo capture succeeded: ${outputFileResults.savedUri}"
)
if (colorMatrix != null && noneFilter != colorMatrix) {
val exif = ExifInterface(outputFileResults.savedUri!!.path!!)

Expand Down Expand Up @@ -439,8 +436,20 @@ class CameraAwesomeX : CameraInterface, FlutterPlugin, ActivityAware {

@SuppressLint("RestrictedApi", "MissingPermission")
override fun recordVideo(
requests: Map<PigeonSensor, String?>, callback: (Result<Unit>) -> Unit
sensors: List<PigeonSensor>,
paths: List<String?>,
callback: (Result<Unit>) -> Unit
) {
if (sensors.size != paths.size) {
throw Exception("sensors and paths must have the same length")
}
if (paths.size != cameraState.videoCaptures.size) {
throw Exception("paths and imageCaptures must have the same length")
}

val requests = sensors.mapIndexed { index, pigeonSensor ->
pigeonSensor to paths[index]
}.toMap()
// TODO Handle multiple videos requests
val path = requests.values.first()!!
CoroutineScope(Dispatchers.Main).launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,22 +288,25 @@ data class AndroidVideoOptions (

/** Generated class from Pigeon that represents data sent in messages. */
data class CupertinoVideoOptions (
val fileType: String,
val codec: String
val fileType: String? = null,
val codec: String? = null,
val fps: Long? = null

) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): CupertinoVideoOptions {
val fileType = list[0] as String
val codec = list[1] as String
return CupertinoVideoOptions(fileType, codec)
val fileType = list[0] as String?
val codec = list[1] as String?
val fps = list[2].let { if (it is Int) it.toLong() else it as Long? }
return CupertinoVideoOptions(fileType, codec, fps)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
fileType,
codec,
fps,
)
}
}
Expand Down Expand Up @@ -718,7 +721,7 @@ interface CameraInterface {
fun requestPermissions(saveGpsLocation: Boolean, callback: (Result<List<String>>) -> Unit)
fun getPreviewTextureId(cameraPosition: Long): Long
fun takePhoto(sensors: List<PigeonSensor>, paths: List<String?>, callback: (Result<Boolean>) -> Unit)
fun recordVideo(requests: Map<PigeonSensor, String?>, callback: (Result<Unit>) -> Unit)
fun recordVideo(sensors: List<PigeonSensor>, paths: List<String?>, callback: (Result<Unit>) -> Unit)
fun pauseVideoRecording()
fun resumeVideoRecording()
fun receivedImageFromStream()
Expand Down Expand Up @@ -874,8 +877,9 @@ interface CameraInterface {
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val requestsArg = args[0] as Map<PigeonSensor, String?>
api.recordVideo(requestsArg) { result: Result<Unit> ->
val sensorsArg = args[0] as List<PigeonSensor>
val pathsArg = args[1] as List<String?>
api.recordVideo(sensorsArg, pathsArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
Expand Down
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class CameraPage extends StatelessWidget {
}
},
videoOptions: VideoOptions(
ios: CupertinoVideoOptions(
fps: 10,
),
android: AndroidVideoOptions(
bitrate: 6000000,
quality: VideoRecordingQuality.fhd,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ - (void)initCameraPreview:(PigeonSensorPosition)sensor {
_captureConnection = [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports
output:_captureVideoOutput];

// TODO: works but deprecated...
// if ([_captureConnection isVideoMinFrameDurationSupported] && [_captureConnection isVideoMaxFrameDurationSupported]) {
// CMTime frameDuration = CMTimeMake(1, 12);
// [_captureConnection setVideoMinFrameDuration:frameDuration];
// [_captureConnection setVideoMaxFrameDuration:frameDuration];
// } else {
// NSLog(@"Failed to set frame duration");
// }

// Attaching to session
[_captureSession addInputWithNoConnections:_captureVideoInput];
[_captureSession addConnection:_captureConnection];
Expand Down Expand Up @@ -452,7 +461,7 @@ - (void)recordVideoAtPath:(NSString *)path completion:(nonnull void (^)(FlutterE
}

if (!_videoController.isRecording) {
[_videoController recordVideoAtPath:path orientation:_deviceOrientation audioSetupCallback:^{
[_videoController recordVideoAtPath:path captureDevice:_captureDevice orientation:_deviceOrientation audioSetupCallback:^{
[self setUpCaptureSessionForAudioError:^(NSError *error) {
completion([FlutterError errorWithCode:@"VIDEO_ERROR" message:@"error when trying to setup audio" details:[error localizedDescription]]);
}];
Expand Down
4 changes: 3 additions & 1 deletion ios/Classes/Controllers/Video/VideoController.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ typedef void(^OnVideoWriterSetup)(void);
@property(readonly, nonatomic) bool isPaused;
@property(readonly, nonatomic) bool isAudioEnabled;
@property(readonly, nonatomic) bool isAudioSetup;
@property(readonly, nonatomic) CupertinoVideoOptions *options;
@property NSInteger orientation;
@property(readonly, nonatomic) AVCaptureDevice *captureDevice;
@property(readonly, nonatomic) AVAssetWriter *videoWriter;
@property(readonly, nonatomic) AVAssetWriterInput *videoWriterInput;
@property(readonly, nonatomic) AVAssetWriterInput *audioWriterInput;
Expand All @@ -35,7 +37,7 @@ typedef void(^OnVideoWriterSetup)(void);
@property(assign, nonatomic) CMTime audioTimeOffset;

- (instancetype)init;
- (void)recordVideoAtPath:(NSString *)path orientation:(NSInteger)orientation audioSetupCallback:(OnAudioSetup)audioSetupCallback videoWriterCallback:(OnVideoWriterSetup)videoWriterCallback options:(CupertinoVideoOptions *)options completion:(nonnull void (^)(FlutterError * _Nullable))completion;
- (void)recordVideoAtPath:(NSString *)path captureDevice:(AVCaptureDevice *)device orientation:(NSInteger)orientation audioSetupCallback:(OnAudioSetup)audioSetupCallback videoWriterCallback:(OnVideoWriterSetup)videoWriterCallback options:(CupertinoVideoOptions *)options completion:(nonnull void (^)(FlutterError * _Nullable))completion;
- (void)stopRecordingVideo:(nonnull void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion;
- (void)pauseVideoRecording;
- (void)resumeVideoRecording;
Expand Down
48 changes: 38 additions & 10 deletions ios/Classes/Controllers/Video/VideoController.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ - (instancetype)init {
# pragma mark - User video interactions

/// Start recording video at given path
- (void)recordVideoAtPath:(NSString *)path orientation:(NSInteger)orientation audioSetupCallback:(OnAudioSetup)audioSetupCallback videoWriterCallback:(OnVideoWriterSetup)videoWriterCallback options:(CupertinoVideoOptions *)options completion:(nonnull void (^)(FlutterError * _Nullable))completion {
- (void)recordVideoAtPath:(NSString *)path captureDevice:(AVCaptureDevice *)device orientation:(NSInteger)orientation audioSetupCallback:(OnAudioSetup)audioSetupCallback videoWriterCallback:(OnVideoWriterSetup)videoWriterCallback options:(CupertinoVideoOptions *)options completion:(nonnull void (^)(FlutterError * _Nullable))completion {
_options = options;

// Create audio & video writer
if (![self setupWriterForPath:path audioSetupCallback:audioSetupCallback options:options completion:completion]) {
completion([FlutterError errorWithCode:@"VIDEO_ERROR" message:@"impossible to write video at path" details:path]);
Expand All @@ -38,10 +40,21 @@ - (void)recordVideoAtPath:(NSString *)path orientation:(NSInteger)orientation au
_videoIsDisconnected = NO;
_audioIsDisconnected = NO;
_orientation = orientation;
_captureDevice = device;

// Change video FPS if provided
if (_options && _options.fps != nil && _options.fps > 0) {
[self adjustCameraFPS:_options.fps];
}
}

/// Stop recording video
- (void)stopRecordingVideo:(nonnull void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion {
if (_options && _options.fps != nil && _options.fps > 0) {
// Reset camera FPS
[self adjustCameraFPS:@(30)];
}

if (_isRecording) {
_isRecording = NO;
if (_videoWriter.status != AVAssetWriterStatusUnknown) {
Expand Down Expand Up @@ -85,16 +98,13 @@ - (BOOL)setupWriterForPath:(NSString *)path audioSetupCallback:(OnAudioSetup)aud
AVVideoCodecType codecType = [self getBestCodecTypeAccordingOptions:options];
AVFileType fileType = [self getBestFileTypeAccordingOptions:options];

// NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:codecType, AVVideoCodecKey,[NSNumber numberWithInt:_previewSize.height], AVVideoWidthKey,
// [NSNumber numberWithInt:_previewSize.width], AVVideoHeightKey,
// nil];
NSDictionary *videoSettings = @{AVVideoCodecKey : codecType,
AVVideoWidthKey : @(_previewSize.height),
AVVideoHeightKey : @(_previewSize.width),
AVVideoExpectedSourceFrameRateKey : @(60),
NSDictionary *videoSettings = @{
AVVideoCodecKey : codecType,
AVVideoWidthKey : @(_previewSize.height),
AVVideoHeightKey : @(_previewSize.width),
};
_videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:videoSettings];

_videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
[_videoWriterInput setTransform:[self getVideoOrientation]];

_videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor
Expand Down Expand Up @@ -191,6 +201,24 @@ - (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset CF_R
return sout;
}

/// Adjust video preview & recording to specified FPS
- (void)adjustCameraFPS:(NSNumber *)fps {
NSArray *frameRateRanges = _captureDevice.activeFormat.videoSupportedFrameRateRanges;

if (frameRateRanges.count > 0) {
AVFrameRateRange *frameRateRange = frameRateRanges.firstObject;
NSError *error = nil;

if ([_captureDevice lockForConfiguration:&error]) {
CMTime frameDuration = CMTimeMake(1, [fps intValue]);
if (CMTIME_COMPARE_INLINE(frameDuration, <=, frameRateRange.maxFrameDuration) && CMTIME_COMPARE_INLINE(frameDuration, >=, frameRateRange.minFrameDuration)) {
_captureDevice.activeVideoMinFrameDuration = frameDuration;
}
[_captureDevice unlockForConfiguration];
}
}
}

# pragma mark - Camera Delegates
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection captureVideoOutput:(AVCaptureVideoDataOutput *)captureVideoOutput {

Expand Down
14 changes: 7 additions & 7 deletions ios/Classes/Pigeon/Pigeon.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ typedef NS_ENUM(NSUInteger, AnalysisRotation) {
@end

@interface CupertinoVideoOptions : NSObject
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)makeWithFileType:(NSString *)fileType
codec:(NSString *)codec;
@property(nonatomic, copy) NSString * fileType;
@property(nonatomic, copy) NSString * codec;
+ (instancetype)makeWithFileType:(nullable NSString *)fileType
codec:(nullable NSString *)codec
fps:(nullable NSNumber *)fps;
@property(nonatomic, copy, nullable) NSString * fileType;
@property(nonatomic, copy, nullable) NSString * codec;
@property(nonatomic, strong, nullable) NSNumber * fps;
@end

@interface PigeonSensorTypeDevice : NSObject
Expand Down Expand Up @@ -248,7 +248,7 @@ NSObject<FlutterMessageCodec> *CameraInterfaceGetCodec(void);
/// @return `nil` only when `error != nil`.
- (nullable NSNumber *)getPreviewTextureIdCameraPosition:(NSNumber *)cameraPosition error:(FlutterError *_Nullable *_Nonnull)error;
- (void)takePhotoSensors:(NSArray<PigeonSensor *> *)sensors paths:(NSArray<NSString *> *)paths completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion;
- (void)recordVideoRequests:(NSDictionary<PigeonSensor *, NSString *> *)requests completion:(void (^)(FlutterError *_Nullable))completion;
- (void)recordVideoSensors:(NSArray<PigeonSensor *> *)sensors paths:(NSArray<NSString *> *)paths completion:(void (^)(FlutterError *_Nullable))completion;
- (void)pauseVideoRecordingWithError:(FlutterError *_Nullable *_Nonnull)error;
- (void)resumeVideoRecordingWithError:(FlutterError *_Nullable *_Nonnull)error;
- (void)receivedImageFromStreamWithError:(FlutterError *_Nullable *_Nonnull)error;
Expand Down
17 changes: 10 additions & 7 deletions ios/Classes/Pigeon/Pigeon.m
Original file line number Diff line number Diff line change
Expand Up @@ -220,19 +220,20 @@ - (NSArray *)toList {
@end

@implementation CupertinoVideoOptions
+ (instancetype)makeWithFileType:(NSString *)fileType
codec:(NSString *)codec {
+ (instancetype)makeWithFileType:(nullable NSString *)fileType
codec:(nullable NSString *)codec
fps:(nullable NSNumber *)fps {
CupertinoVideoOptions* pigeonResult = [[CupertinoVideoOptions alloc] init];
pigeonResult.fileType = fileType;
pigeonResult.codec = codec;
pigeonResult.fps = fps;
return pigeonResult;
}
+ (CupertinoVideoOptions *)fromList:(NSArray *)list {
CupertinoVideoOptions *pigeonResult = [[CupertinoVideoOptions alloc] init];
pigeonResult.fileType = GetNullableObjectAtIndex(list, 0);
NSAssert(pigeonResult.fileType != nil, @"");
pigeonResult.codec = GetNullableObjectAtIndex(list, 1);
NSAssert(pigeonResult.codec != nil, @"");
pigeonResult.fps = GetNullableObjectAtIndex(list, 2);
return pigeonResult;
}
+ (nullable CupertinoVideoOptions *)nullableFromList:(NSArray *)list {
Expand All @@ -242,6 +243,7 @@ - (NSArray *)toList {
return @[
(self.fileType ?: [NSNull null]),
(self.codec ?: [NSNull null]),
(self.fps ?: [NSNull null]),
];
}
@end
Expand Down Expand Up @@ -770,11 +772,12 @@ void CameraInterfaceSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<C
binaryMessenger:binaryMessenger
codec:CameraInterfaceGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(recordVideoRequests:completion:)], @"CameraInterface api (%@) doesn't respond to @selector(recordVideoRequests:completion:)", api);
NSCAssert([api respondsToSelector:@selector(recordVideoSensors:paths:completion:)], @"CameraInterface api (%@) doesn't respond to @selector(recordVideoSensors:paths:completion:)", api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSDictionary<PigeonSensor *, NSString *> *arg_requests = GetNullableObjectAtIndex(args, 0);
[api recordVideoRequests:arg_requests completion:^(FlutterError *_Nullable error) {
NSArray<PigeonSensor *> *arg_sensors = GetNullableObjectAtIndex(args, 0);
NSArray<NSString *> *arg_paths = GetNullableObjectAtIndex(args, 1);
[api recordVideoSensors:arg_sensors paths:arg_paths completion:^(FlutterError *_Nullable error) {
callback(wrapResult(nil, error));
}];
}];
Expand Down
21 changes: 13 additions & 8 deletions lib/pigeon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -223,26 +223,31 @@ class AndroidVideoOptions {

class CupertinoVideoOptions {
CupertinoVideoOptions({
required this.fileType,
required this.codec,
this.fileType,
this.codec,
this.fps,
});

String fileType;
String? fileType;

String codec;
String? codec;

int? fps;

Object encode() {
return <Object?>[
fileType,
codec,
fps,
];
}

static CupertinoVideoOptions decode(Object result) {
result as List<Object?>;
return CupertinoVideoOptions(
fileType: result[0]! as String,
codec: result[1]! as String,
fileType: result[0] as String?,
codec: result[1] as String?,
fps: result[2] as int?,
);
}
}
Expand Down Expand Up @@ -813,12 +818,12 @@ class CameraInterface {
}
}

Future<void> recordVideo(Map<PigeonSensor?, String?> arg_requests) async {
Future<void> recordVideo(List<PigeonSensor?> arg_sensors, List<String?> arg_paths) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.CameraInterface.recordVideo', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_requests]) as List<Object?>?;
await channel.send(<Object?>[arg_sensors, arg_paths]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
Expand Down
4 changes: 4 additions & 0 deletions lib/src/orchestrator/models/video_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,19 @@ class CupertinoVideoOptions {
/// **WARNING:** Be sure to use the correct file type extension for the video!
CupertinoFileType fileType;

int? fps;

CupertinoVideoOptions({
this.codec = CupertinoVideoCodec.h264,
this.fileType = CupertinoFileType.quickTimeMovie,
this.fps,
});

Map<String, dynamic> toMap() {
return {
'codec': codec.name,
'fileType': fileType.name,
'fps': fps,
};
}
}
10 changes: 5 additions & 5 deletions lib/src/orchestrator/states/video_camera_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ class VideoCameraState extends CameraState {
/// You can listen to [cameraSetup.mediaCaptureStream] to get updates
/// of the photo capture (capturing, success/failure)
Future<CaptureRequest> startRecording() async {
CaptureRequest filePath =
CaptureRequest captureRequest =
await filePathBuilder(sensorConfig.sensors.whereNotNull().toList());
_mediaCapture = MediaCapture.capturing(
captureRequest: filePath, videoState: VideoState.started);
captureRequest: captureRequest, videoState: VideoState.started);
try {
await CamerawesomePlugin.recordVideo(filePath);
await CamerawesomePlugin.recordVideo(captureRequest);
} on Exception catch (e) {
_mediaCapture =
MediaCapture.failure(captureRequest: filePath, exception: e);
MediaCapture.failure(captureRequest: captureRequest, exception: e);
}
cameraContext.changeState(VideoRecordingCameraState.from(cameraContext));
return filePath;
return captureRequest;
}

/// If the video recording should [enableAudio].
Expand Down
Loading