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

Ios video logic #86

Merged
merged 15 commits into from
Oct 3, 2024
3 changes: 3 additions & 0 deletions src/ios/CameraSessionManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- (BOOL) deviceHasUltraWideCamera;
- (void) deallocSession;
- (void) updateOrientation:(AVCaptureVideoOrientation)orientation;
- (void) startRecordingToOutputFileURL:(NSURL *)fileURL recordingDelegate:(id<AVCaptureFileOutputRecordingDelegate>)recordingDelegate;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- (void) startRecordingToOutputFileURL:(NSURL *)fileURL recordingDelegate:(id<AVCaptureFileOutputRecordingDelegate>)recordingDelegate;
- (void) startRecording:(NSURL *)fileURL recordingDelegate:(id<AVCaptureFileOutputRecordingDelegate>)recordingDelegate;

Can this method be renamed to startRecording ?

- (void) stopRecording;
- (AVCaptureVideoOrientation) getCurrentOrientation:(UIInterfaceOrientation)toInterfaceOrientation;
+ (AVCaptureSessionPreset) calculateResolution:(NSInteger)targetSize;
- (UIInterfaceOrientation) getOrientation;
Expand All @@ -27,4 +29,5 @@
@property (nonatomic) AVCapturePhotoOutput *imageOutput;
@property (nonatomic) AVCaptureVideoDataOutput *dataOutput;
@property (nonatomic, weak) id delegate;
@property (nonatomic) AVCaptureMovieFileOutput *movieFileOutput;
@end
30 changes: 30 additions & 0 deletions src/ios/CameraSessionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ - (CameraSessionManager *)init {
[self.session setSessionPreset:AVCaptureSessionPresetPhoto];
}
self.filterLock = [[NSLock alloc] init];
self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this line is not done in the setupSession function instead?

Im taking the example of imageOutput. The allocation and checks are all done in setupSession on line 98:

AVCapturePhotoOutput *imageOutput = [[AVCapturePhotoOutput alloc] init];
if ([self.session canAddOutput:imageOutput]) {
    [self.session addOutput:imageOutput];
    self.imageOutput = imageOutput;
}

}
return self;
}
Expand Down Expand Up @@ -101,6 +102,19 @@ - (void) setupSession:(NSString *)defaultCamera completion:(void(^)(BOOL started
}

AVCaptureVideoDataOutput *dataOutput = [[AVCaptureVideoDataOutput alloc] init];

AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
NSError *audioError = nil;
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&audioError];
Copy link
Member

@HashirRajah HashirRajah Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should audioInput be declared in the header file like videoDeviceInput?

if (audioInput && [self.session canAddInput:audioInput]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (audioInput && [self.session canAddInput:audioInput]) {
if ([self.session canAddInput:audioInput]) {

Will this be enough? When looking at similar code blocks, only this check is done. For example on line 93:

if ([self.session canAddInput:videoDeviceInput]) {
    [self.session addInput:videoDeviceInput];
    self.videoDeviceInput = videoDeviceInput;
}

[self.session addInput:audioInput];
} else {
NSLog(@"Error adding audio input: %@", audioError.localizedDescription);
}

if ([self.session canAddOutput:self.movieFileOutput]) {
[self.session addOutput:self.movieFileOutput];
}
if ([self.session canAddOutput:dataOutput]) {
self.dataOutput = dataOutput;
[dataOutput setAlwaysDiscardsLateVideoFrames:YES];
Expand Down Expand Up @@ -225,6 +239,22 @@ - (void)switchCameraTo:(NSString*)cameraMode completion:(void (^)(BOOL success))
});
}

- (void)startRecordingToOutputFileURL:(NSURL *)fileURL recordingDelegate:(id<AVCaptureFileOutputRecordingDelegate>)recordingDelegate {
if (!self.movieFileOutput.isRecording) {
AVCaptureConnection *connection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
if ([connection isVideoOrientationSupported]) {
connection.videoOrientation = [self getCurrentOrientation];
}
[self.movieFileOutput startRecordingToOutputFileURL:fileURL recordingDelegate:recordingDelegate];
Comment on lines +243 to +248
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do an early return?

if (self.movieFileOutput.isRecording) return;

// logic to start recording

}
}

- (void)stopRecording {
if (self.movieFileOutput.isRecording) {
[self.movieFileOutput stopRecording];
}
}

- (BOOL)deviceHasUltraWideCamera {
if (@available(iOS 13.0, *)) {
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInUltraWideCamera] mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionUnspecified];
Expand Down
3 changes: 2 additions & 1 deletion src/ios/SimpleCameraPreview.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#import "CameraSessionManager.h"
#import "CameraRenderController.h"
#import <CoreLocation/CoreLocation.h>
@interface SimpleCameraPreview : CDVPlugin <AVCapturePhotoCaptureDelegate, CLLocationManagerDelegate>{
@interface SimpleCameraPreview : CDVPlugin <AVCapturePhotoCaptureDelegate, CLLocationManagerDelegate, AVCaptureFileOutputRecordingDelegate>{
CLLocationManager *locationManager;
CLLocation* currentLocation;
}
Expand All @@ -19,6 +19,7 @@
- (void) switchCameraTo: (CDVInvokedUrlCommand*) command;
- (void) deviceHasUltraWideCamera: (CDVInvokedUrlCommand*) command;
- (void) deviceHasFlash: (CDVInvokedUrlCommand*)command;
@property (nonatomic) CDVInvokedUrlCommand *videoCallbackContext;
@property (nonatomic) CameraSessionManager *sessionManager;
@property (nonatomic) CameraRenderController *cameraRenderController;
@property (nonatomic) NSString *onPictureTakenHandlerId;
Expand Down
100 changes: 100 additions & 0 deletions src/ios/SimpleCameraPreview.m
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,104 @@ - (void)deallocateMemory {
locationManager = nil;
}

- (void) initVideoCallback:(CDVInvokedUrlCommand*)command {
self.videoCallbackContext = command;
NSDictionary *data = @{ @"videoCallbackInitialized" : @true };

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
[pluginResult setKeepCallbackAsBool:true];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

- (void)startVideoCapture:(CDVInvokedUrlCommand*)command {
if (self.sessionManager != nil && !self.sessionManager.movieFileOutput.isRecording) {
Copy link
Member

@HashirRajah HashirRajah Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can invert the condition and make an early return instead of having an if and an else block

if (self.sessionManager == nil || self.sessionManager.movieFileOutput.isRecording) {
  CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Session not initialized or already recording"];
  [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
  return;
}
// other logic

Copy link
Member

@HashirRajah HashirRajah Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition should be an || condition when reversing.

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"NoCloud"];
NSString* uniqueFileName = [NSString stringWithFormat:@"%@.mp4",[[NSUUID UUID] UUIDString]];
NSString *dataPath = [libraryDirectory stringByAppendingPathComponent:uniqueFileName];
NSURL *fileURL = [NSURL fileURLWithPath:dataPath];
[self.sessionManager startRecordingToOutputFileURL:fileURL recordingDelegate:self];
} else {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Session not initialized or already recording"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setKeepCallback true

same in the other methods

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we keep it same logic as with Androids and the single callback

}
}

- (void)stopVideoCapture:(CDVInvokedUrlCommand*)command {
if (self.sessionManager != nil && self.sessionManager.movieFileOutput.isRecording) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments for the if part as above.

[self.sessionManager stopRecording];
} else {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Session not initialized or not recording"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}

- (NSString*)generateThumbnailForVideoAtURL:(NSURL *)videoURL {
AVAsset *asset = [AVAsset assetWithURL:videoURL];
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
imageGenerator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMakeWithSeconds(1.0, 600);
NSError *error = nil;
CMTime actualTime;
CGImageRef imageRef = [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];

if (error) {
NSLog(@"Error generating thumbnail: %@", error.localizedDescription);
YushraJewon marked this conversation as resolved.
Show resolved Hide resolved
return nil;
}

UIImage *thumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"NoCloud"];

NSError *directoryError = nil;
if (![[NSFileManager defaultManager] fileExistsAtPath:libraryDirectory]) {
[[NSFileManager defaultManager] createDirectoryAtPath:libraryDirectory withIntermediateDirectories:YES attributes:nil error:&directoryError];

if (directoryError) {
NSLog(@"Error creating NoCloud directory: %@", directoryError.localizedDescription);
YushraJewon marked this conversation as resolved.
Show resolved Hide resolved
return nil;
}
}

NSString *uniqueFileName = [NSString stringWithFormat:@"video_thumb_%@.jpg", [[NSUUID UUID] UUIDString]];
NSString *filePath = [libraryDirectory stringByAppendingPathComponent:uniqueFileName];

NSData *jpegData = UIImageJPEGRepresentation(thumbnail, 1.0);

if ([jpegData writeToFile:filePath atomically:YES]) {
NSLog(@"Thumbnail saved successfully at path: %@", filePath);
} else {
NSLog(@"Failed to save thumbnail.");
YushraJewon marked this conversation as resolved.
Show resolved Hide resolved
return nil;
}
return filePath;
}


- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections {
NSDictionary *result = @{@"recording": @TRUE};

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
[pluginResult setKeepCallbackAsBool:true];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.videoCallbackContext.callbackId];
}

- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error {
if (error) {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error.localizedDescription];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.videoCallbackContext.callbackId];
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add a return in the if instead of an else block.

NSString *thumbnail = [self generateThumbnailForVideoAtURL:outputFileURL];
NSString *filePath = [outputFileURL path];
NSDictionary *result = @{@"nativePath": filePath, @"thumbnail": thumbnail};
YushraJewon marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if we actually have the thumbnail generated before adding it to the dictionary?

we can return null if we get errors from the generateThumbnailForVideoAtURL method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can remove recording in the onStop for Android too.


YushraJewon marked this conversation as resolved.
Show resolved Hide resolved
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
[pluginResult setKeepCallbackAsBool:true];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.videoCallbackContext.callbackId];
}
}

@end
Loading