├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── npm-publish.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── RNSoundPlayer.podspec ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── johnsonsu │ └── rnsoundplayer │ ├── RNSoundPlayerModule.java │ └── RNSoundPlayerPackage.java ├── index.d.ts ├── index.js ├── ios ├── RNSoundPlayer.h ├── RNSoundPlayer.m └── RNSoundPlayer.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ └── contents.xcworkspacedata └── package.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Platform (please complete the following information):** 24 | - OS: [e.g. iOS or Android] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish NPM Package 5 | 6 | on: 7 | workflow_dispatch: 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | publish-npm: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | registry-url: https://registry.npmjs.org/ 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{secrets.NPM_ACCESS_TOKEN}} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | 5 | # Icon must end with two \r 6 | Icon 7 | 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # Xcode 29 | # 30 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 31 | 32 | ## Build generated 33 | build/ 34 | DerivedData/ 35 | 36 | ## Various settings 37 | *.pbxuser 38 | !default.pbxuser 39 | *.mode1v3 40 | !default.mode1v3 41 | *.mode2v3 42 | !default.mode2v3 43 | *.perspectivev3 44 | !default.perspectivev3 45 | xcuserdata/ 46 | 47 | ## Other 48 | *.moved-aside 49 | *.xccheckout 50 | *.xcscmblueprint 51 | 52 | ## Obj-C/Swift specific 53 | *.hmap 54 | *.ipa 55 | *.dSYM.zip 56 | *.dSYM 57 | 58 | # CocoaPods 59 | # 60 | # We recommend against adding the Pods directory to your .gitignore. However 61 | # you should judge for yourself, the pros and cons are mentioned at: 62 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 63 | # 64 | # Pods/ 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 76 | # screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Johnson Su 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-sound-player 2 | 3 | Play audio files, stream audio from URL, using ReactNative. 4 | 5 | ## Installation 6 | 7 | ### 1. `yarn` or `npm` 8 | 9 | ``` 10 | // yarn 11 | yarn add react-native-sound-player 12 | // or npm 13 | npm install --save react-native-sound-player 14 | ``` 15 | 16 | ### 2. Link 17 | 18 | For RN >= 0.60 you can skip this step. 19 | 20 | ``` 21 | react-native link react-native-sound-player 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Play sound with file name and type 27 | 28 | 1. Add sound files to iOS/Android. 29 | 30 | - On iOS, drag and drop sound file into project in Xcode. Remember to check **"Copy items if needed"** option and **"Add to targets"**. 31 | - On Android, put sound files in `{project_root}/android/app/src/main/res/raw/`. Just create the folder if it doesn't exist. 32 | - When using playAsset() you only need to copy the file to the projects root directory or a subfolder like assets 33 | 34 | 2. Import the library and call the `playSoundFile(fileName, fileType)` function: 35 | 36 | ```javascript 37 | import SoundPlayer from "react-native-sound-player"; 38 | 39 | try { 40 | // play the file tone.mp3 41 | SoundPlayer.playSoundFile("tone", "mp3"); 42 | // or play from url 43 | SoundPlayer.playUrl("https://example.com/music.mp3"); 44 | // or play file from folder 45 | SoundPlayer.playAsset(require("./assets/tone.mp3")); 46 | } catch (e) { 47 | console.log(`cannot play the sound file`, e); 48 | } 49 | ``` 50 | 51 | > Please note that the device can still go to sleep (screen goes off) while audio is playing. 52 | > When this happens, the audio will stop playing. 53 | > To prevent this, you can use something like [react-native-keep-awake](https://github.com/corbt/react-native-keep-awake). 54 | > Or alternatively, for iOS, you can add a Background Mode of `Audio, AirPlay, and Picture in Picture` in XCode. To do this, select your application from Targets, then click on `Signing & Capabilities` and add `Background Modes`. once the options for it appear on your `Signing & Capabilities` page select the checkbox with `Audio, AirPlay, and Picture in Picture`. This will allow the application to continue playing audio when the app is in the background and even when the device is locked. 55 | 56 | ## Functions 57 | 58 | ### `playSoundFile(fileName: string, fileType: string)` 59 | 60 | Play the sound file named `fileName` with file type `fileType`. 61 | 62 | ### `playSoundFileWithDelay(fileName: string, fileType: string, delay: number)` - iOS Only 63 | 64 | Play the sound file named `fileName` with file type `fileType` after a a delay of `delay` in _seconds_ from the current device time. 65 | 66 | ### `loadSoundFile(fileName: string, fileType: string)` 67 | 68 | Load the sound file named `fileName` with file type `fileType`, without playing it. 69 | This is useful when you want to play a large file, which can be slow to mount, 70 | and have precise control on when the sound is played. This can also be used in 71 | combination with `getInfo()` to get audio file `duration` without playing it. 72 | You should subscribe to the `onFinishedLoading` event to get notified when the 73 | file is loaded. 74 | 75 | ### `playUrl(url: string)` 76 | 77 | Play the audio from url. Supported formats are: 78 | 79 | - [AVPlayer (iOS)](https://stackoverflow.com/questions/21879981/avfoundation-avplayer-supported-formats-no-vob-or-mpg-containers) 80 | - [MediaPlayer (Android)](https://developer.android.com/guide/topics/media/media-formats) 81 | 82 | ### `loadUrl(url: string)` 83 | 84 | Load the audio from the given `url` without playing it. You can then play the audio 85 | by calling `play()`. This might be useful when you find the delay between calling 86 | `playUrl()` and the sound actually starts playing is too much. 87 | 88 | ### `playAsset(asset: number)` 89 | 90 | Play the audio from an asset, to get the asset number use `require('./assets/tone.mp3')`. 91 | 92 | Supported formats see `playUrl()` function. 93 | 94 | ### `loadAsset(asset: number)` 95 | 96 | Load the audio from an asset like above but without playing it. You can then play the audio by calling `play()`. This might be useful when you find the delay between calling `playAsset()` and the sound actually starts playing is too much. 97 | 98 | ### `addEventListener(callback: (object: ResultObject) => SubscriptionObject)` 99 | 100 | Subscribe to any event. Returns a subscription object. Subscriptions created by this function cannot be removed by calling `unmount()`. You **NEED** to call `yourSubscriptionObject.remove()` when you no longer need this event listener or whenever your component unmounts. 101 | 102 | Supported events are: 103 | 104 | 1. `FinishedLoading` 105 | 2. `FinishedPlaying` 106 | 3. `FinishedLoadingURL` 107 | 4. `FinishedLoadingFile` 108 | 109 | ```javascript 110 | // Example 111 | ... 112 | // Create instance variable(s) to store your subscriptions in your class 113 | _onFinishedPlayingSubscription = null 114 | _onFinishedLoadingSubscription = null 115 | _onFinishedLoadingFileSubscription = null 116 | _onFinishedLoadingURLSubscription = null 117 | 118 | // Subscribe to event(s) you want when component mounted 119 | componentDidMount() { 120 | _onFinishedPlayingSubscription = SoundPlayer.addEventListener('FinishedPlaying', ({ success }) => { 121 | console.log('finished playing', success) 122 | }) 123 | _onFinishedLoadingSubscription = SoundPlayer.addEventListener('FinishedLoading', ({ success }) => { 124 | console.log('finished loading', success) 125 | }) 126 | _onFinishedLoadingFileSubscription = SoundPlayer.addEventListener('FinishedLoadingFile', ({ success, name, type }) => { 127 | console.log('finished loading file', success, name, type) 128 | }) 129 | _onFinishedLoadingURLSubscription = SoundPlayer.addEventListener('FinishedLoadingURL', ({ success, url }) => { 130 | console.log('finished loading url', success, url) 131 | }) 132 | } 133 | 134 | // Remove all the subscriptions when component will unmount 135 | componentWillUnmount() { 136 | _onFinishedPlayingSubscription.remove() 137 | _onFinishedLoadingSubscription.remove() 138 | _onFinishedLoadingURLSubscription.remove() 139 | _onFinishedLoadingFileSubscription.remove() 140 | } 141 | ... 142 | ``` 143 | 144 | ### `onFinishedPlaying(callback: (success: boolean) => any)` 145 | 146 | Subscribe to the "finished playing" event. The `callback` function is called whenever a file is finished playing. **This function will be deprecated soon, please use `addEventListener` above**. 147 | 148 | ### `onFinishedLoading(callback: (success: boolean) => any)` 149 | 150 | Subscribe to the "finished loading" event. The `callback` function is called whenever a file is finished loading, i.e. the file is ready to be `play()`, `resume()`, `getInfo()`, etc. **This function will be deprecated soon, please use `addEventListener` above**. 151 | 152 | ### `unmount()` 153 | 154 | Unsubscribe the "finished playing" and "finished loading" event. **This function will be deprecated soon, please use `addEventListener` and remove your own listener by calling `yourSubscriptionObject.remove()`**. 155 | 156 | ### `play()` 157 | 158 | Play the loaded sound file. This function is the same as `resume()`. 159 | 160 | ### `pause()` 161 | 162 | Pause the currently playing file. 163 | 164 | ### `resume()` 165 | 166 | Resume from pause and continue playing the same file. This function is the same as `play()`. 167 | 168 | ### `stop()` 169 | 170 | Stop playing, call `playSound(fileName: string, fileType: string)` to start playing again. 171 | 172 | ### `seek(seconds: number)` 173 | 174 | Seek to `seconds` of the currently playing file. 175 | 176 | ### `setSpeaker(on: boolean)` 177 | 178 | Overwrite default audio output to speaker, which forces `playUrl()` function to play from speaker. 179 | 180 | ### `setMixAudio(on: boolean)` 181 | 182 | Only available on iOS. If you set this option, your audio will be mixed with audio playing in background apps, such as the Music app. 183 | 184 | ### `setVolume(volume: number)` 185 | 186 | Set the volume of the current player. This does not change the volume of the device. 187 | 188 | ### `setNumberOfLoops(loops: number)` 189 | 190 | **iOS**: Set the number of loops. A negative value will loop indefinitely until the `stop()` command is called. 191 | 192 | **Android**: 0 will play the sound once. Any other number will loop indefinitely until the `stop()` command is called. 193 | 194 | ### `getInfo() => Promise<{currentTime: number, duration: number}>` 195 | 196 | Get the `currentTime` and `duration` of the currently mounted audio media. This function returns a promise which resolves to an Object containing `currentTime` and `duration` properties. 197 | 198 | ```javascript 199 | // Example 200 | ... 201 | playSong() { 202 | try { 203 | SoundPlayer.playSoundFile('engagementParty', 'm4a') 204 | } catch (e) { 205 | alert('Cannot play the file') 206 | console.log('cannot play the song file', e) 207 | } 208 | } 209 | 210 | async getInfo() { // You need the keyword `async` 211 | try { 212 | const info = await SoundPlayer.getInfo() // Also, you need to await this because it is async 213 | console.log('getInfo', info) // {duration: 12.416, currentTime: 7.691} 214 | } catch (e) { 215 | console.log('There is no song playing', e) 216 | } 217 | } 218 | 219 | onPressPlayButton() { 220 | this.playSong() 221 | this.getInfo() 222 | } 223 | 224 | ... 225 | ``` 226 | -------------------------------------------------------------------------------- /RNSoundPlayer.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 3 | 4 | Pod::Spec.new do |s| 5 | 6 | s.name = "RNSoundPlayer" 7 | s.version = package['version'] 8 | s.summary = package["description"] 9 | s.homepage = "https://github.com/johnsonsu/react-native-sound-player" 10 | s.license = package["license"] 11 | s.author = { "Johnson Su" => "johnsonsu@johnsonsu.com" } 12 | s.platforms = { :ios => "9.0", :tvos => "9.0" } 13 | s.source = { :git => "https://github.com/johnsonsu/react-native-sound-player.git", :tag => s.version } 14 | s.source_files = 'ios/**/*.{h,m}' 15 | s.preserve_paths = "package.json", "LICENSE" 16 | s.dependency 'React-Core' 17 | 18 | end 19 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | apply plugin: 'com.android.library' 3 | 4 | def DEFAULT_COMPILE_SDK_VERSION = 31 5 | def DEFAULT_BUILD_TOOLS_VERSION = '31.0.3' 6 | def DEFAULT_MIN_SDK_VERSION = 16 7 | def DEFAULT_TARGET_SDK_VERSION = 31 8 | 9 | def safeExtGet(prop, fallback) { 10 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 11 | } 12 | 13 | android { 14 | compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) 15 | buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION) 16 | 17 | defaultConfig { 18 | minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) 19 | targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) 20 | versionCode 1 21 | versionName "1.0" 22 | ndk { 23 | abiFilters "armeabi-v7a", "x86" 24 | } 25 | } 26 | lintOptions { 27 | warning 'InvalidPackage' 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation 'com.facebook.react:react-native:+' 33 | } 34 | 35 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/johnsonsu/rnsoundplayer/RNSoundPlayerModule.java: -------------------------------------------------------------------------------- 1 | package com.johnsonsu.rnsoundplayer; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.media.MediaPlayer; 6 | import android.media.MediaPlayer.OnCompletionListener; 7 | import android.media.MediaPlayer.OnPreparedListener; 8 | import android.net.Uri; 9 | 10 | import java.io.File; 11 | 12 | import java.io.IOException; 13 | import javax.annotation.Nullable; 14 | 15 | import com.facebook.react.bridge.ReactApplicationContext; 16 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 17 | import com.facebook.react.bridge.ReactMethod; 18 | import com.facebook.react.bridge.Callback; 19 | import com.facebook.react.bridge.ReactContext; 20 | import com.facebook.react.modules.core.DeviceEventManagerModule; 21 | import com.facebook.react.bridge.WritableMap; 22 | import com.facebook.react.bridge.Arguments; 23 | import com.facebook.react.bridge.Promise; 24 | import com.facebook.react.bridge.LifecycleEventListener; 25 | 26 | 27 | public class RNSoundPlayerModule extends ReactContextBaseJavaModule implements LifecycleEventListener { 28 | 29 | public final static String EVENT_SETUP_ERROR = "OnSetupError"; 30 | public final static String EVENT_FINISHED_PLAYING = "FinishedPlaying"; 31 | public final static String EVENT_FINISHED_LOADING = "FinishedLoading"; 32 | public final static String EVENT_FINISHED_LOADING_FILE = "FinishedLoadingFile"; 33 | public final static String EVENT_FINISHED_LOADING_URL = "FinishedLoadingURL"; 34 | 35 | private final ReactApplicationContext reactContext; 36 | private MediaPlayer mediaPlayer; 37 | private float volume; 38 | private AudioManager audioManager; 39 | 40 | public RNSoundPlayerModule(ReactApplicationContext reactContext) { 41 | super(reactContext); 42 | this.reactContext = reactContext; 43 | this.volume = 1.0f; 44 | this.audioManager = (AudioManager) this.reactContext.getSystemService(Context.AUDIO_SERVICE); 45 | reactContext.addLifecycleEventListener(this); 46 | } 47 | 48 | @Override 49 | public String getName() { 50 | return "RNSoundPlayer"; 51 | } 52 | 53 | @ReactMethod 54 | public void setSpeaker(Boolean on) { 55 | audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 56 | audioManager.setSpeakerphoneOn(on); 57 | } 58 | 59 | @Override 60 | public void onHostResume() { 61 | } 62 | 63 | @Override 64 | public void onHostPause() { 65 | } 66 | 67 | @Override 68 | public void onHostDestroy() { 69 | 70 | this.stop(); 71 | if (mediaPlayer != null) { 72 | mediaPlayer.release(); 73 | mediaPlayer = null; 74 | } 75 | } 76 | 77 | @ReactMethod 78 | public void playSoundFile(String name, String type) throws IOException { 79 | mountSoundFile(name, type); 80 | this.resume(); 81 | } 82 | 83 | @ReactMethod 84 | public void loadSoundFile(String name, String type) throws IOException { 85 | mountSoundFile(name, type); 86 | } 87 | 88 | @ReactMethod 89 | public void playUrl(String url) throws IOException { 90 | prepareUrl(url); 91 | this.resume(); 92 | } 93 | 94 | @ReactMethod 95 | public void loadUrl(String url) throws IOException { 96 | prepareUrl(url); 97 | } 98 | 99 | @ReactMethod 100 | public void pause() throws IllegalStateException { 101 | if (this.mediaPlayer != null) { 102 | this.mediaPlayer.pause(); 103 | } 104 | } 105 | 106 | @ReactMethod 107 | public void resume() throws IOException, IllegalStateException { 108 | if (this.mediaPlayer != null) { 109 | this.setVolume(this.volume); 110 | this.mediaPlayer.start(); 111 | } 112 | } 113 | 114 | @ReactMethod 115 | public void stop() throws IllegalStateException { 116 | if (this.mediaPlayer != null) { 117 | this.mediaPlayer.stop(); 118 | } 119 | } 120 | 121 | @ReactMethod 122 | public void seek(float seconds) throws IllegalStateException { 123 | if (this.mediaPlayer != null) { 124 | this.mediaPlayer.seekTo((int) seconds * 1000); 125 | } 126 | } 127 | 128 | @ReactMethod 129 | public void setVolume(float volume) throws IOException { 130 | this.volume = volume; 131 | if (this.mediaPlayer != null) { 132 | this.mediaPlayer.setVolume(volume, volume); 133 | } 134 | } 135 | 136 | @ReactMethod 137 | public void setNumberOfLoops(int noOfLooping){ 138 | // The expected boolean value 139 | Boolean looping; 140 | if (noOfLooping == 0) { 141 | looping = false; 142 | } 143 | else { 144 | looping = true; 145 | } 146 | 147 | if (this.mediaPlayer != null) { 148 | this.mediaPlayer.setLooping(looping); 149 | } 150 | } 151 | 152 | @ReactMethod 153 | public void getInfo( 154 | Promise promise) { 155 | if (this.mediaPlayer == null) { 156 | promise.resolve(null); 157 | return; 158 | } 159 | WritableMap map = Arguments.createMap(); 160 | map.putDouble("currentTime", this.mediaPlayer.getCurrentPosition() / 1000.0); 161 | map.putDouble("duration", this.mediaPlayer.getDuration() / 1000.0); 162 | promise.resolve(map); 163 | } 164 | 165 | @ReactMethod 166 | public void addListener(String eventName) { 167 | // Set up any upstream listeners or background tasks as necessary 168 | } 169 | 170 | @ReactMethod 171 | public void removeListeners(Integer count) { 172 | // Remove upstream listeners, stop unnecessary background tasks 173 | } 174 | 175 | private void sendEvent(ReactApplicationContext reactContext, 176 | String eventName, 177 | @Nullable WritableMap params) { 178 | reactContext 179 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 180 | .emit(eventName, params); 181 | } 182 | 183 | private void mountSoundFile(String name, String type) throws IOException { 184 | try { 185 | Uri uri; 186 | int soundResID = getReactApplicationContext().getResources().getIdentifier(name, "raw", getReactApplicationContext().getPackageName()); 187 | 188 | if (soundResID > 0) { 189 | uri = Uri.parse("android.resource://" + getReactApplicationContext().getPackageName() + "/raw/" + name); 190 | } else { 191 | uri = this.getUriFromFile(name, type); 192 | } 193 | 194 | if (this.mediaPlayer == null) { 195 | this.mediaPlayer = initializeMediaPlayer(uri); 196 | } else { 197 | this.mediaPlayer.reset(); 198 | this.mediaPlayer.setDataSource(getCurrentActivity(), uri); 199 | this.mediaPlayer.prepare(); 200 | } 201 | sendMountFileSuccessEvents(name, type); 202 | } catch (IOException e) { 203 | sendErrorEvent(e); 204 | } 205 | } 206 | 207 | private Uri getUriFromFile(String name, String type) { 208 | String folder = getReactApplicationContext().getFilesDir().getAbsolutePath(); 209 | String file = (!type.isEmpty()) ? name + "." + type : name; 210 | 211 | // http://blog.weston-fl.com/android-mediaplayer-prepare-throws-status0x1-error1-2147483648 212 | // this helps avoid a common error state when mounting the file 213 | File ref = new File(folder + "/" + file); 214 | 215 | if (ref.exists()) { 216 | ref.setReadable(true, false); 217 | } 218 | 219 | return Uri.parse("file://" + folder + "/" + file); 220 | } 221 | 222 | private void prepareUrl(final String url) throws IOException { 223 | try { 224 | if (this.mediaPlayer == null) { 225 | Uri uri = Uri.parse(url); 226 | this.mediaPlayer = initializeMediaPlayer(uri); 227 | this.mediaPlayer.setOnPreparedListener( 228 | new OnPreparedListener() { 229 | @Override 230 | public void onPrepared(MediaPlayer mediaPlayer) { 231 | WritableMap onFinishedLoadingURLParams = Arguments.createMap(); 232 | onFinishedLoadingURLParams.putBoolean("success", true); 233 | onFinishedLoadingURLParams.putString("url", url); 234 | sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING_URL, onFinishedLoadingURLParams); 235 | } 236 | } 237 | ); 238 | } else { 239 | Uri uri = Uri.parse(url); 240 | this.mediaPlayer.reset(); 241 | this.mediaPlayer.setDataSource(getCurrentActivity(), uri); 242 | this.mediaPlayer.prepare(); 243 | } 244 | WritableMap params = Arguments.createMap(); 245 | params.putBoolean("success", true); 246 | sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING, params); 247 | } catch (IOException e) { 248 | WritableMap errorParams = Arguments.createMap(); 249 | errorParams.putString("error", e.getMessage()); 250 | sendEvent(getReactApplicationContext(), EVENT_SETUP_ERROR, errorParams); 251 | } 252 | } 253 | 254 | private MediaPlayer initializeMediaPlayer(Uri uri) throws IOException { 255 | MediaPlayer mediaPlayer = MediaPlayer.create(getCurrentActivity(), uri); 256 | 257 | if (mediaPlayer == null) { 258 | throw new IOException("Failed to initialize MediaPlayer for URI: " + uri.toString()); 259 | } 260 | 261 | mediaPlayer.setOnCompletionListener( 262 | new OnCompletionListener() { 263 | @Override 264 | public void onCompletion(MediaPlayer arg0) { 265 | WritableMap params = Arguments.createMap(); 266 | params.putBoolean("success", true); 267 | sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params); 268 | } 269 | } 270 | ); 271 | 272 | return mediaPlayer; 273 | } 274 | 275 | private void sendMountFileSuccessEvents(String name, String type) { 276 | WritableMap params = Arguments.createMap(); 277 | params.putBoolean("success", true); 278 | sendEvent(reactContext, EVENT_FINISHED_LOADING, params); 279 | 280 | WritableMap onFinishedLoadingFileParams = Arguments.createMap(); 281 | onFinishedLoadingFileParams.putBoolean("success", true); 282 | onFinishedLoadingFileParams.putString("name", name); 283 | onFinishedLoadingFileParams.putString("type", type); 284 | sendEvent(reactContext, EVENT_FINISHED_LOADING_FILE, onFinishedLoadingFileParams); 285 | } 286 | 287 | 288 | private void sendErrorEvent(IOException e) { 289 | WritableMap errorParams = Arguments.createMap(); 290 | errorParams.putString("error", e.getMessage()); 291 | sendEvent(reactContext, EVENT_SETUP_ERROR, errorParams); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /android/src/main/java/com/johnsonsu/rnsoundplayer/RNSoundPlayerPackage.java: -------------------------------------------------------------------------------- 1 | package com.johnsonsu.rnsoundplayer; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.bridge.NativeModule; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.uimanager.ViewManager; 11 | import com.facebook.react.bridge.JavaScriptModule; 12 | 13 | public class RNSoundPlayerPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new RNSoundPlayerModule(reactContext)); 17 | } 18 | 19 | @Override 20 | public List createViewManagers(ReactApplicationContext reactContext) { 21 | return Collections.emptyList(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-native-sound-player" { 2 | import { EmitterSubscription } from "react-native"; 3 | 4 | export type SoundPlayerEvent = 5 | | "OnSetupError" 6 | | "FinishedLoading" 7 | | "FinishedPlaying" 8 | | "FinishedLoadingURL" 9 | | "FinishedLoadingFile"; 10 | 11 | export type SoundPlayerEventData = { 12 | success?: boolean; 13 | url?: string; 14 | name?: string; 15 | type?: string; 16 | }; 17 | 18 | interface SoundPlayerType { 19 | playSoundFile: (name: string, type: string) => void; 20 | playSoundFileWithDelay: (name: string, type: string, delay: number) => void; 21 | loadSoundFile: (name: string, type: string) => void; 22 | playUrl: (url: string) => void; 23 | loadUrl: (url: string) => void; 24 | playAsset: (asset: number) => void; 25 | loadAsset: (asset: number) => void; 26 | /** @deprecated please use addEventListener*/ 27 | onFinishedPlaying: (callback: (success: boolean) => unknown) => void; 28 | /** @deprecated please use addEventListener*/ 29 | onFinishedLoading: (callback: (success: boolean) => unknown) => void; 30 | /** Subscribe to any event. Returns a subscription object. Subscriptions created by this function cannot be removed by calling unmount(). You NEED to call yourSubscriptionObject.remove() when you no longer need this event listener or whenever your component unmounts. */ 31 | addEventListener: ( 32 | eventName: SoundPlayerEvent, 33 | callback: (data: SoundPlayerEventData) => void 34 | ) => EmitterSubscription; 35 | /** Play the loaded sound file. This function is the same as `resume`. */ 36 | play: () => void; 37 | /** Pause the currently playing file. */ 38 | pause: () => void; 39 | /** Resume from pause and continue playing the same file. This function is the same as `play`. */ 40 | resume: () => void; 41 | /** Stop playing, call `playSound` to start playing again. */ 42 | stop: () => void; 43 | /** Seek to seconds of the currently playing file. */ 44 | seek: (seconds: number) => void; 45 | /** Set the volume of the current player. This does not change the volume of the device. */ 46 | setVolume: (volume: number) => void; 47 | /** Only available on iOS. Overwrite default audio output to speaker, which forces playUrl() function to play from speaker. */ 48 | setSpeaker: (on: boolean) => void; 49 | /** Only available on iOS. If you set this option, your audio will be mixed with audio playing in background apps, such as the Music app. */ 50 | setMixAudio: (on: boolean) => void; 51 | /** iOS: 0 means to play the sound once, a positive number specifies the number of times to return to the start and play again, a negative number indicates an indefinite loop. Android: 0 means to play the sound once, other numbers indicate an indefinite loop. */ 52 | setNumberOfLoops: (loops: number) => void; 53 | /** Get the currentTime and duration of the currently mounted audio media. This function returns a promise which resolves to an Object containing currentTime and duration properties. */ 54 | getInfo: () => Promise<{ currentTime: number; duration: number }>; 55 | /** @deprecated Please use addEventListener and remove your own listener by calling yourSubscriptionObject.remove(). */ 56 | unmount: () => void; 57 | } 58 | 59 | const SoundPlayer: SoundPlayerType; 60 | 61 | export default SoundPlayer; 62 | } 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | "use strict"; 5 | 6 | import { NativeModules, NativeEventEmitter, Platform } from "react-native"; 7 | import resolveAsset from 'react-native/Libraries/Image/resolveAssetSource'; 8 | const { RNSoundPlayer } = NativeModules; 9 | 10 | const _soundPlayerEmitter = new NativeEventEmitter(RNSoundPlayer); 11 | let _finishedPlayingListener = null; 12 | let _finishedLoadingListener = null; 13 | 14 | export default { 15 | playSoundFile: (name: string, type: string) => { 16 | RNSoundPlayer.playSoundFile(name, type); 17 | }, 18 | 19 | playSoundFileWithDelay: (name: string, type: string, delay: number) => { 20 | RNSoundPlayer.playSoundFileWithDelay(name, type, delay); 21 | }, 22 | 23 | loadSoundFile: (name: string, type: string) => { 24 | RNSoundPlayer.loadSoundFile(name, type); 25 | }, 26 | 27 | setNumberOfLoops: (loops: number) => { 28 | RNSoundPlayer.setNumberOfLoops(loops); 29 | }, 30 | 31 | playUrl: (url: string) => { 32 | RNSoundPlayer.playUrl(url); 33 | }, 34 | 35 | loadUrl: (url: string) => { 36 | RNSoundPlayer.loadUrl(url); 37 | }, 38 | 39 | playAsset: async (asset: number) => { 40 | if (!(__DEV__) && Platform.OS === "android") { 41 | RNSoundPlayer.playSoundFile(resolveAsset(asset).uri, ''); 42 | } else { 43 | RNSoundPlayer.playUrl(resolveAsset(asset).uri); 44 | } 45 | }, 46 | 47 | loadAsset: (asset: number) => { 48 | if (!(__DEV__) && Platform.OS === "android") { 49 | RNSoundPlayer.loadSoundFile(resolveAsset(asset).uri, ''); 50 | } else { 51 | RNSoundPlayer.loadUrl(resolveAsset(asset).uri); 52 | } 53 | }, 54 | 55 | onFinishedPlaying: (callback: (success: boolean) => any) => { 56 | if (_finishedPlayingListener) { 57 | _finishedPlayingListener.remove(); 58 | _finishedPlayingListener = undefined; 59 | } 60 | 61 | _finishedPlayingListener = _soundPlayerEmitter.addListener( 62 | "FinishedPlaying", 63 | callback 64 | ); 65 | }, 66 | 67 | onFinishedLoading: (callback: (success: boolean) => any) => { 68 | if (_finishedLoadingListener) { 69 | _finishedLoadingListener.remove(); 70 | _finishedLoadingListener = undefined; 71 | } 72 | 73 | _finishedLoadingListener = _soundPlayerEmitter.addListener( 74 | "FinishedLoading", 75 | callback 76 | ); 77 | }, 78 | 79 | addEventListener: ( 80 | eventName: 81 | | "OnSetupError" 82 | | "FinishedLoading" 83 | | "FinishedPlaying" 84 | | "FinishedLoadingURL" 85 | | "FinishedLoadingFile", 86 | callback: Function 87 | ) => _soundPlayerEmitter.addListener(eventName, callback), 88 | 89 | play: () => { 90 | // play and resume has the exact same implementation natively 91 | RNSoundPlayer.resume(); 92 | }, 93 | 94 | pause: () => { 95 | RNSoundPlayer.pause(); 96 | }, 97 | 98 | resume: () => { 99 | RNSoundPlayer.resume(); 100 | }, 101 | 102 | stop: () => { 103 | RNSoundPlayer.stop(); 104 | }, 105 | 106 | seek: (seconds: number) => { 107 | RNSoundPlayer.seek(seconds); 108 | }, 109 | 110 | setVolume: (volume: number) => { 111 | RNSoundPlayer.setVolume(volume); 112 | }, 113 | 114 | setSpeaker: (on: boolean) => { 115 | RNSoundPlayer.setSpeaker(on); 116 | }, 117 | 118 | setMixAudio: (on: boolean) => { 119 | if (Platform.OS === "android") { 120 | console.log("setMixAudio is not implemented on Android"); 121 | } else { 122 | RNSoundPlayer.setMixAudio(on); 123 | } 124 | }, 125 | 126 | getInfo: async () => RNSoundPlayer.getInfo(), 127 | 128 | unmount: () => { 129 | if (_finishedPlayingListener) { 130 | _finishedPlayingListener.remove(); 131 | _finishedPlayingListener = undefined; 132 | } 133 | 134 | if (_finishedLoadingListener) { 135 | _finishedLoadingListener.remove(); 136 | _finishedLoadingListener = undefined; 137 | } 138 | }, 139 | }; 140 | -------------------------------------------------------------------------------- /ios/RNSoundPlayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNSoundPlayer 3 | // 4 | // Created by Johnson Su on 2018-07-10. 5 | // 6 | 7 | #import 8 | #import 9 | #import 10 | 11 | @interface RNSoundPlayer : RCTEventEmitter 12 | @property (nonatomic, strong) AVAudioPlayer *player; 13 | @property (nonatomic, strong) AVPlayer *avPlayer; 14 | @property (nonatomic) int loopCount; 15 | @end 16 | -------------------------------------------------------------------------------- /ios/RNSoundPlayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNSoundPlayer 3 | // 4 | // Created by Johnson Su on 2018-07-10. 5 | // 6 | 7 | #import "RNSoundPlayer.h" 8 | #import 9 | 10 | @implementation RNSoundPlayer 11 | { 12 | bool hasListeners; 13 | } 14 | 15 | static NSString *const EVENT_SETUP_ERROR = @"OnSetupError"; 16 | static NSString *const EVENT_FINISHED_LOADING = @"FinishedLoading"; 17 | static NSString *const EVENT_FINISHED_LOADING_FILE = @"FinishedLoadingFile"; 18 | static NSString *const EVENT_FINISHED_LOADING_URL = @"FinishedLoadingURL"; 19 | static NSString *const EVENT_FINISHED_PLAYING = @"FinishedPlaying"; 20 | 21 | RCT_EXPORT_MODULE(); 22 | 23 | @synthesize bridge = _bridge; 24 | 25 | + (BOOL)requiresMainQueueSetup { 26 | return YES; 27 | } 28 | 29 | - (instancetype)init { 30 | self = [super init]; 31 | if (self) { 32 | self.loopCount = 0; 33 | [[NSNotificationCenter defaultCenter] addObserver:self 34 | selector:@selector(itemDidFinishPlaying:) 35 | name:AVPlayerItemDidPlayToEndTimeNotification 36 | object:nil]; 37 | } 38 | return self; 39 | } 40 | 41 | - (void)dealloc { 42 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 43 | } 44 | 45 | - (NSArray *)supportedEvents { 46 | return @[EVENT_FINISHED_PLAYING, EVENT_FINISHED_LOADING, EVENT_FINISHED_LOADING_URL, EVENT_FINISHED_LOADING_FILE, EVENT_SETUP_ERROR]; 47 | } 48 | 49 | -(void)startObserving { 50 | hasListeners = YES; 51 | } 52 | 53 | -(void)stopObserving { 54 | hasListeners = NO; 55 | } 56 | 57 | RCT_EXPORT_METHOD(playUrl:(NSString *)url) { 58 | [self prepareUrl:url]; 59 | if (self.avPlayer) { 60 | [self.avPlayer play]; 61 | } 62 | } 63 | 64 | RCT_EXPORT_METHOD(loadUrl:(NSString *)url) { 65 | [self prepareUrl:url]; 66 | } 67 | 68 | RCT_EXPORT_METHOD(playSoundFile:(NSString *)name ofType:(NSString *)type) { 69 | [self mountSoundFile:name ofType:type]; 70 | if (self.player) { 71 | [self.player play]; 72 | } 73 | } 74 | 75 | RCT_EXPORT_METHOD(playSoundFileWithDelay:(NSString *)name ofType:(NSString *)type delay:(double)delay) { 76 | [self mountSoundFile:name ofType:type]; 77 | if (self.player) { 78 | [self.player playAtTime:(self.player.deviceCurrentTime + delay)]; 79 | } 80 | } 81 | 82 | RCT_EXPORT_METHOD(loadSoundFile:(NSString *)name ofType:(NSString *)type) { 83 | [self mountSoundFile:name ofType:type]; 84 | } 85 | 86 | RCT_EXPORT_METHOD(pause) { 87 | if (self.player != nil) { 88 | [self.player pause]; 89 | } 90 | if (self.avPlayer != nil) { 91 | [self.avPlayer pause]; 92 | } 93 | } 94 | 95 | RCT_EXPORT_METHOD(resume) { 96 | if (self.player != nil) { 97 | [self.player play]; 98 | } 99 | if (self.avPlayer != nil) { 100 | [self.avPlayer play]; 101 | } 102 | } 103 | 104 | RCT_EXPORT_METHOD(stop) { 105 | if (self.player != nil) { 106 | [self.player stop]; 107 | } 108 | if (self.avPlayer != nil) { 109 | [self.avPlayer pause]; 110 | [self.avPlayer seekToTime:kCMTimeZero]; 111 | } 112 | } 113 | 114 | RCT_EXPORT_METHOD(seek:(float)seconds) { 115 | if (self.player != nil) { 116 | self.player.currentTime = seconds; 117 | } 118 | if (self.avPlayer != nil) { 119 | [self.avPlayer seekToTime:CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC)]; 120 | } 121 | } 122 | 123 | #if !TARGET_OS_TV 124 | RCT_EXPORT_METHOD(setSpeaker:(BOOL)on) { 125 | AVAudioSession *session = [AVAudioSession sharedInstance]; 126 | NSError *error = nil; 127 | if (on) { 128 | [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; 129 | [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]; 130 | } else { 131 | [session setCategory:AVAudioSessionCategoryPlayback error:&error]; 132 | [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]; 133 | } 134 | [session setActive:YES error:&error]; 135 | if (error) { 136 | [self sendErrorEvent:error]; 137 | } 138 | } 139 | #endif 140 | 141 | RCT_EXPORT_METHOD(setMixAudio:(BOOL)on) { 142 | AVAudioSession *session = [AVAudioSession sharedInstance]; 143 | NSError *error = nil; 144 | if (on) { 145 | [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]; 146 | } else { 147 | [session setCategory:AVAudioSessionCategoryPlayback withOptions:0 error:&error]; 148 | } 149 | [session setActive:YES error:&error]; 150 | if (error) { 151 | [self sendErrorEvent:error]; 152 | } 153 | } 154 | 155 | RCT_EXPORT_METHOD(setVolume:(float)volume) { 156 | if (self.player != nil) { 157 | [self.player setVolume:volume]; 158 | } 159 | if (self.avPlayer != nil) { 160 | [self.avPlayer setVolume:volume]; 161 | } 162 | } 163 | 164 | RCT_EXPORT_METHOD(setNumberOfLoops:(NSInteger)loopCount) { 165 | self.loopCount = loopCount; 166 | if (self.player != nil) { 167 | [self.player setNumberOfLoops:loopCount]; 168 | } 169 | } 170 | 171 | RCT_REMAP_METHOD(getInfo, 172 | getInfoWithResolver:(RCTPromiseResolveBlock)resolve 173 | rejecter:(RCTPromiseRejectBlock)reject) { 174 | if (self.player != nil) { 175 | NSDictionary *data = @{ 176 | @"currentTime": [NSNumber numberWithDouble:[self.player currentTime]], 177 | @"duration": [NSNumber numberWithDouble:[self.player duration]] 178 | }; 179 | resolve(data); 180 | } else if (self.avPlayer != nil) { 181 | CMTime currentTime = [[self.avPlayer currentItem] currentTime]; 182 | CMTime duration = [[[self.avPlayer currentItem] asset] duration]; 183 | NSDictionary *data = @{ 184 | @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)], 185 | @"duration": [NSNumber numberWithFloat:CMTimeGetSeconds(duration)] 186 | }; 187 | resolve(data); 188 | } else { 189 | resolve(nil); 190 | } 191 | } 192 | 193 | - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { 194 | if (hasListeners) { 195 | [self sendEventWithName:EVENT_FINISHED_PLAYING body:@{@"success": [NSNumber numberWithBool:flag]}]; 196 | } 197 | } 198 | 199 | - (void)itemDidFinishPlaying:(NSNotification *)notification { 200 | if (hasListeners) { 201 | [self sendEventWithName:EVENT_FINISHED_PLAYING body:@{@"success": [NSNumber numberWithBool:YES]}]; 202 | } 203 | } 204 | 205 | - (void)mountSoundFile:(NSString *)name ofType:(NSString *)type { 206 | if (self.avPlayer) { 207 | self.avPlayer = nil; 208 | } 209 | 210 | NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:name ofType:type]; 211 | 212 | if (soundFilePath == nil) { 213 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 214 | NSString *documentsDirectory = [paths objectAtIndex:0]; 215 | soundFilePath = [NSString stringWithFormat:@"%@.%@", [documentsDirectory stringByAppendingPathComponent:name], type]; 216 | } 217 | 218 | NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath]; 219 | NSError *error = nil; 220 | self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error]; 221 | if (error) { 222 | [self sendErrorEvent:error]; 223 | return; 224 | } 225 | [self.player setDelegate:self]; 226 | [self.player setNumberOfLoops:self.loopCount]; 227 | [self.player prepareToPlay]; 228 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; 229 | if (error) { 230 | [self sendErrorEvent:error]; 231 | return; 232 | } 233 | if (hasListeners) { 234 | [self sendEventWithName:EVENT_FINISHED_LOADING body:@{@"success": [NSNumber numberWithBool:YES]}]; 235 | [self sendEventWithName:EVENT_FINISHED_LOADING_FILE body:@{@"success": [NSNumber numberWithBool:YES], @"name": name, @"type": type}]; 236 | } 237 | } 238 | 239 | - (void)prepareUrl:(NSString *)url { 240 | if (self.player) { 241 | self.player = nil; 242 | } 243 | NSURL *soundURL = [NSURL URLWithString:url]; 244 | self.avPlayer = [[AVPlayer alloc] initWithURL:soundURL]; 245 | [self.avPlayer.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil]; 246 | } 247 | 248 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 249 | if (object == self.avPlayer.currentItem && [keyPath isEqualToString:@"status"] && hasListeners) { 250 | if (self.avPlayer.currentItem.status == AVPlayerItemStatusReadyToPlay) { 251 | [self sendEventWithName:EVENT_FINISHED_LOADING body:@{@"success": [NSNumber numberWithBool:YES]}]; 252 | NSURL *url = [(AVURLAsset *)self.avPlayer.currentItem.asset URL]; 253 | [self sendEventWithName:EVENT_FINISHED_LOADING_URL body:@{@"success": [NSNumber numberWithBool:YES], @"url": [url absoluteString]}]; 254 | } else if (self.avPlayer.currentItem.status == AVPlayerItemStatusFailed) { 255 | [self sendErrorEvent:self.avPlayer.currentItem.error]; 256 | } 257 | } 258 | } 259 | 260 | - (void)sendErrorEvent:(NSError *)error { 261 | if (hasListeners) { 262 | [self sendEventWithName:EVENT_SETUP_ERROR body:@{@"error": [error localizedDescription]}]; 263 | } 264 | } 265 | 266 | @end 267 | -------------------------------------------------------------------------------- /ios/RNSoundPlayer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13BE3DEE1AC21097009241FE /* RNSoundPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* RNSoundPlayer.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libRNSoundPlayer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNSoundPlayer.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 13BE3DEC1AC21097009241FE /* RNSoundPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSoundPlayer.h; sourceTree = ""; }; 28 | 13BE3DED1AC21097009241FE /* RNSoundPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSoundPlayer.m; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 134814211AA4EA7D00B7C361 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 134814201AA4EA6300B7C361 /* libRNSoundPlayer.a */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | 58B511D21A9E6C8500147676 = { 51 | isa = PBXGroup; 52 | children = ( 53 | 13BE3DEC1AC21097009241FE /* RNSoundPlayer.h */, 54 | 13BE3DED1AC21097009241FE /* RNSoundPlayer.m */, 55 | 134814211AA4EA7D00B7C361 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | /* End PBXGroup section */ 60 | 61 | /* Begin PBXNativeTarget section */ 62 | 58B511DA1A9E6C8500147676 /* RNSoundPlayer */ = { 63 | isa = PBXNativeTarget; 64 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNSoundPlayer" */; 65 | buildPhases = ( 66 | 58B511D71A9E6C8500147676 /* Sources */, 67 | 58B511D81A9E6C8500147676 /* Frameworks */, 68 | 58B511D91A9E6C8500147676 /* CopyFiles */, 69 | ); 70 | buildRules = ( 71 | ); 72 | dependencies = ( 73 | ); 74 | name = RNSoundPlayer; 75 | productName = RCTDataManager; 76 | productReference = 134814201AA4EA6300B7C361 /* libRNSoundPlayer.a */; 77 | productType = "com.apple.product-type.library.static"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 58B511D31A9E6C8500147676 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | LastUpgradeCheck = 0610; 86 | ORGANIZATIONNAME = Facebook; 87 | TargetAttributes = { 88 | 58B511DA1A9E6C8500147676 = { 89 | CreatedOnToolsVersion = 6.1.1; 90 | }; 91 | }; 92 | }; 93 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNSoundPlayer" */; 94 | compatibilityVersion = "Xcode 3.2"; 95 | developmentRegion = English; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | ); 100 | mainGroup = 58B511D21A9E6C8500147676; 101 | productRefGroup = 58B511D21A9E6C8500147676; 102 | projectDirPath = ""; 103 | projectRoot = ""; 104 | targets = ( 105 | 58B511DA1A9E6C8500147676 /* RNSoundPlayer */, 106 | ); 107 | }; 108 | /* End PBXProject section */ 109 | 110 | /* Begin PBXSourcesBuildPhase section */ 111 | 58B511D71A9E6C8500147676 /* Sources */ = { 112 | isa = PBXSourcesBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | 13BE3DEE1AC21097009241FE /* RNSoundPlayer.m in Sources */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXSourcesBuildPhase section */ 120 | 121 | /* Begin XCBuildConfiguration section */ 122 | 58B511ED1A9E6C8500147676 /* Debug */ = { 123 | isa = XCBuildConfiguration; 124 | buildSettings = { 125 | ALWAYS_SEARCH_USER_PATHS = NO; 126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 127 | CLANG_CXX_LIBRARY = "libc++"; 128 | CLANG_ENABLE_MODULES = YES; 129 | CLANG_ENABLE_OBJC_ARC = YES; 130 | CLANG_WARN_BOOL_CONVERSION = YES; 131 | CLANG_WARN_CONSTANT_CONVERSION = YES; 132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 133 | CLANG_WARN_EMPTY_BODY = YES; 134 | CLANG_WARN_ENUM_CONVERSION = YES; 135 | CLANG_WARN_INT_CONVERSION = YES; 136 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 137 | CLANG_WARN_UNREACHABLE_CODE = YES; 138 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 139 | COPY_PHASE_STRIP = NO; 140 | ENABLE_STRICT_OBJC_MSGSEND = YES; 141 | GCC_C_LANGUAGE_STANDARD = gnu99; 142 | GCC_DYNAMIC_NO_PIC = NO; 143 | GCC_OPTIMIZATION_LEVEL = 0; 144 | GCC_PREPROCESSOR_DEFINITIONS = ( 145 | "DEBUG=1", 146 | "$(inherited)", 147 | ); 148 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 149 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 150 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 151 | GCC_WARN_UNDECLARED_SELECTOR = YES; 152 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 153 | GCC_WARN_UNUSED_FUNCTION = YES; 154 | GCC_WARN_UNUSED_VARIABLE = YES; 155 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 156 | MTL_ENABLE_DEBUG_INFO = YES; 157 | ONLY_ACTIVE_ARCH = YES; 158 | SDKROOT = iphoneos; 159 | }; 160 | name = Debug; 161 | }; 162 | 58B511EE1A9E6C8500147676 /* Release */ = { 163 | isa = XCBuildConfiguration; 164 | buildSettings = { 165 | ALWAYS_SEARCH_USER_PATHS = NO; 166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 167 | CLANG_CXX_LIBRARY = "libc++"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_WARN_BOOL_CONVERSION = YES; 171 | CLANG_WARN_CONSTANT_CONVERSION = YES; 172 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 173 | CLANG_WARN_EMPTY_BODY = YES; 174 | CLANG_WARN_ENUM_CONVERSION = YES; 175 | CLANG_WARN_INT_CONVERSION = YES; 176 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 177 | CLANG_WARN_UNREACHABLE_CODE = YES; 178 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 179 | COPY_PHASE_STRIP = YES; 180 | ENABLE_NS_ASSERTIONS = NO; 181 | ENABLE_STRICT_OBJC_MSGSEND = YES; 182 | GCC_C_LANGUAGE_STANDARD = gnu99; 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 190 | MTL_ENABLE_DEBUG_INFO = NO; 191 | SDKROOT = iphoneos; 192 | VALIDATE_PRODUCT = YES; 193 | }; 194 | name = Release; 195 | }; 196 | 58B511F01A9E6C8500147676 /* Debug */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | HEADER_SEARCH_PATHS = ( 200 | "$(inherited)", 201 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 202 | "$(SRCROOT)/../../React/**", 203 | "$(SRCROOT)/../../node_modules/react-native/React/**", 204 | ); 205 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 206 | OTHER_LDFLAGS = "-ObjC"; 207 | PRODUCT_NAME = RNSoundPlayer; 208 | SKIP_INSTALL = YES; 209 | }; 210 | name = Debug; 211 | }; 212 | 58B511F11A9E6C8500147676 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | HEADER_SEARCH_PATHS = ( 216 | "$(inherited)", 217 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 218 | "$(SRCROOT)/../../React/**", 219 | ); 220 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 221 | OTHER_LDFLAGS = "-ObjC"; 222 | PRODUCT_NAME = RNSoundPlayer; 223 | SKIP_INSTALL = YES; 224 | }; 225 | name = Release; 226 | }; 227 | /* End XCBuildConfiguration section */ 228 | 229 | /* Begin XCConfigurationList section */ 230 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNSoundPlayer" */ = { 231 | isa = XCConfigurationList; 232 | buildConfigurations = ( 233 | 58B511ED1A9E6C8500147676 /* Debug */, 234 | 58B511EE1A9E6C8500147676 /* Release */, 235 | ); 236 | defaultConfigurationIsVisible = 0; 237 | defaultConfigurationName = Release; 238 | }; 239 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNSoundPlayer" */ = { 240 | isa = XCConfigurationList; 241 | buildConfigurations = ( 242 | 58B511F01A9E6C8500147676 /* Debug */, 243 | 58B511F11A9E6C8500147676 /* Release */, 244 | ); 245 | defaultConfigurationIsVisible = 0; 246 | defaultConfigurationName = Release; 247 | }; 248 | /* End XCConfigurationList section */ 249 | }; 250 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 251 | } 252 | -------------------------------------------------------------------------------- /ios/RNSoundPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-sound-player", 3 | "version": "0.14.5", 4 | "description": "Play or stream audio files in ReactNative on iOS/Android", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "keywords": [ 8 | "reactnative", 9 | "react-native", 10 | "sound", 11 | "player", 12 | "audio", 13 | "streaming" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/johnsonsu/react-native-sound-player.git" 18 | }, 19 | "author": { 20 | "name": "Johnson Su", 21 | "email": "johnsonsu@johnsonsu.com" 22 | }, 23 | "prettier": { 24 | "trailingComma": "es5", 25 | "tabWidth": 2, 26 | "semi": true, 27 | "singleQuote": false 28 | }, 29 | "license": "MIT" 30 | } 31 | --------------------------------------------------------------------------------