| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| #import "CDVCapture.h" |
| #import "CDVFile.h" |
| #import <Cordova/CDVAvailability.h> |
| |
| #define kW3CMediaFormatHeight @"height" |
| #define kW3CMediaFormatWidth @"width" |
| #define kW3CMediaFormatCodecs @"codecs" |
| #define kW3CMediaFormatBitrate @"bitrate" |
| #define kW3CMediaFormatDuration @"duration" |
| #define kW3CMediaModeType @"type" |
| |
| @implementation NSBundle (PluginExtensions) |
| |
| + (NSBundle*) pluginBundle:(CDVPlugin*)plugin { |
| NSBundle* bundle = [NSBundle bundleWithPath: [[NSBundle mainBundle] pathForResource:NSStringFromClass([plugin class]) ofType: @"bundle"]]; |
| return bundle; |
| } |
| @end |
| |
| #define PluginLocalizedString(plugin, key, comment) [[NSBundle pluginBundle:(plugin)] localizedStringForKey:(key) value:nil table:nil] |
| |
| @implementation CDVImagePicker |
| |
| @synthesize quality; |
| @synthesize callbackId; |
| @synthesize mimeType; |
| |
| - (uint64_t)accessibilityTraits |
| { |
| NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; |
| |
| if (([systemVersion compare:@"4.0" options:NSNumericSearch] != NSOrderedAscending)) { // this means system version is not less than 4.0 |
| return UIAccessibilityTraitStartsMediaSession; |
| } |
| |
| return UIAccessibilityTraitNone; |
| } |
| |
| - (BOOL)prefersStatusBarHidden { |
| return YES; |
| } |
| |
| - (UIViewController*)childViewControllerForStatusBarHidden { |
| return nil; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| SEL sel = NSSelectorFromString(@"setNeedsStatusBarAppearanceUpdate"); |
| if ([self respondsToSelector:sel]) { |
| [self performSelector:sel withObject:nil afterDelay:0]; |
| } |
| |
| [super viewWillAppear:animated]; |
| } |
| |
| @end |
| |
| @implementation CDVCapture |
| @synthesize inUse; |
| |
| - (void)pluginInitialize |
| { |
| self.inUse = NO; |
| } |
| |
| - (void)captureAudio:(CDVInvokedUrlCommand*)command |
| { |
| NSString* callbackId = command.callbackId; |
| NSDictionary* options = [command argumentAtIndex:0]; |
| |
| if ([options isKindOfClass:[NSNull class]]) { |
| options = [NSDictionary dictionary]; |
| } |
| |
| NSNumber* duration = [options objectForKey:@"duration"]; |
| // the default value of duration is 0 so use nil (no duration) if default value |
| if (duration) { |
| duration = [duration doubleValue] == 0 ? nil : duration; |
| } |
| CDVPluginResult* result = nil; |
| |
| if (NSClassFromString(@"AVAudioRecorder") == nil) { |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; |
| } else if (self.inUse == YES) { |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_APPLICATION_BUSY]; |
| } else { |
| // all the work occurs here |
| CDVAudioRecorderViewController* audioViewController = [[CDVAudioRecorderViewController alloc] initWithCommand:self duration:duration callbackId:callbackId]; |
| |
| // Now create a nav controller and display the view... |
| CDVAudioNavigationController* navController = [[CDVAudioNavigationController alloc] initWithRootViewController:audioViewController]; |
| |
| self.inUse = YES; |
| |
| [self.viewController presentViewController:navController animated:YES completion:nil]; |
| } |
| |
| if (result) { |
| [self.commandDelegate sendPluginResult:result callbackId:callbackId]; |
| } |
| } |
| |
| - (void)captureImage:(CDVInvokedUrlCommand*)command |
| { |
| NSString* callbackId = command.callbackId; |
| NSDictionary* options = [command argumentAtIndex:0]; |
| |
| if ([options isKindOfClass:[NSNull class]]) { |
| options = [NSDictionary dictionary]; |
| } |
| |
| // options could contain limit and mode neither of which are supported at this time |
| // taking more than one picture (limit) is only supported if provide own controls via cameraOverlayView property |
| // can support mode in OS |
| |
| if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { |
| NSLog(@"Capture.imageCapture: camera not available."); |
| CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; |
| [self.commandDelegate sendPluginResult:result callbackId:callbackId]; |
| } else { |
| if (pickerController == nil) { |
| pickerController = [[CDVImagePicker alloc] init]; |
| } |
| |
| pickerController.delegate = self; |
| pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; |
| pickerController.allowsEditing = NO; |
| if ([pickerController respondsToSelector:@selector(mediaTypes)]) { |
| // iOS 3.0 |
| pickerController.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil]; |
| } |
| |
| /*if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]){ |
| // iOS 4.0 |
| pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto; |
| pickerController.cameraDevice = UIImagePickerControllerCameraDeviceRear; |
| pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto; |
| }*/ |
| // CDVImagePicker specific property |
| pickerController.callbackId = callbackId; |
| |
| [self.viewController presentViewController:pickerController animated:YES completion:nil]; |
| } |
| } |
| |
| /* Process a still image from the camera. |
| * IN: |
| * UIImage* image - the UIImage data returned from the camera |
| * NSString* callbackId |
| */ |
| - (CDVPluginResult*)processImage:(UIImage*)image type:(NSString*)mimeType forCallbackId:(NSString*)callbackId |
| { |
| CDVPluginResult* result = nil; |
| |
| // save the image to photo album |
| UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); |
| |
| NSData* data = nil; |
| if (mimeType && [mimeType isEqualToString:@"image/png"]) { |
| data = UIImagePNGRepresentation(image); |
| } else { |
| data = UIImageJPEGRepresentation(image, 0.5); |
| } |
| |
| // write to temp directory and return URI |
| NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; // use file system temporary directory |
| NSError* err = nil; |
| NSFileManager* fileMgr = [[NSFileManager alloc] init]; |
| |
| // generate unique file name |
| NSString* filePath; |
| int i = 1; |
| do { |
| filePath = [NSString stringWithFormat:@"%@/photo_%03d.jpg", docsPath, i++]; |
| } while ([fileMgr fileExistsAtPath:filePath]); |
| |
| if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) { |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR]; |
| if (err) { |
| NSLog(@"Error saving image: %@", [err localizedDescription]); |
| } |
| } else { |
| // create MediaFile object |
| |
| NSDictionary* fileDict = [self getMediaDictionaryFromPath:filePath ofType:mimeType]; |
| NSArray* fileArray = [NSArray arrayWithObject:fileDict]; |
| |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; |
| } |
| |
| return result; |
| } |
| |
| - (void)captureVideo:(CDVInvokedUrlCommand*)command |
| { |
| NSString* callbackId = command.callbackId; |
| NSDictionary* options = [command argumentAtIndex:0]; |
| |
| if ([options isKindOfClass:[NSNull class]]) { |
| options = [NSDictionary dictionary]; |
| } |
| |
| // options could contain limit, duration and mode |
| // taking more than one video (limit) is only supported if provide own controls via cameraOverlayView property |
| NSNumber* duration = [options objectForKey:@"duration"]; |
| NSString* mediaType = nil; |
| |
| if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { |
| // there is a camera, it is available, make sure it can do movies |
| pickerController = [[CDVImagePicker alloc] init]; |
| |
| NSArray* types = nil; |
| if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) { |
| types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; |
| // NSLog(@"MediaTypes: %@", [types description]); |
| |
| if ([types containsObject:(NSString*)kUTTypeMovie]) { |
| mediaType = (NSString*)kUTTypeMovie; |
| } else if ([types containsObject:(NSString*)kUTTypeVideo]) { |
| mediaType = (NSString*)kUTTypeVideo; |
| } |
| } |
| } |
| if (!mediaType) { |
| // don't have video camera return error |
| NSLog(@"Capture.captureVideo: video mode not available."); |
| CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; |
| [self.commandDelegate sendPluginResult:result callbackId:callbackId]; |
| pickerController = nil; |
| } else { |
| pickerController.delegate = self; |
| pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; |
| pickerController.allowsEditing = NO; |
| // iOS 3.0 |
| pickerController.mediaTypes = [NSArray arrayWithObjects:mediaType, nil]; |
| |
| if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]){ |
| if (duration) { |
| pickerController.videoMaximumDuration = [duration doubleValue]; |
| } |
| //NSLog(@"pickerController.videoMaximumDuration = %f", pickerController.videoMaximumDuration); |
| } |
| |
| // iOS 4.0 |
| if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]) { |
| pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; |
| // pickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; |
| // pickerController.cameraDevice = UIImagePickerControllerCameraDeviceRear; |
| // pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto; |
| } |
| // CDVImagePicker specific property |
| pickerController.callbackId = callbackId; |
| |
| [self.viewController presentViewController:pickerController animated:YES completion:nil]; |
| } |
| } |
| |
| - (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId |
| { |
| // save the movie to photo album (only avail as of iOS 3.1) |
| |
| /* don't need, it should automatically get saved |
| NSLog(@"can save %@: %d ?", moviePath, UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(moviePath)); |
| if (&UIVideoAtPathIsCompatibleWithSavedPhotosAlbum != NULL && UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(moviePath) == YES) { |
| NSLog(@"try to save movie"); |
| UISaveVideoAtPathToSavedPhotosAlbum(moviePath, nil, nil, nil); |
| NSLog(@"finished saving movie"); |
| }*/ |
| // create MediaFile object |
| NSDictionary* fileDict = [self getMediaDictionaryFromPath:moviePath ofType:nil]; |
| NSArray* fileArray = [NSArray arrayWithObject:fileDict]; |
| |
| return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; |
| } |
| |
| - (void)getMediaModes:(CDVInvokedUrlCommand*)command |
| { |
| // NSString* callbackId = [command argumentAtIndex:0]; |
| // NSMutableDictionary* imageModes = nil; |
| NSArray* imageArray = nil; |
| NSArray* movieArray = nil; |
| NSArray* audioArray = nil; |
| |
| if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { |
| // there is a camera, find the modes |
| // can get image/jpeg or image/png from camera |
| |
| /* can't find a way to get the default height and width and other info |
| * for images/movies taken with UIImagePickerController |
| */ |
| NSDictionary* jpg = [NSDictionary dictionaryWithObjectsAndKeys: |
| [NSNumber numberWithInt:0], kW3CMediaFormatHeight, |
| [NSNumber numberWithInt:0], kW3CMediaFormatWidth, |
| @"image/jpeg", kW3CMediaModeType, |
| nil]; |
| NSDictionary* png = [NSDictionary dictionaryWithObjectsAndKeys: |
| [NSNumber numberWithInt:0], kW3CMediaFormatHeight, |
| [NSNumber numberWithInt:0], kW3CMediaFormatWidth, |
| @"image/png", kW3CMediaModeType, |
| nil]; |
| imageArray = [NSArray arrayWithObjects:jpg, png, nil]; |
| |
| if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) { |
| NSArray* types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; |
| |
| if ([types containsObject:(NSString*)kUTTypeMovie]) { |
| NSDictionary* mov = [NSDictionary dictionaryWithObjectsAndKeys: |
| [NSNumber numberWithInt:0], kW3CMediaFormatHeight, |
| [NSNumber numberWithInt:0], kW3CMediaFormatWidth, |
| @"video/quicktime", kW3CMediaModeType, |
| nil]; |
| movieArray = [NSArray arrayWithObject:mov]; |
| } |
| } |
| } |
| NSDictionary* modes = [NSDictionary dictionaryWithObjectsAndKeys: |
| imageArray ? (NSObject*) imageArray:[NSNull null], @"image", |
| movieArray ? (NSObject*) movieArray:[NSNull null], @"video", |
| audioArray ? (NSObject*) audioArray:[NSNull null], @"audio", |
| nil]; |
| |
| NSData* jsonData = [NSJSONSerialization dataWithJSONObject:modes options:0 error:nil]; |
| NSString* jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; |
| |
| NSString* jsString = [NSString stringWithFormat:@"navigator.device.capture.setSupportedModes(%@);", jsonStr]; |
| [self.commandDelegate evalJs:jsString]; |
| } |
| |
| - (void)getFormatData:(CDVInvokedUrlCommand*)command |
| { |
| NSString* callbackId = command.callbackId; |
| // existence of fullPath checked on JS side |
| NSString* fullPath = [command argumentAtIndex:0]; |
| // mimeType could be null |
| NSString* mimeType = nil; |
| |
| if ([command.arguments count] > 1) { |
| mimeType = [command argumentAtIndex:1]; |
| } |
| BOOL bError = NO; |
| CDVCaptureError errorCode = CAPTURE_INTERNAL_ERR; |
| CDVPluginResult* result = nil; |
| |
| if (!mimeType || [mimeType isKindOfClass:[NSNull class]]) { |
| // try to determine mime type if not provided |
| id command = [self.commandDelegate getCommandInstance:@"File"]; |
| bError = !([command isKindOfClass:[CDVFile class]]); |
| if (!bError) { |
| CDVFile* cdvFile = (CDVFile*)command; |
| mimeType = [cdvFile getMimeTypeFromPath:fullPath]; |
| if (!mimeType) { |
| // can't do much without mimeType, return error |
| bError = YES; |
| errorCode = CAPTURE_INVALID_ARGUMENT; |
| } |
| } |
| } |
| if (!bError) { |
| // create and initialize return dictionary |
| NSMutableDictionary* formatData = [NSMutableDictionary dictionaryWithCapacity:5]; |
| [formatData setObject:[NSNull null] forKey:kW3CMediaFormatCodecs]; |
| [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatBitrate]; |
| [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatHeight]; |
| [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatWidth]; |
| [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatDuration]; |
| |
| if ([mimeType rangeOfString:@"image/"].location != NSNotFound) { |
| UIImage* image = [UIImage imageWithContentsOfFile:fullPath]; |
| if (image) { |
| CGSize imgSize = [image size]; |
| [formatData setObject:[NSNumber numberWithInteger:imgSize.width] forKey:kW3CMediaFormatWidth]; |
| [formatData setObject:[NSNumber numberWithInteger:imgSize.height] forKey:kW3CMediaFormatHeight]; |
| } |
| } else if (([mimeType rangeOfString:@"video/"].location != NSNotFound) && (NSClassFromString(@"AVURLAsset") != nil)) { |
| NSURL* movieURL = [NSURL fileURLWithPath:fullPath]; |
| AVURLAsset* movieAsset = [[AVURLAsset alloc] initWithURL:movieURL options:nil]; |
| CMTime duration = [movieAsset duration]; |
| [formatData setObject:[NSNumber numberWithFloat:CMTimeGetSeconds(duration)] forKey:kW3CMediaFormatDuration]; |
| |
| NSArray* allVideoTracks = [movieAsset tracksWithMediaType:AVMediaTypeVideo]; |
| if ([allVideoTracks count] > 0) { |
| AVAssetTrack* track = [[movieAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; |
| CGSize size = [track naturalSize]; |
| |
| [formatData setObject:[NSNumber numberWithFloat:size.height] forKey:kW3CMediaFormatHeight]; |
| [formatData setObject:[NSNumber numberWithFloat:size.width] forKey:kW3CMediaFormatWidth]; |
| // not sure how to get codecs or bitrate??? |
| // AVMetadataItem |
| // AudioFile |
| } else { |
| NSLog(@"No video tracks found for %@", fullPath); |
| } |
| } else if ([mimeType rangeOfString:@"audio/"].location != NSNotFound) { |
| if (NSClassFromString(@"AVAudioPlayer") != nil) { |
| NSURL* fileURL = [NSURL fileURLWithPath:fullPath]; |
| NSError* err = nil; |
| |
| AVAudioPlayer* avPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&err]; |
| if (!err) { |
| // get the data |
| [formatData setObject:[NSNumber numberWithDouble:[avPlayer duration]] forKey:kW3CMediaFormatDuration]; |
| if ([avPlayer respondsToSelector:@selector(settings)]) { |
| NSDictionary* info = [avPlayer settings]; |
| NSNumber* bitRate = [info objectForKey:AVEncoderBitRateKey]; |
| if (bitRate) { |
| [formatData setObject:bitRate forKey:kW3CMediaFormatBitrate]; |
| } |
| } |
| } // else leave data init'ed to 0 |
| } |
| } |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:formatData]; |
| // NSLog(@"getFormatData: %@", [formatData description]); |
| } |
| if (bError) { |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:(int)errorCode]; |
| } |
| if (result) { |
| [self.commandDelegate sendPluginResult:result callbackId:callbackId]; |
| } |
| } |
| |
| - (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type |
| { |
| NSFileManager* fileMgr = [[NSFileManager alloc] init]; |
| NSMutableDictionary* fileDict = [NSMutableDictionary dictionaryWithCapacity:5]; |
| |
| CDVFile *fs = [self.commandDelegate getCommandInstance:@"File"]; |
| |
| // Get canonical version of localPath |
| NSURL *fileURL = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@", fullPath]]; |
| NSURL *resolvedFileURL = [fileURL URLByResolvingSymlinksInPath]; |
| NSString *path = [resolvedFileURL path]; |
| |
| CDVFilesystemURL *url = [fs fileSystemURLforLocalPath:path]; |
| |
| [fileDict setObject:[fullPath lastPathComponent] forKey:@"name"]; |
| [fileDict setObject:fullPath forKey:@"fullPath"]; |
| if (url) { |
| [fileDict setObject:[url absoluteURL] forKey:@"localURL"]; |
| } |
| // determine type |
| if (!type) { |
| id command = [self.commandDelegate getCommandInstance:@"File"]; |
| if ([command isKindOfClass:[CDVFile class]]) { |
| CDVFile* cdvFile = (CDVFile*)command; |
| NSString* mimeType = [cdvFile getMimeTypeFromPath:fullPath]; |
| [fileDict setObject:(mimeType != nil ? (NSObject*)mimeType : [NSNull null]) forKey:@"type"]; |
| } |
| } |
| NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:fullPath error:nil]; |
| [fileDict setObject:[NSNumber numberWithUnsignedLongLong:[fileAttrs fileSize]] forKey:@"size"]; |
| NSDate* modDate = [fileAttrs fileModificationDate]; |
| NSNumber* msDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000]; |
| [fileDict setObject:msDate forKey:@"lastModifiedDate"]; |
| |
| return fileDict; |
| } |
| |
| - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo |
| { |
| // older api calls new one |
| [self imagePickerController:picker didFinishPickingMediaWithInfo:editingInfo]; |
| } |
| |
| /* Called when image/movie is finished recording. |
| * Calls success or error code as appropriate |
| * if successful, result contains an array (with just one entry since can only get one image unless build own camera UI) of MediaFile object representing the image |
| * name |
| * fullPath |
| * type |
| * lastModifiedDate |
| * size |
| */ |
| - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info |
| { |
| CDVImagePicker* cameraPicker = (CDVImagePicker*)picker; |
| NSString* callbackId = cameraPicker.callbackId; |
| |
| [[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil]; |
| |
| CDVPluginResult* result = nil; |
| |
| UIImage* image = nil; |
| NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType]; |
| if (!mediaType || [mediaType isEqualToString:(NSString*)kUTTypeImage]) { |
| // mediaType is nil then only option is UIImagePickerControllerOriginalImage |
| if ([UIImagePickerController respondsToSelector:@selector(allowsEditing)] && |
| (cameraPicker.allowsEditing && [info objectForKey:UIImagePickerControllerEditedImage])) { |
| image = [info objectForKey:UIImagePickerControllerEditedImage]; |
| } else { |
| image = [info objectForKey:UIImagePickerControllerOriginalImage]; |
| } |
| } |
| if (image != nil) { |
| // mediaType was image |
| result = [self processImage:image type:cameraPicker.mimeType forCallbackId:callbackId]; |
| } else if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]) { |
| // process video |
| NSString* moviePath = [(NSURL *)[info objectForKey:UIImagePickerControllerMediaURL] path]; |
| if (moviePath) { |
| result = [self processVideo:moviePath forCallbackId:callbackId]; |
| } |
| } |
| if (!result) { |
| result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_INTERNAL_ERR]; |
| } |
| [self.commandDelegate sendPluginResult:result callbackId:callbackId]; |
| pickerController = nil; |
| } |
| |
| - (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker |
| { |
| CDVImagePicker* cameraPicker = (CDVImagePicker*)picker; |
| NSString* callbackId = cameraPicker.callbackId; |
| |
| [[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil]; |
| |
| CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NO_MEDIA_FILES]; |
| [self.commandDelegate sendPluginResult:result callbackId:callbackId]; |
| pickerController = nil; |
| } |
| |
| @end |
| |
| @implementation CDVAudioNavigationController |
| |
| #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 |
| - (UIInterfaceOrientationMask)supportedInterfaceOrientations |
| { |
| // delegate to CVDAudioRecorderViewController |
| return [self.topViewController supportedInterfaceOrientations]; |
| } |
| #else |
| - (NSUInteger)supportedInterfaceOrientations |
| { |
| // delegate to CVDAudioRecorderViewController |
| return [self.topViewController supportedInterfaceOrientations]; |
| } |
| #endif |
| |
| @end |
| |
| @interface CDVAudioRecorderViewController () { |
| UIStatusBarStyle _previousStatusBarStyle; |
| } |
| @end |
| |
| @implementation CDVAudioRecorderViewController |
| @synthesize errorCode, callbackId, duration, captureCommand, doneButton, recordingView, recordButton, recordImage, stopRecordImage, timerLabel, avRecorder, avSession, pluginResult, timer, isTimed; |
| |
| - (NSString*)resolveImageResource:(NSString*)resource |
| { |
| NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; |
| BOOL isLessThaniOS4 = ([systemVersion compare:@"4.0" options:NSNumericSearch] == NSOrderedAscending); |
| |
| // the iPad image (nor retina) differentiation code was not in 3.x, and we have to explicitly set the path |
| // if user wants iPhone only app to run on iPad they must remove *~ipad.* images from CDVCapture.bundle |
| if (isLessThaniOS4) { |
| NSString* iPadResource = [NSString stringWithFormat:@"%@~ipad.png", resource]; |
| if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && [UIImage imageNamed:iPadResource]) { |
| return iPadResource; |
| } else { |
| return [NSString stringWithFormat:@"%@.png", resource]; |
| } |
| } |
| |
| return resource; |
| } |
| |
| - (id)initWithCommand:(CDVCapture*)theCommand duration:(NSNumber*)theDuration callbackId:(NSString*)theCallbackId |
| { |
| if ((self = [super init])) { |
| self.captureCommand = theCommand; |
| self.duration = theDuration; |
| self.callbackId = theCallbackId; |
| self.errorCode = CAPTURE_NO_MEDIA_FILES; |
| self.isTimed = self.duration != nil; |
| _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; |
| |
| return self; |
| } |
| |
| return nil; |
| } |
| |
| - (void)loadView |
| { |
| if ([self respondsToSelector:@selector(edgesForExtendedLayout)]) { |
| self.edgesForExtendedLayout = UIRectEdgeNone; |
| } |
| |
| // create view and display |
| CGRect viewRect = [[UIScreen mainScreen] applicationFrame]; |
| UIView* tmp = [[UIView alloc] initWithFrame:viewRect]; |
| |
| // make backgrounds |
| NSString* microphoneResource = @"CDVCapture.bundle/microphone"; |
| |
| BOOL isIphone5 = ([[UIScreen mainScreen] bounds].size.width == 568 && [[UIScreen mainScreen] bounds].size.height == 320) || ([[UIScreen mainScreen] bounds].size.height == 568 && [[UIScreen mainScreen] bounds].size.width == 320); |
| if (isIphone5) { |
| microphoneResource = @"CDVCapture.bundle/microphone-568h"; |
| } |
| |
| NSBundle* cdvBundle = [NSBundle bundleForClass:[CDVCapture class]]; |
| UIImage* microphone = [UIImage imageNamed:[self resolveImageResource:microphoneResource] inBundle:cdvBundle compatibleWithTraitCollection:nil]; |
| UIView* microphoneView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, microphone.size.height)]; |
| [microphoneView setBackgroundColor:[UIColor colorWithPatternImage:microphone]]; |
| [microphoneView setUserInteractionEnabled:NO]; |
| [microphoneView setIsAccessibilityElement:NO]; |
| [tmp addSubview:microphoneView]; |
| |
| // add bottom bar view |
| UIImage* grayBkg = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/controls_bg"] inBundle:cdvBundle compatibleWithTraitCollection:nil]; |
| UIView* controls = [[UIView alloc] initWithFrame:CGRectMake(0, microphone.size.height, viewRect.size.width, grayBkg.size.height)]; |
| [controls setBackgroundColor:[UIColor colorWithPatternImage:grayBkg]]; |
| [controls setUserInteractionEnabled:NO]; |
| [controls setIsAccessibilityElement:NO]; |
| [tmp addSubview:controls]; |
| |
| // make red recording background view |
| UIImage* recordingBkg = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/recording_bg"] inBundle:cdvBundle compatibleWithTraitCollection:nil]; |
| UIColor* background = [UIColor colorWithPatternImage:recordingBkg]; |
| self.recordingView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, recordingBkg.size.height)]; |
| [self.recordingView setBackgroundColor:background]; |
| [self.recordingView setHidden:YES]; |
| [self.recordingView setUserInteractionEnabled:NO]; |
| [self.recordingView setIsAccessibilityElement:NO]; |
| [tmp addSubview:self.recordingView]; |
| |
| // add label |
| self.timerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, recordingBkg.size.height)]; |
| // timerLabel.autoresizingMask = reSizeMask; |
| [self.timerLabel setBackgroundColor:[UIColor clearColor]]; |
| [self.timerLabel setTextColor:[UIColor whiteColor]]; |
| #ifdef __IPHONE_6_0 |
| [self.timerLabel setTextAlignment:NSTextAlignmentCenter]; |
| #else |
| // for iOS SDK < 6.0 |
| [self.timerLabel setTextAlignment:UITextAlignmentCenter]; |
| #endif |
| [self.timerLabel setText:@"0:00"]; |
| [self.timerLabel setAccessibilityHint:PluginLocalizedString(captureCommand, @"recorded time in minutes and seconds", nil)]; |
| self.timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently; |
| self.timerLabel.accessibilityTraits &= ~UIAccessibilityTraitStaticText; |
| [tmp addSubview:self.timerLabel]; |
| |
| // Add record button |
| |
| self.recordImage = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/record_button"] inBundle:cdvBundle compatibleWithTraitCollection:nil]; |
| self.stopRecordImage = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/stop_button"] inBundle:cdvBundle compatibleWithTraitCollection:nil]; |
| self.recordButton.accessibilityTraits |= [self accessibilityTraits]; |
| self.recordButton = [[UIButton alloc] initWithFrame:CGRectMake((viewRect.size.width - recordImage.size.width) / 2, (microphone.size.height + (grayBkg.size.height - recordImage.size.height) / 2), recordImage.size.width, recordImage.size.height)]; |
| [self.recordButton setAccessibilityLabel:PluginLocalizedString(captureCommand, @"toggle audio recording", nil)]; |
| [self.recordButton setImage:recordImage forState:UIControlStateNormal]; |
| [self.recordButton addTarget:self action:@selector(processButton:) forControlEvents:UIControlEventTouchUpInside]; |
| [tmp addSubview:recordButton]; |
| |
| // make and add done button to navigation bar |
| self.doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissAudioView:)]; |
| [self.doneButton setStyle:UIBarButtonItemStyleDone]; |
| self.navigationItem.rightBarButtonItem = self.doneButton; |
| |
| [self setView:tmp]; |
| } |
| |
| - (void)viewDidLoad |
| { |
| [super viewDidLoad]; |
| UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); |
| NSError* error = nil; |
| |
| if (self.avSession == nil) { |
| // create audio session |
| self.avSession = [AVAudioSession sharedInstance]; |
| if (error) { |
| // return error if can't create recording audio session |
| NSLog(@"error creating audio session: %@", [[error userInfo] description]); |
| self.errorCode = CAPTURE_INTERNAL_ERR; |
| [self dismissAudioView:nil]; |
| } |
| } |
| |
| // create file to record to in temporary dir |
| |
| NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; // use file system temporary directory |
| NSError* err = nil; |
| NSFileManager* fileMgr = [[NSFileManager alloc] init]; |
| |
| // generate unique file name |
| NSString* filePath; |
| int i = 1; |
| do { |
| filePath = [NSString stringWithFormat:@"%@/audio_%03d.wav", docsPath, i++]; |
| } while ([fileMgr fileExistsAtPath:filePath]); |
| |
| NSURL* fileURL = [NSURL fileURLWithPath:filePath isDirectory:NO]; |
| |
| // create AVAudioPlayer |
| NSDictionary *recordSetting = [[NSMutableDictionary alloc] init]; |
| self.avRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:recordSetting error:&err]; |
| if (err) { |
| NSLog(@"Failed to initialize AVAudioRecorder: %@\n", [err localizedDescription]); |
| self.avRecorder = nil; |
| // return error |
| self.errorCode = CAPTURE_INTERNAL_ERR; |
| [self dismissAudioView:nil]; |
| } else { |
| self.avRecorder.delegate = self; |
| [self.avRecorder prepareToRecord]; |
| self.recordButton.enabled = YES; |
| self.doneButton.enabled = YES; |
| } |
| } |
| |
| #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 |
| - (UIInterfaceOrientationMask)supportedInterfaceOrientations |
| { |
| UIInterfaceOrientationMask orientation = UIInterfaceOrientationMaskPortrait; |
| UIInterfaceOrientationMask supported = [captureCommand.viewController supportedInterfaceOrientations]; |
| |
| orientation = orientation | (supported & UIInterfaceOrientationMaskPortraitUpsideDown); |
| return orientation; |
| } |
| #else |
| - (NSUInteger)supportedInterfaceOrientations |
| { |
| NSUInteger orientation = UIInterfaceOrientationMaskPortrait; // must support portrait |
| NSUInteger supported = [captureCommand.viewController supportedInterfaceOrientations]; |
| |
| orientation = orientation | (supported & UIInterfaceOrientationMaskPortraitUpsideDown); |
| return orientation; |
| } |
| #endif |
| |
| - (void)viewDidUnload |
| { |
| [self setView:nil]; |
| [self.captureCommand setInUse:NO]; |
| } |
| |
| - (void)processButton:(id)sender |
| { |
| if (self.avRecorder.recording) { |
| // stop recording |
| [self.avRecorder stop]; |
| self.isTimed = NO; // recording was stopped via button so reset isTimed |
| // view cleanup will occur in audioRecordingDidFinishRecording |
| } else { |
| // begin recording |
| [self.recordButton setImage:stopRecordImage forState:UIControlStateNormal]; |
| self.recordButton.accessibilityTraits &= ~[self accessibilityTraits]; |
| [self.recordingView setHidden:NO]; |
| __block NSError* error = nil; |
| |
| __weak CDVAudioRecorderViewController* weakSelf = self; |
| |
| void (^startRecording)(void) = ^{ |
| [weakSelf.avSession setCategory:AVAudioSessionCategoryRecord error:&error]; |
| [weakSelf.avSession setActive:YES error:&error]; |
| if (error) { |
| // can't continue without active audio session |
| weakSelf.errorCode = CAPTURE_INTERNAL_ERR; |
| [weakSelf dismissAudioView:nil]; |
| } else { |
| if (weakSelf.duration) { |
| weakSelf.isTimed = true; |
| [weakSelf.avRecorder recordForDuration:[duration doubleValue]]; |
| } else { |
| [weakSelf.avRecorder record]; |
| } |
| [weakSelf.timerLabel setText:@"0.00"]; |
| weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:weakSelf selector:@selector(updateTime) userInfo:nil repeats:YES]; |
| weakSelf.doneButton.enabled = NO; |
| } |
| UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); |
| }; |
| |
| SEL rrpSel = NSSelectorFromString(@"requestRecordPermission:"); |
| if ([self.avSession respondsToSelector:rrpSel]) |
| { |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Warc-performSelector-leaks" |
| [self.avSession performSelector:rrpSel withObject:^(BOOL granted){ |
| if (granted) { |
| startRecording(); |
| } else { |
| NSLog(@"Error creating audio session, microphone permission denied."); |
| weakSelf.errorCode = CAPTURE_INTERNAL_ERR; |
| [weakSelf dismissAudioView:nil]; |
| } |
| }]; |
| #pragma clang diagnostic pop |
| } else { |
| startRecording(); |
| } |
| } |
| } |
| |
| /* |
| * helper method to clean up when stop recording |
| */ |
| - (void)stopRecordingCleanup |
| { |
| if (self.avRecorder.recording) { |
| [self.avRecorder stop]; |
| } |
| [self.recordButton setImage:recordImage forState:UIControlStateNormal]; |
| self.recordButton.accessibilityTraits |= [self accessibilityTraits]; |
| [self.recordingView setHidden:YES]; |
| self.doneButton.enabled = YES; |
| if (self.avSession) { |
| // deactivate session so sounds can come through |
| [self.avSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; |
| [self.avSession setActive:NO error:nil]; |
| } |
| if (self.duration && self.isTimed) { |
| // VoiceOver announcement so user knows timed recording has finished |
| //BOOL isUIAccessibilityAnnouncementNotification = (&UIAccessibilityAnnouncementNotification != NULL); |
| if (UIAccessibilityAnnouncementNotification) { |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500ull * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ |
| UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, PluginLocalizedString(captureCommand, @"timed recording complete", nil)); |
| }); |
| } |
| } else { |
| // issue a layout notification change so that VO will reannounce the button label when recording completes |
| UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); |
| } |
| } |
| |
| - (void)dismissAudioView:(id)sender |
| { |
| // called when done button pressed or when error condition to do cleanup and remove view |
| [[self.captureCommand.viewController.presentedViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil]; |
| |
| if (!self.pluginResult) { |
| // return error |
| self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:(int)self.errorCode]; |
| } |
| |
| self.avRecorder = nil; |
| [self.avSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; |
| [self.avSession setActive:NO error:nil]; |
| [self.captureCommand setInUse:NO]; |
| UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); |
| // return result |
| [self.captureCommand.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; |
| |
| if (IsAtLeastiOSVersion(@"7.0")) { |
| [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; |
| } |
| } |
| |
| - (void)updateTime |
| { |
| // update the label with the elapsed time |
| [self.timerLabel setText:[self formatTime:self.avRecorder.currentTime]]; |
| } |
| |
| - (NSString*)formatTime:(int)interval |
| { |
| // is this format universal? |
| int secs = interval % 60; |
| int min = interval / 60; |
| |
| if (interval < 60) { |
| return [NSString stringWithFormat:@"0:%02d", interval]; |
| } else { |
| return [NSString stringWithFormat:@"%d:%02d", min, secs]; |
| } |
| } |
| |
| - (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag |
| { |
| // may be called when timed audio finishes - need to stop time and reset buttons |
| [self.timer invalidate]; |
| [self stopRecordingCleanup]; |
| |
| // generate success result |
| if (flag) { |
| NSString* filePath = [avRecorder.url path]; |
| // NSLog(@"filePath: %@", filePath); |
| NSDictionary* fileDict = [captureCommand getMediaDictionaryFromPath:filePath ofType:@"audio/wav"]; |
| NSArray* fileArray = [NSArray arrayWithObject:fileDict]; |
| |
| self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; |
| } else { |
| self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR]; |
| } |
| } |
| |
| - (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder*)recorder error:(NSError*)error |
| { |
| [self.timer invalidate]; |
| [self stopRecordingCleanup]; |
| |
| NSLog(@"error recording audio"); |
| self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR]; |
| [self dismissAudioView:nil]; |
| } |
| |
| - (UIStatusBarStyle)preferredStatusBarStyle |
| { |
| return UIStatusBarStyleDefault; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated |
| { |
| if (IsAtLeastiOSVersion(@"7.0")) { |
| [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; |
| } |
| |
| [super viewWillAppear:animated]; |
| } |
| |
| @end |