diff options
| author | Arjun Roychowdhury <pliablepixels@gmail.com> | 2015-10-28 13:31:08 -0400 |
|---|---|---|
| committer | Arjun Roychowdhury <pliablepixels@gmail.com> | 2015-10-28 13:31:08 -0400 |
| commit | e76b54b8e3f3d7299e5a921dcecc9dc442b278e1 (patch) | |
| tree | cbfa4476dae975ed443361e37acef0ab0a45bfa1 /plugins/org.apache.cordova.media/src/ios/CDVSound.m | |
| parent | 3cb5cda7583566cec66aabf3543b0d876a864369 (diff) | |
media plugin
Diffstat (limited to 'plugins/org.apache.cordova.media/src/ios/CDVSound.m')
| -rw-r--r-- | plugins/org.apache.cordova.media/src/ios/CDVSound.m | 703 |
1 files changed, 703 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.media/src/ios/CDVSound.m b/plugins/org.apache.cordova.media/src/ios/CDVSound.m new file mode 100644 index 00000000..309b2e29 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/ios/CDVSound.m @@ -0,0 +1,703 @@ +/* + 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 "CDVSound.h" +#import "CDVFile.h" +#import <Cordova/NSArray+Comparisons.h> + +#define DOCUMENTS_SCHEME_PREFIX @"documents://" +#define HTTP_SCHEME_PREFIX @"http://" +#define HTTPS_SCHEME_PREFIX @"https://" +#define CDVFILE_PREFIX @"cdvfile://" +#define RECORDING_WAV @"wav" + +@implementation CDVSound + +@synthesize soundCache, avSession; + +// Maps a url for a resource path for recording +- (NSURL*)urlForRecording:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + + // first check for correct extension + if ([[resourcePath pathExtension] caseInsensitiveCompare:RECORDING_WAV] != NSOrderedSame) { + resourceURL = nil; + NSLog(@"Resource for recording must have %@ extension", RECORDING_WAV); + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + // try to find Documents:// resources + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else if ([resourcePath hasPrefix:CDVFILE_PREFIX]) { + CDVFile *filePlugin = [self.commandDelegate getCommandInstance:@"File"]; + CDVFilesystemURL *url = [CDVFilesystemURL fileSystemURLWithString:resourcePath]; + filePath = [filePlugin filesystemPathForURL:url]; + if (filePath == nil) { + resourceURL = [NSURL URLWithString:resourcePath]; + } + } else { + // if resourcePath is not from FileSystem put in tmp dir, else attempt to use provided resource path + NSString* tmpPath = [NSTemporaryDirectory()stringByStandardizingPath]; + BOOL isTmp = [resourcePath rangeOfString:tmpPath].location != NSNotFound; + BOOL isDoc = [resourcePath rangeOfString:docsPath].location != NSNotFound; + if (!isTmp && !isDoc) { + // put in temp dir + filePath = [NSString stringWithFormat:@"%@/%@", tmpPath, resourcePath]; + } else { + filePath = resourcePath; + } + } + + if (filePath != nil) { + // create resourceURL + resourceURL = [NSURL fileURLWithPath:filePath]; + } + return resourceURL; +} + +// Maps a url for a resource path for playing +// "Naked" resource paths are assumed to be from the www folder as its base +- (NSURL*)urlForPlaying:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + + // first try to find HTTP:// or Documents:// resources + + if ([resourcePath hasPrefix:HTTP_SCHEME_PREFIX] || [resourcePath hasPrefix:HTTPS_SCHEME_PREFIX]) { + // if it is a http url, use it + NSLog(@"Will use resource '%@' from the Internet.", resourcePath); + resourceURL = [NSURL URLWithString:resourcePath]; + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else if ([resourcePath hasPrefix:CDVFILE_PREFIX]) { + CDVFile *filePlugin = [self.commandDelegate getCommandInstance:@"File"]; + CDVFilesystemURL *url = [CDVFilesystemURL fileSystemURLWithString:resourcePath]; + filePath = [filePlugin filesystemPathForURL:url]; + if (filePath == nil) { + resourceURL = [NSURL URLWithString:resourcePath]; + } + } else { + // attempt to find file path in www directory or LocalFileSystem.TEMPORARY directory + filePath = [self.commandDelegate pathForResource:resourcePath]; + if (filePath == nil) { + // see if this exists in the documents/temp directory from a previous recording + NSString* testPath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], resourcePath]; + if ([[NSFileManager defaultManager] fileExistsAtPath:testPath]) { + // inefficient as existence will be checked again below but only way to determine if file exists from previous recording + filePath = testPath; + NSLog(@"Will attempt to use file resource from LocalFileSystem.TEMPORARY directory"); + } else { + // attempt to use path provided + filePath = resourcePath; + NSLog(@"Will attempt to use file resource '%@'", filePath); + } + } else { + NSLog(@"Found resource '%@' in the web folder.", filePath); + } + } + // if the resourcePath resolved to a file path, check that file exists + if (filePath != nil) { + // create resourceURL + resourceURL = [NSURL fileURLWithPath:filePath]; + // try to access file + NSFileManager* fMgr = [NSFileManager defaultManager]; + if (![fMgr fileExistsAtPath:filePath]) { + resourceURL = nil; + NSLog(@"Unknown resource '%@'", resourcePath); + } + } + + return resourceURL; +} + +// Creates or gets the cached audio file resource object +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord +{ + BOOL bError = NO; + CDVMediaError errcode = MEDIA_ERR_NONE_SUPPORTED; + NSString* errMsg = @""; + NSString* jsString = nil; + CDVAudioFile* audioFile = nil; + NSURL* resourceURL = nil; + + if ([self soundCache] == nil) { + [self setSoundCache:[NSMutableDictionary dictionaryWithCapacity:1]]; + } else { + audioFile = [[self soundCache] objectForKey:mediaId]; + } + if (audioFile == nil) { + // validate resourcePath and create + if ((resourcePath == nil) || ![resourcePath isKindOfClass:[NSString class]] || [resourcePath isEqualToString:@""]) { + bError = YES; + errcode = MEDIA_ERR_ABORTED; + errMsg = @"invalid media src argument"; + } else { + audioFile = [[CDVAudioFile alloc] init]; + audioFile.resourcePath = resourcePath; + audioFile.resourceURL = nil; // validate resourceURL when actually play or record + [[self soundCache] setObject:audioFile forKey:mediaId]; + } + } + if (bValidate && (audioFile.resourceURL == nil)) { + if (bRecord) { + resourceURL = [self urlForRecording:resourcePath]; + } else { + resourceURL = [self urlForPlaying:resourcePath]; + } + if (resourceURL == nil) { + bError = YES; + errcode = MEDIA_ERR_ABORTED; + errMsg = [NSString stringWithFormat:@"Cannot use audio file from resource '%@'", resourcePath]; + } else { + audioFile.resourceURL = resourceURL; + } + } + + if (bError) { + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:errcode message:errMsg]]; + [self.commandDelegate evalJs:jsString]; + } + + return audioFile; +} + +// returns whether or not audioSession is available - creates it if necessary +- (BOOL)hasAudioSession +{ + BOOL bSession = YES; + + if (!self.avSession) { + NSError* error = nil; + + self.avSession = [AVAudioSession sharedInstance]; + if (error) { + // is not fatal if can't get AVAudioSession , just log the error + NSLog(@"error creating audio session: %@", [[error userInfo] description]); + self.avSession = nil; + bSession = NO; + } + } + return bSession; +} + +// helper function to create a error object string +- (NSString*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message +{ + NSMutableDictionary* errorDict = [NSMutableDictionary dictionaryWithCapacity:2]; + + [errorDict setObject:[NSNumber numberWithUnsignedInteger:code] forKey:@"code"]; + [errorDict setObject:message ? message:@"" forKey:@"message"]; + + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:errorDict options:0 error:nil]; + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + +- (void)create:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + NSString* resourcePath = [command argumentAtIndex:1]; + + CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:NO forRecording:NO]; + + if (audioFile == nil) { + NSString* errorMessage = [NSString stringWithFormat:@"Failed to initialize Media file with path %@", resourcePath]; + NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMessage]]; + [self.commandDelegate evalJs:jsString]; + } else { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } +} + +- (void)setVolume:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + NSString* mediaId = [command argumentAtIndex:0]; + NSNumber* volume = [command argumentAtIndex:1 withDefault:[NSNumber numberWithFloat:1.0]]; + + if ([self soundCache] != nil) { + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + if (audioFile != nil) { + audioFile.volume = volume; + if (audioFile.player) { + audioFile.player.volume = [volume floatValue]; + } + [[self soundCache] setObject:audioFile forKey:mediaId]; + } + } + + // don't care for any callbacks +} + +- (void)startPlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + NSString* mediaId = [command argumentAtIndex:0]; + NSString* resourcePath = [command argumentAtIndex:1]; + NSDictionary* options = [command argumentAtIndex:2 withDefault:nil]; + + BOOL bError = NO; + NSString* jsString = nil; + + CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:YES forRecording:NO]; + if ((audioFile != nil) && (audioFile.resourceURL != nil)) { + if (audioFile.player == nil) { + bError = [self prepareToPlay:audioFile withId:mediaId]; + } + if (!bError) { + // audioFile.player != nil or player was successfully created + // get the audioSession and set the category to allow Playing when device is locked or ring/silent switch engaged + if ([self hasAudioSession]) { + NSError* __autoreleasing err = nil; + NSNumber* playAudioWhenScreenIsLocked = [options objectForKey:@"playAudioWhenScreenIsLocked"]; + BOOL bPlayAudioWhenScreenIsLocked = YES; + if (playAudioWhenScreenIsLocked != nil) { + bPlayAudioWhenScreenIsLocked = [playAudioWhenScreenIsLocked boolValue]; + } + + NSString* sessionCategory = bPlayAudioWhenScreenIsLocked ? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient; + [self.avSession setCategory:sessionCategory error:&err]; + if (![self.avSession setActive:YES error:&err]) { + // other audio with higher priority that does not allow mixing could cause this to fail + NSLog(@"Unable to play audio: %@", [err localizedFailureReason]); + bError = YES; + } + } + if (!bError) { + NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + NSNumber* loopOption = [options objectForKey:@"numberOfLoops"]; + NSInteger numberOfLoops = 0; + if (loopOption != nil) { + numberOfLoops = [loopOption intValue] - 1; + } + audioFile.player.numberOfLoops = numberOfLoops; + if (audioFile.player.isPlaying) { + [audioFile.player stop]; + audioFile.player.currentTime = 0; + } + if (audioFile.volume != nil) { + audioFile.player.volume = [audioFile.volume floatValue]; + } + + [audioFile.player play]; + double position = round(audioFile.player.duration * 1000) / 1000; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_DURATION, position, @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_RUNNING]; + [self.commandDelegate evalJs:jsString]; + } + } + if (bError) { + /* I don't see a problem playing previously recorded audio so removing this section - BG + NSError* error; + // try loading it one more time, in case the file was recorded previously + audioFile.player = [[ AVAudioPlayer alloc ] initWithContentsOfURL:audioFile.resourceURL error:&error]; + if (error != nil) { + NSLog(@"Failed to initialize AVAudioPlayer: %@\n", error); + audioFile.player = nil; + } else { + NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + audioFile.player.numberOfLoops = numberOfLoops; + [audioFile.player play]; + } */ + // error creating the session or player + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_NONE_SUPPORTED]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_NONE_SUPPORTED message:nil]]; + [self.commandDelegate evalJs:jsString]; + } + } + // else audioFile was nil - error already returned from audioFile for resource + return; +} + +- (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId +{ + BOOL bError = NO; + NSError* __autoreleasing playerError = nil; + + // create the player + NSURL* resourceURL = audioFile.resourceURL; + + if ([resourceURL isFileURL]) { + audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:resourceURL error:&playerError]; + } else { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:resourceURL]; + NSString* userAgent = [self.commandDelegate userAgent]; + if (userAgent) { + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + NSURLResponse* __autoreleasing response = nil; + NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&playerError]; + if (playerError) { + NSLog(@"Unable to download audio from: %@", [resourceURL absoluteString]); + } else { + // bug in AVAudioPlayer when playing downloaded data in NSData - we have to download the file and play from disk + CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); + CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + NSString* filePath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], uuidString]; + CFRelease(uuidString); + CFRelease(uuidRef); + + [data writeToFile:filePath atomically:YES]; + NSURL* fileURL = [NSURL fileURLWithPath:filePath]; + audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&playerError]; + } + } + + if (playerError != nil) { + NSLog(@"Failed to initialize AVAudioPlayer: %@\n", [playerError localizedDescription]); + audioFile.player = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + bError = YES; + } else { + audioFile.player.mediaId = mediaId; + audioFile.player.delegate = self; + bError = ![audioFile.player prepareToPlay]; + } + return bError; +} + +- (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSLog(@"Stopped playing audio sample '%@'", audioFile.resourcePath); + [audioFile.player stop]; + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } // ignore if no media playing + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + NSString* jsString = nil; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSLog(@"Paused playing audio sample '%@'", audioFile.resourcePath); + [audioFile.player pause]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_PAUSED]; + } + // ignore if no media playing + + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)seekToAudio:(CDVInvokedUrlCommand*)command +{ + // args: + // 0 = Media id + // 1 = path to resource + // 2 = seek to location in milliseconds + + NSString* mediaId = [command argumentAtIndex:0]; + + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + double position = [[command argumentAtIndex:1] doubleValue]; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSString* jsString; + double posInSeconds = position / 1000; + if (posInSeconds >= audioFile.player.duration) { + // The seek is past the end of file. Stop media and reset to beginning instead of seeking past the end. + [audioFile.player stop]; + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_POSITION, 0.0, @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + // NSLog(@"seekToEndJsString=%@",jsString); + } else { + audioFile.player.currentTime = posInSeconds; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%f);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_POSITION, posInSeconds]; + // NSLog(@"seekJsString=%@",jsString); + } + + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)release:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + + if (mediaId != nil) { + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + if (audioFile != nil) { + if (audioFile.player && [audioFile.player isPlaying]) { + [audioFile.player stop]; + } + if (audioFile.recorder && [audioFile.recorder isRecording]) { + [audioFile.recorder stop]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + self.avSession = nil; + } + [[self soundCache] removeObjectForKey:mediaId]; + NSLog(@"Media with id %@ released", mediaId); + } + } +} + +- (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* mediaId = [command argumentAtIndex:0]; + +#pragma unused(mediaId) + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + double position = -1; + + if ((audioFile != nil) && (audioFile.player != nil) && [audioFile.player isPlaying]) { + position = round(audioFile.player.currentTime * 1000) / 1000; + } + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:position]; + + NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_POSITION, position]; + [self.commandDelegate evalJs:jsString]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)startRecordingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + + NSString* mediaId = [command argumentAtIndex:0]; + CDVAudioFile* audioFile = [self audioFileForResource:[command argumentAtIndex:1] withId:mediaId doValidation:YES forRecording:YES]; + __block NSString* jsString = nil; + __block NSString* errorMsg = @""; + + if ((audioFile != nil) && (audioFile.resourceURL != nil)) { + void (^startRecording)(void) = ^{ + NSError* __autoreleasing error = nil; + + if (audioFile.recorder != nil) { + [audioFile.recorder stop]; + audioFile.recorder = nil; + } + // get the audioSession and set the category to allow recording when device is locked or ring/silent switch engaged + if ([self hasAudioSession]) { + if (![self.avSession.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { + [self.avSession setCategory:AVAudioSessionCategoryRecord error:nil]; + } + + if (![self.avSession setActive:YES error:&error]) { + // other audio with higher priority that does not allow mixing could cause this to fail + errorMsg = [NSString stringWithFormat:@"Unable to record audio: %@", [error localizedFailureReason]]; + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_ABORTED]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + return; + } + } + + // create a new recorder for each start record + audioFile.recorder = [[CDVAudioRecorder alloc] initWithURL:audioFile.resourceURL settings:nil error:&error]; + + bool recordingSuccess = NO; + if (error == nil) { + audioFile.recorder.delegate = self; + audioFile.recorder.mediaId = mediaId; + recordingSuccess = [audioFile.recorder record]; + if (recordingSuccess) { + NSLog(@"Started recording audio sample '%@'", audioFile.resourcePath); + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_RUNNING]; + [self.commandDelegate evalJs:jsString]; + } + } + + if ((error != nil) || (recordingSuccess == NO)) { + if (error != nil) { + errorMsg = [NSString stringWithFormat:@"Failed to initialize AVAudioRecorder: %@\n", [error localizedFailureReason]]; + } else { + errorMsg = @"Failed to start recording using AVAudioRecorder"; + } + audioFile.recorder = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + } + }; + + SEL rrpSel = NSSelectorFromString(@"requestRecordPermission:"); + if ([self hasAudioSession] && [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 { + NSString* msg = @"Error creating audio session, microphone permission denied."; + NSLog(@"%@", msg); + audioFile.recorder = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:msg]]; + [self.commandDelegate evalJs:jsString]; + } + }]; +#pragma clang diagnostic pop + } else { + startRecording(); + } + + } else { + // file did not validate + NSString* errorMsg = [NSString stringWithFormat:@"Could not record audio at '%@'", audioFile.resourcePath]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if ((audioFile != nil) && (audioFile.recorder != nil)) { + NSLog(@"Stopped recording audio sample '%@'", audioFile.resourcePath); + [audioFile.recorder stop]; + // no callback - that will happen in audioRecorderDidFinishRecording + } + // ignore if no media recording + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag +{ + CDVAudioRecorder* aRecorder = (CDVAudioRecorder*)recorder; + NSString* mediaId = aRecorder.mediaId; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if (audioFile != nil) { + NSLog(@"Finished recording audio sample '%@'", audioFile.resourcePath); + } + if (flag) { + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } else { + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_DECODE]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + [self.commandDelegate evalJs:jsString]; +} + +- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer*)player successfully:(BOOL)flag +{ + CDVAudioPlayer* aPlayer = (CDVAudioPlayer*)player; + NSString* mediaId = aPlayer.mediaId; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if (audioFile != nil) { + NSLog(@"Finished playing audio sample '%@'", audioFile.resourcePath); + } + if (flag) { + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } else { + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_DECODE]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + [self.commandDelegate evalJs:jsString]; +} + +- (void)onMemoryWarning +{ + [[self soundCache] removeAllObjects]; + [self setSoundCache:nil]; + [self setAvSession:nil]; + + [super onMemoryWarning]; +} + +- (void)dealloc +{ + [[self soundCache] removeAllObjects]; +} + +- (void)onReset +{ + for (CDVAudioFile* audioFile in [[self soundCache] allValues]) { + if (audioFile != nil) { + if (audioFile.player != nil) { + [audioFile.player stop]; + audioFile.player.currentTime = 0; + } + if (audioFile.recorder != nil) { + [audioFile.recorder stop]; + } + } + } + + [[self soundCache] removeAllObjects]; +} + +@end + +@implementation CDVAudioFile + +@synthesize resourcePath; +@synthesize resourceURL; +@synthesize player, volume; +@synthesize recorder; + +@end +@implementation CDVAudioPlayer +@synthesize mediaId; + +@end + +@implementation CDVAudioRecorder +@synthesize mediaId; + +@end |
