diff --git a/android/src/main/java/com/johnsonsu/rnsoundplayer/RNSoundPlayerModule.java b/android/src/main/java/com/johnsonsu/rnsoundplayer/RNSoundPlayerModule.java index 2a1561c..de48497 100644 --- a/android/src/main/java/com/johnsonsu/rnsoundplayer/RNSoundPlayerModule.java +++ b/android/src/main/java/com/johnsonsu/rnsoundplayer/RNSoundPlayerModule.java @@ -6,6 +6,7 @@ import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnPreparedListener; import android.net.Uri; + import java.io.File; import java.io.IOException; @@ -25,6 +26,7 @@ public class RNSoundPlayerModule extends ReactContextBaseJavaModule implements LifecycleEventListener { + public final static String EVENT_SETUP_ERROR = "OnSetupError"; public final static String EVENT_FINISHED_PLAYING = "FinishedPlaying"; public final static String EVENT_FINISHED_LOADING = "FinishedLoading"; public final static String EVENT_FINISHED_LOADING_FILE = "FinishedLoadingFile"; @@ -40,6 +42,7 @@ public RNSoundPlayerModule(ReactApplicationContext reactContext) { this.reactContext = reactContext; this.volume = 1.0f; this.audioManager = (AudioManager) this.reactContext.getSystemService(Context.AUDIO_SERVICE); + reactContext.addLifecycleEventListener(this); } @Override @@ -54,14 +57,21 @@ public void setSpeaker(Boolean on) { } @Override - public void onHostResume() {} + public void onHostResume() { + } @Override - public void onHostPause() {} + public void onHostPause() { + } @Override public void onHostDestroy() { + this.stop(); + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } } @ReactMethod @@ -111,7 +121,7 @@ public void stop() throws IllegalStateException { @ReactMethod public void seek(float seconds) throws IllegalStateException { if (this.mediaPlayer != null) { - this.mediaPlayer.seekTo((int)seconds * 1000); + this.mediaPlayer.seekTo((int) seconds * 1000); } } @@ -125,7 +135,7 @@ public void setVolume(float volume) throws IOException { @ReactMethod public void getInfo( - Promise promise) { + Promise promise) { if (this.mediaPlayer == null) { promise.resolve(null); return; @@ -147,33 +157,15 @@ public void removeListeners(Integer count) { } private void sendEvent(ReactApplicationContext reactContext, - String eventName, - @Nullable WritableMap params) { + String eventName, + @Nullable WritableMap params) { reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, params); + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); } private void mountSoundFile(String name, String type) throws IOException { - if (this.mediaPlayer == null) { - int soundResID = getReactApplicationContext().getResources().getIdentifier(name, "raw", getReactApplicationContext().getPackageName()); - - if (soundResID > 0) { - this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), soundResID); - } else { - this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), this.getUriFromFile(name, type)); - } - - this.mediaPlayer.setOnCompletionListener( - new OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer arg0) { - WritableMap params = Arguments.createMap(); - params.putBoolean("success", true); - sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params); - } - }); - } else { + try { Uri uri; int soundResID = getReactApplicationContext().getResources().getIdentifier(name, "raw", getReactApplicationContext().getPackageName()); @@ -183,19 +175,17 @@ public void onCompletion(MediaPlayer arg0) { uri = this.getUriFromFile(name, type); } - this.mediaPlayer.reset(); - this.mediaPlayer.setDataSource(getCurrentActivity(), uri); - this.mediaPlayer.prepare(); + if (this.mediaPlayer == null) { + this.mediaPlayer = initializeMediaPlayer(uri); + } else { + this.mediaPlayer.reset(); + this.mediaPlayer.setDataSource(getCurrentActivity(), uri); + this.mediaPlayer.prepare(); + } + sendMountFileSuccessEvents(name, type); + } catch (IOException e) { + sendErrorEvent(e); } - - WritableMap params = Arguments.createMap(); - params.putBoolean("success", true); - sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING, params); - WritableMap onFinishedLoadingFileParams = Arguments.createMap(); - onFinishedLoadingFileParams.putBoolean("success", true); - onFinishedLoadingFileParams.putString("name", name); - onFinishedLoadingFileParams.putString("type", type); - sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING_FILE, onFinishedLoadingFileParams); } private Uri getUriFromFile(String name, String type) { @@ -214,37 +204,74 @@ private Uri getUriFromFile(String name, String type) { } private void prepareUrl(final String url) throws IOException { - if (this.mediaPlayer == null) { - Uri uri = Uri.parse(url); - this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), uri); - this.mediaPlayer.setOnCompletionListener( - new OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer arg0) { - WritableMap params = Arguments.createMap(); - params.putBoolean("success", true); - sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params); - } - }); - this.mediaPlayer.setOnPreparedListener( - new OnPreparedListener() { - @Override - public void onPrepared(MediaPlayer mediaPlayer) { - WritableMap onFinishedLoadingURLParams = Arguments.createMap(); - onFinishedLoadingURLParams.putBoolean("success", true); - onFinishedLoadingURLParams.putString("url", url); - sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING_URL, onFinishedLoadingURLParams); - } - } - ); - } else { - Uri uri = Uri.parse(url); - this.mediaPlayer.reset(); - this.mediaPlayer.setDataSource(getCurrentActivity(), uri); - this.mediaPlayer.prepare(); + try { + if (this.mediaPlayer == null) { + Uri uri = Uri.parse(url); + this.mediaPlayer = initializeMediaPlayer(uri); + this.mediaPlayer.setOnPreparedListener( + new OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mediaPlayer) { + WritableMap onFinishedLoadingURLParams = Arguments.createMap(); + onFinishedLoadingURLParams.putBoolean("success", true); + onFinishedLoadingURLParams.putString("url", url); + sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING_URL, onFinishedLoadingURLParams); + } + } + ); + } else { + Uri uri = Uri.parse(url); + this.mediaPlayer.reset(); + this.mediaPlayer.setDataSource(getCurrentActivity(), uri); + this.mediaPlayer.prepare(); + } + WritableMap params = Arguments.createMap(); + params.putBoolean("success", true); + sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING, params); + } catch (IOException e) { + WritableMap errorParams = Arguments.createMap(); + errorParams.putString("error", e.getMessage()); + sendEvent(getReactApplicationContext(), EVENT_SETUP_ERROR, errorParams); + } + } + + private MediaPlayer initializeMediaPlayer(Uri uri) throws IOException { + MediaPlayer mediaPlayer = MediaPlayer.create(getCurrentActivity(), uri); + + if (mediaPlayer == null) { + throw new IOException("Failed to initialize MediaPlayer for URI: " + uri.toString()); } + + mediaPlayer.setOnCompletionListener( + new OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer arg0) { + WritableMap params = Arguments.createMap(); + params.putBoolean("success", true); + sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params); + } + } + ); + + return mediaPlayer; + } + + private void sendMountFileSuccessEvents(String name, String type) { WritableMap params = Arguments.createMap(); params.putBoolean("success", true); - sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING, params); + sendEvent(reactContext, EVENT_FINISHED_LOADING, params); + + WritableMap onFinishedLoadingFileParams = Arguments.createMap(); + onFinishedLoadingFileParams.putBoolean("success", true); + onFinishedLoadingFileParams.putString("name", name); + onFinishedLoadingFileParams.putString("type", type); + sendEvent(reactContext, EVENT_FINISHED_LOADING_FILE, onFinishedLoadingFileParams); + } + + + private void sendErrorEvent(IOException e) { + WritableMap errorParams = Arguments.createMap(); + errorParams.putString("error", e.getMessage()); + sendEvent(reactContext, EVENT_SETUP_ERROR, errorParams); } } diff --git a/index.d.ts b/index.d.ts index 436e072..9d52912 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,6 +2,7 @@ declare module "react-native-sound-player" { import { EmitterSubscription } from "react-native"; export type SoundPlayerEvent = + | "OnSetupError" | "FinishedLoading" | "FinishedPlaying" | "FinishedLoadingURL" diff --git a/index.js b/index.js index 9952c76..f4ea331 100644 --- a/index.js +++ b/index.js @@ -42,8 +42,8 @@ export default { } _finishedPlayingListener = _soundPlayerEmitter.addListener( - "FinishedPlaying", - callback + "FinishedPlaying", + callback ); }, @@ -61,6 +61,7 @@ export default { addEventListener: ( eventName: + | "OnSetupError" | "FinishedLoading" | "FinishedPlaying" | "FinishedLoadingURL" diff --git a/ios/RNSoundPlayer.m b/ios/RNSoundPlayer.m index 77bf99b..c07ab9f 100644 --- a/ios/RNSoundPlayer.m +++ b/ios/RNSoundPlayer.m @@ -5,18 +5,49 @@ // #import "RNSoundPlayer.h" +#import @implementation RNSoundPlayer +static NSString *const EVENT_SETUP_ERROR = @"OnSetupError"; static NSString *const EVENT_FINISHED_LOADING = @"FinishedLoading"; static NSString *const EVENT_FINISHED_LOADING_FILE = @"FinishedLoadingFile"; static NSString *const EVENT_FINISHED_LOADING_URL = @"FinishedLoadingURL"; static NSString *const EVENT_FINISHED_PLAYING = @"FinishedPlaying"; +RCT_EXPORT_MODULE(); + +@synthesize bridge = _bridge; + ++ (BOOL)requiresMainQueueSetup { + return YES; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.loopCount = 0; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(itemDidFinishPlaying:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (NSArray *)supportedEvents { + return @[EVENT_FINISHED_PLAYING, EVENT_FINISHED_LOADING, EVENT_FINISHED_LOADING_URL, EVENT_FINISHED_LOADING_FILE, EVENT_SETUP_ERROR]; +} RCT_EXPORT_METHOD(playUrl:(NSString *)url) { [self prepareUrl:url]; - [self.avPlayer play]; + if (self.avPlayer) { + [self.avPlayer play]; + } } RCT_EXPORT_METHOD(loadUrl:(NSString *)url) { @@ -25,22 +56,22 @@ @implementation RNSoundPlayer RCT_EXPORT_METHOD(playSoundFile:(NSString *)name ofType:(NSString *)type) { [self mountSoundFile:name ofType:type]; - [self.player play]; + if (self.player) { + [self.player play]; + } } RCT_EXPORT_METHOD(playSoundFileWithDelay:(NSString *)name ofType:(NSString *)type delay:(double)delay) { [self mountSoundFile:name ofType:type]; - [self.player playAtTime:(self.player.deviceCurrentTime + delay)]; + if (self.player) { + [self.player playAtTime:(self.player.deviceCurrentTime + delay)]; + } } RCT_EXPORT_METHOD(loadSoundFile:(NSString *)name ofType:(NSString *)type) { [self mountSoundFile:name ofType:type]; } -- (NSArray *)supportedEvents { - return @[EVENT_FINISHED_PLAYING, EVENT_FINISHED_LOADING, EVENT_FINISHED_LOADING_URL, EVENT_FINISHED_LOADING_FILE]; -} - RCT_EXPORT_METHOD(pause) { if (self.player != nil) { [self.player pause]; @@ -65,6 +96,7 @@ @implementation RNSoundPlayer } if (self.avPlayer != nil) { [self.avPlayer pause]; + [self.avPlayer seekToTime:kCMTimeZero]; } } @@ -73,45 +105,52 @@ @implementation RNSoundPlayer self.player.currentTime = seconds; } if (self.avPlayer != nil) { - [self.avPlayer seekToTime: CMTimeMakeWithSeconds(seconds, 1.0)]; + [self.avPlayer seekToTime:CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC)]; } } #if !TARGET_OS_TV -RCT_EXPORT_METHOD(setSpeaker:(BOOL) on) { +RCT_EXPORT_METHOD(setSpeaker:(BOOL)on) { AVAudioSession *session = [AVAudioSession sharedInstance]; + NSError *error = nil; if (on) { - [session setCategory: AVAudioSessionCategoryPlayAndRecord error: nil]; - [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil]; + [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; + [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]; } else { - [session setCategory: AVAudioSessionCategoryPlayback error: nil]; - [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil]; + [session setCategory:AVAudioSessionCategoryPlayback error:&error]; + [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]; + } + [session setActive:YES error:&error]; + if (error) { + [self sendErrorEvent:error]; } - [session setActive:true error:nil]; } #endif -RCT_EXPORT_METHOD(setMixAudio:(BOOL) on) { +RCT_EXPORT_METHOD(setMixAudio:(BOOL)on) { AVAudioSession *session = [AVAudioSession sharedInstance]; - + NSError *error = nil; if (on) { - [session setCategory: AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; + [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]; } else { - [session setCategory: AVAudioSessionCategoryPlayback withOptions:0 error:nil]; + [session setCategory:AVAudioSessionCategoryPlayback withOptions:0 error:&error]; + } + [session setActive:YES error:&error]; + if (error) { + [self sendErrorEvent:error]; } - [session setActive:true error:nil]; } -RCT_EXPORT_METHOD(setVolume:(float) volume) { +RCT_EXPORT_METHOD(setVolume:(float)volume) { if (self.player != nil) { - [self.player setVolume: volume]; + [self.player setVolume:volume]; } if (self.avPlayer != nil) { - [self.avPlayer setVolume: volume]; + [self.avPlayer setVolume:volume]; } } -RCT_EXPORT_METHOD(setNumberOfLoops:(NSInteger) loopCount) { +RCT_EXPORT_METHOD(setNumberOfLoops:(NSInteger)loopCount) { self.loopCount = loopCount; if (self.player != nil) { [self.player setNumberOfLoops:loopCount]; @@ -119,17 +158,15 @@ @implementation RNSoundPlayer } RCT_REMAP_METHOD(getInfo, - getInfoWithResolver:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject) { + getInfoWithResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { if (self.player != nil) { NSDictionary *data = @{ @"currentTime": [NSNumber numberWithDouble:[self.player currentTime]], @"duration": [NSNumber numberWithDouble:[self.player duration]] }; resolve(data); - return; - } - if (self.avPlayer != nil) { + } else if (self.avPlayer != nil) { CMTime currentTime = [[self.avPlayer currentItem] currentTime]; CMTime duration = [[[self.avPlayer currentItem] asset] duration]; NSDictionary *data = @{ @@ -137,60 +174,74 @@ @implementation RNSoundPlayer @"duration": [NSNumber numberWithFloat:CMTimeGetSeconds(duration)] }; resolve(data); - return; + } else { + resolve(nil); } - resolve(nil); } -- (void) audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { +- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { [self sendEventWithName:EVENT_FINISHED_PLAYING body:@{@"success": [NSNumber numberWithBool:flag]}]; } -- (void) itemDidFinishPlaying:(NSNotification *) notification { - [self sendEventWithName:EVENT_FINISHED_PLAYING body:@{@"success": [NSNumber numberWithBool:TRUE]}]; +- (void)itemDidFinishPlaying:(NSNotification *)notification { + [self sendEventWithName:EVENT_FINISHED_PLAYING body:@{@"success": [NSNumber numberWithBool:YES]}]; } -- (void) mountSoundFile:(NSString *)name ofType:(NSString *)type { +- (void)mountSoundFile:(NSString *)name ofType:(NSString *)type { if (self.avPlayer) { self.avPlayer = nil; } - + NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:name ofType:type]; - + if (soundFilePath == nil) { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES); + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; - soundFilePath = [NSString stringWithFormat:@"%@.%@", [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",name]], type]; + soundFilePath = [NSString stringWithFormat:@"%@.%@", [documentsDirectory stringByAppendingPathComponent:name], type]; } - + NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath]; - self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:nil]; + NSError *error = nil; + self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error]; + if (error) { + [self sendErrorEvent:error]; + return; + } [self.player setDelegate:self]; [self.player setNumberOfLoops:self.loopCount]; [self.player prepareToPlay]; - [[AVAudioSession sharedInstance] - setCategory: AVAudioSessionCategoryPlayback - error: nil]; - [self sendEventWithName:EVENT_FINISHED_LOADING body:@{@"success": [NSNumber numberWithBool:true]}]; - [self sendEventWithName:EVENT_FINISHED_LOADING_FILE body:@{@"success": [NSNumber numberWithBool:true], @"name": name, @"type": type}]; + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; + if (error) { + [self sendErrorEvent:error]; + return; + } + [self sendEventWithName:EVENT_FINISHED_LOADING body:@{@"success": [NSNumber numberWithBool:YES]}]; + [self sendEventWithName:EVENT_FINISHED_LOADING_FILE body:@{@"success": [NSNumber numberWithBool:YES], @"name": name, @"type": type}]; } -- (void) prepareUrl:(NSString *)url { +- (void)prepareUrl:(NSString *)url { if (self.player) { self.player = nil; } NSURL *soundURL = [NSURL URLWithString:url]; - - if (!self.avPlayer) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; - } - self.avPlayer = [[AVPlayer alloc] initWithURL:soundURL]; - [self.player prepareToPlay]; - [self sendEventWithName:EVENT_FINISHED_LOADING body:@{@"success": [NSNumber numberWithBool:true]}]; - [self sendEventWithName:EVENT_FINISHED_LOADING_URL body: @{@"success": [NSNumber numberWithBool:true], @"url": url}]; + [self.avPlayer.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil]; } -RCT_EXPORT_MODULE(); +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == self.avPlayer.currentItem && [keyPath isEqualToString:@"status"]) { + if (self.avPlayer.currentItem.status == AVPlayerItemStatusReadyToPlay) { + [self sendEventWithName:EVENT_FINISHED_LOADING body:@{@"success": [NSNumber numberWithBool:YES]}]; + NSURL *url = [(AVURLAsset *)self.avPlayer.currentItem.asset URL]; + [self sendEventWithName:EVENT_FINISHED_LOADING_URL body:@{@"success": [NSNumber numberWithBool:YES], @"url": [url absoluteString]}]; + } else if (self.avPlayer.currentItem.status == AVPlayerItemStatusFailed) { + [self sendErrorEvent:self.avPlayer.currentItem.error]; + } + } +} + +- (void)sendErrorEvent:(NSError *)error { + [self sendEventWithName:EVENT_SETUP_ERROR body:@{@"error": [error localizedDescription]}]; +} @end