├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .watchmanconfig ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.6.1.cjs ├── .yarnrc.yml ├── AudioManager.podspec ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── android ├── CMakeLists.txt ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── AndroidManifestNew.xml │ ├── cpp │ └── cpp-adapter.cpp │ └── java │ └── com │ ├── audiomanager │ └── AudioManagerPackage.kt │ └── margelo │ └── nitro │ └── audiomanager │ └── AudioManager.kt ├── babel.config.js ├── banner.jpg ├── eslint.config.mjs ├── example ├── .bundle │ └── config ├── .watchmanconfig ├── Gemfile ├── Gemfile.lock ├── README.md ├── app.json ├── babel.config.js ├── index.js ├── jest.config.js ├── metro.config.js ├── package.json ├── react-native.config.js ├── src │ ├── App.tsx │ ├── IOSSessionPicker.tsx │ ├── iosCombinations.ts │ └── utils.ts └── tsconfig.json ├── ios ├── AudioManager.swift └── HiddenVolumeView.swift ├── lefthook.yml ├── nitro.json ├── package.json ├── src ├── AudioManager.nitro.ts ├── __tests__ │ └── index.test.tsx ├── functions.tsx ├── hooks.tsx ├── index.tsx └── types.ts ├── tsconfig.build.json ├── tsconfig.json ├── turbo.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Report a reproducible bug or regression in this library. 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # Bug report 9 | 10 | 👋 Hi! 11 | 12 | **Please fill the following carefully before opening a new issue ❗** 13 | *(Your issue may be closed if it doesn't provide the required pieces of information)* 14 | - type: checkboxes 15 | attributes: 16 | label: Before submitting a new issue 17 | description: Please perform simple checks first. 18 | options: 19 | - label: I tested using the latest version of the library, as the bug might be already fixed. 20 | required: true 21 | - label: I tested using a [supported version](https://github.com/reactwg/react-native-releases/blob/main/docs/support.md) of react native. 22 | required: true 23 | - label: I checked for possible duplicate issues, with possible answers. 24 | required: true 25 | - type: textarea 26 | id: summary 27 | attributes: 28 | label: Bug summary 29 | description: | 30 | Provide a clear and concise description of what the bug is. 31 | If needed, you can also provide other samples: error messages / stack traces, screenshots, gifs, etc. 32 | validations: 33 | required: true 34 | - type: input 35 | id: library-version 36 | attributes: 37 | label: Library version 38 | description: What version of the library are you using? 39 | placeholder: "x.x.x" 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: react-native-info 44 | attributes: 45 | label: Environment info 46 | description: Run `react-native info` in your terminal and paste the results here. 47 | render: shell 48 | validations: 49 | required: true 50 | - type: textarea 51 | id: steps-to-reproduce 52 | attributes: 53 | label: Steps to reproduce 54 | description: | 55 | You must provide a clear list of steps and code to reproduce the problem. 56 | value: | 57 | 1. … 58 | 2. … 59 | validations: 60 | required: true 61 | - type: input 62 | id: reproducible-example 63 | attributes: 64 | label: Reproducible example repository 65 | description: Please provide a link to a repository on GitHub with a reproducible example. 66 | validations: 67 | required: true 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 💡 4 | url: https://github.com/ChristopherGabba/react-native-nitro-audio-manager/discussions/new?category=ideas 5 | about: If you have a feature request, please create a new discussion on GitHub. 6 | - name: Discussions on GitHub 💬 7 | url: https://github.com/ChristopherGabba/react-native-nitro-audio-manager/discussions 8 | about: If this library works as promised but you need help, please ask questions there. 9 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v4 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Restore dependencies 13 | id: yarn-cache 14 | uses: actions/cache/restore@v4 15 | with: 16 | path: | 17 | **/node_modules 18 | .yarn/install-state.gz 19 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} 20 | restore-keys: | 21 | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 22 | ${{ runner.os }}-yarn- 23 | 24 | - name: Install dependencies 25 | if: steps.yarn-cache.outputs.cache-hit != 'true' 26 | run: yarn install --immutable 27 | shell: bash 28 | 29 | - name: Cache dependencies 30 | if: steps.yarn-cache.outputs.cache-hit != 'true' 31 | uses: actions/cache/save@v4 32 | with: 33 | path: | 34 | **/node_modules 35 | .yarn/install-state.gz 36 | key: ${{ steps.yarn-cache.outputs.cache-primary-key }} 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | merge_group: 10 | types: 11 | - checks_requested 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup 21 | uses: ./.github/actions/setup 22 | 23 | - name: Lint files 24 | run: yarn lint 25 | 26 | - name: Typecheck files 27 | run: yarn typecheck 28 | 29 | test: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup 36 | uses: ./.github/actions/setup 37 | 38 | - name: Run unit tests 39 | run: yarn test --maxWorkers=2 --coverage 40 | 41 | build-library: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Setup 48 | uses: ./.github/actions/setup 49 | 50 | - name: Build package 51 | run: yarn prepare 52 | 53 | build-android: 54 | runs-on: ubuntu-latest 55 | env: 56 | TURBO_CACHE_DIR: .turbo/android 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | 61 | - name: Setup 62 | uses: ./.github/actions/setup 63 | 64 | - name: Generate nitrogen code 65 | run: yarn nitrogen 66 | 67 | - name: Cache turborepo for Android 68 | uses: actions/cache@v4 69 | with: 70 | path: ${{ env.TURBO_CACHE_DIR }} 71 | key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} 72 | restore-keys: | 73 | ${{ runner.os }}-turborepo-android- 74 | 75 | - name: Check turborepo cache for Android 76 | run: | 77 | TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") 78 | 79 | if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then 80 | echo "turbo_cache_hit=1" >> $GITHUB_ENV 81 | fi 82 | 83 | - name: Install JDK 84 | if: env.turbo_cache_hit != 1 85 | uses: actions/setup-java@v4 86 | with: 87 | distribution: 'zulu' 88 | java-version: '17' 89 | 90 | - name: Finalize Android SDK 91 | if: env.turbo_cache_hit != 1 92 | run: | 93 | /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" 94 | 95 | - name: Cache Gradle 96 | if: env.turbo_cache_hit != 1 97 | uses: actions/cache@v4 98 | with: 99 | path: | 100 | ~/.gradle/wrapper 101 | ~/.gradle/caches 102 | key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} 103 | restore-keys: | 104 | ${{ runner.os }}-gradle- 105 | 106 | - name: Build example for Android 107 | env: 108 | JAVA_OPTS: "-XX:MaxHeapSize=6g" 109 | run: | 110 | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" 111 | 112 | build-ios: 113 | runs-on: macos-latest 114 | env: 115 | TURBO_CACHE_DIR: .turbo/ios 116 | steps: 117 | - name: Checkout 118 | uses: actions/checkout@v4 119 | 120 | - name: Setup 121 | uses: ./.github/actions/setup 122 | 123 | - name: Generate nitrogen code 124 | run: yarn nitrogen 125 | 126 | - name: Cache turborepo for iOS 127 | uses: actions/cache@v4 128 | with: 129 | path: ${{ env.TURBO_CACHE_DIR }} 130 | key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} 131 | restore-keys: | 132 | ${{ runner.os }}-turborepo-ios- 133 | 134 | - name: Check turborepo cache for iOS 135 | run: | 136 | TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") 137 | 138 | if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then 139 | echo "turbo_cache_hit=1" >> $GITHUB_ENV 140 | fi 141 | 142 | - name: Restore cocoapods 143 | if: env.turbo_cache_hit != 1 144 | id: cocoapods-cache 145 | uses: actions/cache/restore@v4 146 | with: 147 | path: | 148 | **/ios/Pods 149 | key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} 150 | restore-keys: | 151 | ${{ runner.os }}-cocoapods- 152 | 153 | - name: Install cocoapods 154 | if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' 155 | run: | 156 | cd example/ios 157 | pod install 158 | env: 159 | NO_FLIPPER: 1 160 | 161 | - name: Cache cocoapods 162 | if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' 163 | uses: actions/cache/save@v4 164 | with: 165 | path: | 166 | **/ios/Pods 167 | key: ${{ steps.cocoapods-cache.outputs.cache-key }} 168 | 169 | - name: Build example for iOS 170 | run: | 171 | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | **/.xcode.env.local 32 | 33 | # Android/IJ 34 | # 35 | .classpath 36 | .cxx 37 | .gradle 38 | .idea 39 | .project 40 | .settings 41 | local.properties 42 | android.iml 43 | 44 | # Cocoapods 45 | # 46 | # Ignore example platform builds 47 | /example/ios/ 48 | /example/android/ 49 | 50 | # Ruby 51 | example/vendor/ 52 | 53 | # node.js 54 | # 55 | node_modules/ 56 | npm-debug.log 57 | yarn-debug.log 58 | yarn-error.log 59 | 60 | # BUCK 61 | buck-out/ 62 | \.buckd/ 63 | android/app/libs 64 | android/keystores/debug.keystore 65 | 66 | # Yarn 67 | .yarn/* 68 | !.yarn/patches 69 | !.yarn/plugins 70 | !.yarn/releases 71 | !.yarn/sdks 72 | !.yarn/versions 73 | 74 | # Expo 75 | .expo/ 76 | 77 | # Turborepo 78 | .turbo/ 79 | 80 | # generated by bob 81 | lib/ 82 | 83 | # React Native Codegen 84 | ios/generated 85 | android/generated 86 | 87 | # React Native Nitro Modules 88 | nitrogen/ 89 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.19.0 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | plugins: 5 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 6 | spec: "@yarnpkg/plugin-interactive-tools" 7 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 8 | spec: "@yarnpkg/plugin-workspace-tools" 9 | 10 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 11 | -------------------------------------------------------------------------------- /AudioManager.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "AudioManager" 7 | s.version = package["version"] 8 | s.summary = package["description"] 9 | s.homepage = package["homepage"] 10 | s.license = package["license"] 11 | s.authors = package["author"] 12 | 13 | s.platforms = { :ios => min_ios_version_supported } 14 | s.source = { :git => "https://github.com/ChristopherGabba/react-native-nitro-audio-manager.git", :tag => "#{s.version}" } 15 | 16 | s.source_files = "ios/**/*.{h,m,mm,swift}" 17 | 18 | load 'nitrogen/generated/ios/AudioManager+autolinking.rb' 19 | add_nitrogen_files(s) 20 | 21 | # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. 22 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. 23 | if respond_to?(:install_modules_dependencies, true) 24 | install_modules_dependencies(s) 25 | else 26 | s.dependency "React-Core" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages: 10 | 11 | - The library package in the root directory. 12 | - An example app in the `example/` directory. 13 | 14 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 15 | 16 | ```sh 17 | yarn 18 | ``` 19 | 20 | > Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development. 21 | 22 | This project uses Nitro Modules. If you're not familiar with how Nitro works, make sure to check the [Nitro Modules Docs](https://nitro.margelo.com/). 23 | 24 | You need to run [Nitrogen](https://nitro.margelo.com/docs/nitrogen) to generate the boilerplate code required for this project. The example app will not build without this step. 25 | 26 | Run **Nitrogen** in following cases: 27 | - When you make changes to any `*.nitro.ts` files. 28 | - When running the project for the first time (since the generated files are not committed to the repository). 29 | 30 | To invoke **Nitrogen**, use the following command: 31 | 32 | ```sh 33 | yarn nitrogen 34 | ``` 35 | 36 | The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. 37 | 38 | It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. 39 | 40 | If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/AudioManagerExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-nitro-audio-manager`. 41 | 42 | To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-nitro-audio-manager` under `Android`. 43 | 44 | You can use various commands from the root directory to work with the project. 45 | 46 | To start the packager: 47 | 48 | ```sh 49 | yarn example start 50 | ``` 51 | 52 | To run the example app on Android: 53 | 54 | ```sh 55 | yarn example android 56 | ``` 57 | 58 | To run the example app on iOS: 59 | 60 | ```sh 61 | yarn example ios 62 | ``` 63 | 64 | To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this: 65 | 66 | ```sh 67 | Running "AudioManagerExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1} 68 | ``` 69 | 70 | Note the `"fabric":true` and `"concurrentRoot":true` properties. 71 | 72 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 73 | 74 | ```sh 75 | yarn typecheck 76 | yarn lint 77 | ``` 78 | 79 | To fix formatting errors, run the following: 80 | 81 | ```sh 82 | yarn lint --fix 83 | ``` 84 | 85 | Remember to add tests for your change if possible. Run the unit tests by: 86 | 87 | ```sh 88 | yarn test 89 | ``` 90 | 91 | ### Commit message convention 92 | 93 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 94 | 95 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 96 | - `feat`: new features, e.g. add new method to the module. 97 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 98 | - `docs`: changes into documentation, e.g. add usage example for the module.. 99 | - `test`: adding or updating tests, e.g. add integration tests using detox. 100 | - `chore`: tooling changes, e.g. change CI config. 101 | 102 | Our pre-commit hooks verify that your commit message matches this format when committing. 103 | 104 | ### Linting and tests 105 | 106 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 107 | 108 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 109 | 110 | Our pre-commit hooks verify that the linter and tests pass when committing. 111 | 112 | ### Publishing to npm 113 | 114 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 115 | 116 | To publish new versions, run the following: 117 | 118 | ```sh 119 | yarn release 120 | ``` 121 | 122 | ### Scripts 123 | 124 | The `package.json` file contains various scripts for common tasks: 125 | 126 | - `yarn`: setup project by installing dependencies. 127 | - `yarn typecheck`: type-check files with TypeScript. 128 | - `yarn lint`: lint files with ESLint. 129 | - `yarn test`: run unit tests with Jest. 130 | - `yarn example start`: start the Metro server for the example app. 131 | - `yarn example android`: run the example app on Android. 132 | - `yarn example ios`: run the example app on iOS. 133 | 134 | ### Sending a pull request 135 | 136 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 137 | 138 | When you're sending a pull request: 139 | 140 | - Prefer small pull requests focused on one change. 141 | - Verify that linters and tests are passing. 142 | - Review the documentation to make sure it looks good. 143 | - Follow the pull request template when opening a pull request. 144 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ChristopherGabba 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![React Native Audio Manager](./banner.jpg) 2 | 3 | # react-native-nitro-audio-manager 4 | 5 | > ⚠️ This package is currently in alpha and under active development. Please report any issues that you run across on either platform. 6 | 7 | A React Native library powered by [NitroModules](https://reactnative.dev/docs/native-modules-nitro) that gives access to [`AVAudioSession`](https://developer.apple.com/documentation/avfaudio/avaudiosession) on iOS and [`AudioManager`](https://developer.android.com/reference/android/media/AudioManager) on Android. This library is designed to give more granular control of audio sessions than what is typically needed in packages like: 8 | 9 | - [expo-video](https://docs.expo.dev/versions/latest/sdk/video/) 10 | - [expo-camera](https://docs.expo.dev/versions/latest/sdk/camera/) 11 | - [react-native-video](https://github.com/react-native-video/react-native-video) 12 | - [react-native-vision-camera](https://github.com/mrousavy/react-native-vision-camera) 13 | - etc. 14 | 15 | > ⚠️ When using this library alongside those packages, you'll often need to **disable or override their built-in audio session handling**, to avoid conflicts. [react-native-video](https://github.com/react-native-video/react-native-video) recently announced they will be supporting an audio session disabled [prop](https://docs.thewidlarzgroup.com/react-native-video/component/props#disableaudiosessionmanagement). 16 | 17 | ## Features 18 | - Configure audio sessions (category, mode, options, etc.) 19 | - Configure audio manager preferences 20 | - Activate and deactivate sessions / audio focus 21 | - Get device inputs/outputs (headphones, etc.) 22 | - Get and set system volume 23 | - Listen for system volume changes 24 | - Listen for input/output changes 25 | - Listen for audio interruption / category changes events 26 | 27 | ## Installation 28 | 29 | Using npm: 30 | 31 | ```sh 32 | npm install react-native-nitro-audio-manager react-native-nitro-modules 33 | ``` 34 | 35 | Using yarn: 36 | 37 | ```sh 38 | yarn add react-native-nitro-audio-manager react-native-nitro-modules 39 | ``` 40 | 41 | # Documentation 42 | 43 | ## Table of Contents 44 | 45 | - [Volume Control](#volume-control) 46 | - [`getSystemVolume()`](#getsystemvolumepromisenumber) 47 | - [`setSystemVolume()`](#setsystemvolumevalue-options-promisevoid) 48 | - [`useVolume()`](#usevolumenumber) 49 | - [Listeners](#listeners) 50 | - [`addListener()`](#addlistenertype-listenercallback--void) 51 | - [Inputs / Outputs](#inputs--outputs) 52 | - [`getOutputLatency()`](#getoutputlatency-number) 53 | - [`getInputLatency()`](#getinputlatency-number) 54 | - [`getCurrentInputRoutes()`](#getcurrentinputroutes-portdescription) 55 | - [`getCurrentOutputRoutes()`](#getcurrentoutputroutes-portdescription) 56 | - [`getCategoryCompatibleInputs()`](#getcategorycompatibleinputs-portdescription--undefined) 57 | - [`forceOutputToSpeaker()`](#forceoutputtospeaker-void) 58 | - [`cancelForcedOutputToSpeaker()`](#cancelforcedoutputtospeaker-void) 59 | - [AudioSession / AudioFocus Management](#audiosession-ios--audiofocus-android-management) 60 | - [`configureAudio()`](#configureaudioparams-void) 61 | - [`getAudioSessionStatus()`](#getaudiosessionstatus-audiosessionstatus--audiomanagerstatus--undefined) 62 | - [`activate()`](#activateoptions-promisevoid) 63 | - [`deactivate()`](#deactivateoptions-promisevoid) 64 | 65 | 66 | ## Volume Control 67 | 68 | ### `getSystemVolume(): Promise` 69 | 70 | Returns a promise with the current system volume: 71 | 72 | - **iOS:** A single number in the range `[0–1]`. 73 | - **Android:** The music stream volume in the range `[0–1]` (Android has mutliple streams). 74 | 75 | #### Example 76 | ```ts 77 | import { getSystemVolume } from 'react-native-nitro-audio-manager'; 78 | 79 | const volume = await getSystemVolume() 80 | console.log(volume) // ex. 1.0 81 | ``` 82 | 83 | ### `setSystemVolume(value, options): Promise` 84 | 85 | Sets the system between 0 and 1. 86 | 87 | - **value**: A number in the range [0–1]. On Android, there is a slight rounding inaccuracy. For example, `setVolume(0.5)` may set the volume to 0.53. 88 | - **options.showUI**: (Android Only) If true, shows the system volume UI. On iOS, the system UI always appears when volume changes. *Default: true*. 89 | 90 | #### Example 91 | 92 | ```ts 93 | import { setSystemVolume } from 'react-native-nitro-audio-manager'; 94 | 95 | await setSystemVolume(0.5, { showUI: true }) 96 | ``` 97 | 98 | ### `useVolume(): number ` 99 | 100 | A hook that gives live feedback of the volume (listens for volume changes). 101 | 102 | #### Example 103 | 104 | ```tsx 105 | import React from 'react'; 106 | import { View, Text, StyleSheet } from 'react-native'; 107 | import { useVolume } from 'react-native-nitro-audio-manager'; 108 | 109 | export default function VolumeDisplay() { 110 | 111 | const volume = useVolume(); // 0.0 to 1.0 112 | 113 | return ( 114 | 115 | Current Volume: 116 | {(volume * 100).toFixed(0)}% 117 | 118 | ); 119 | } 120 | ``` 121 | 122 | ## Listeners 123 | 124 | ### `addListener(type, listenerCallback): () => void` 125 | 126 | Adds a type-safe system event listener for audio session changes. 127 | 128 | - **type**: `'audioInterruption' | 'routeChange' | 'volume'` 129 | - **listenerCallback**: A callback function receiving event-specific data depending on the type. 130 | 131 | Returns an unsubscribe function that removes the listener. 132 | 133 | --- 134 | 135 | #### Listener Types 136 | 137 | | Type | Event Payload | Description | 138 | |:--------------------|:-------------------------------------|:-------------------------------------| 139 | | `'audioInterruption'`| `InterruptionEvent` | Fired when an audio session is interrupted or resumed (e.g., incoming phone call, app backgrounding). | 140 | | `'routeChange'` | `RouteChangeEvent` | Fired when audio output devices change (e.g., Bluetooth connected/disconnected, headphones plugged in). | 141 | | `'volume'` | `number` (normalized between `0.0–1.0`) | Fired when the system volume changes. | 142 | 143 | --- 144 | 145 | #### Example 146 | 147 | ```tsx 148 | import { useEffect } from 'react' 149 | import { addListener } from 'react-native-nitro-audio-manager'; 150 | 151 | useEffect(() => { 152 | 153 | // Listen for audio interruptions (incomming phone calls, category changes on ios, etc.) 154 | const removeInterruptionListener = addListener('audioInterruption', (event) => { 155 | console.log('Audio interruption event:', event); 156 | }); 157 | 158 | // Listen for route changes (headphones connecting, etc.) 159 | const removeRouteChangeListener = addListener('routeChange', (event) => { 160 | console.log('Route changed:', event); 161 | }); 162 | 163 | // Listen for volume changes 164 | const removeVolumeListener = addListener('volume', (volume) => { 165 | console.log('Volume changed:', volume); 166 | }); 167 | 168 | return () => { 169 | removeInterruptionListener(); 170 | removeRouteChangeListener(); 171 | removeVolumeListener(); 172 | } 173 | },[]) 174 | ``` 175 | 176 | ## Inputs / Outputs 177 | 178 | ### `getOutputLatency(): number` 179 | 180 | Returns the current **output latency** in **seconds**. 181 | 182 | - **Android:** Output latency is calculated as `buffer-size / sample-rate`. 183 | - **IOS:** Output latency comes from [AVAudioSession.outputLatency](https://developer.apple.com/documentation/avfaudio/avaudiosession/outputlatency). 184 | 185 | #### Example 186 | 187 | ```ts 188 | import { getOutputLatency } from 'react-native-nitro-audio-manager'; 189 | 190 | const latency = getOutputLatency(); 191 | console.log(`Output Latency: ${latency * 1000} ms`); 192 | 193 | ``` 194 | 195 | ### `getInputLatency(): number` 196 | 197 | Returns the current **input Latency** in **seconds**. 198 | 199 | - **Android S+ only:** Calculated using INPUT_FRAMES_PER_BUFFER and sample rate. 200 | - **older Android:** Not available — returns -1.0. 201 | - **IOS**: Input latency comes from [`AVAudioSession.inputLatency`](https://developer.apple.com/documentation/avfaudio/avaudiosession/inputlatency). 202 | 203 | #### Example 204 | 205 | ```ts 206 | import { getInputLatency } from 'react-native-nitro-audio-manager'; 207 | 208 | const latency = getInputLatency(); 209 | console.log(`Input Latency: ${latency * 1000} ms`); 210 | ``` 211 | 212 | ### `getCurrentInputRoutes(): PortDescription[]` 213 | 214 | Returns an array of the currently connected **input routes**. 215 | 216 | - **iOS & Android:** Provides a list of active input devices, such as built-in mic, headset mic, or Bluetooth SCO. 217 | - Always returns an array, even if empty. 218 | 219 | #### Example 220 | 221 | ```ts 222 | import { getCurrentInputRoutes } from 'react-native-nitro-audio-manager'; 223 | 224 | const inputs = getCurrentInputRoutes(); 225 | console.log('Current Input Routes:', inputs); 226 | ``` 227 | 228 | ### `getCurrentOutputRoutes(): PortDescription[]` 229 | 230 | Returns an array of the currently connected **output routes**. 231 | 232 | - **iOS & Android:** Provides a list of active output devices, such as built-in speaker, Bluetooth headphones, or wired headset. 233 | - Always returns an array, even if empty. 234 | 235 | Example: 236 | 237 | ```ts 238 | import { getCurrentOutputRoutes } from 'react-native-nitro-audio-manager'; 239 | 240 | const outputs = getCurrentOutputRoutes(); 241 | console.log('Current Output Routes:', outputs); 242 | ``` 243 | 244 | ### `getCategoryCompatibleInputs(): PortDescription[] | undefined` 245 | 246 | Returns an array of **available input ports** that are compatible with the currently active audio session category and mode. 247 | 248 | > **@platform iOS only** 249 | 250 | - If the session category is `playAndRecord`, the returned array may contain: 251 | - Built-in microphone 252 | - Headset microphone (if connected) 253 | - If the session category is `playback`, this function returns an **empty array**. 254 | - On **Android**, this function returns `undefined`. 255 | 256 | #### Example 257 | 258 | ```ts 259 | import { getCategoryCompatibleInputs } from 'react-native-nitro-audio-manager'; 260 | 261 | const inputs = getCategoryCompatibleInputs(); 262 | if (inputs) { 263 | console.log('Compatible input ports:', inputs); 264 | } else { 265 | console.log('No inputs supported with category'); 266 | } 267 | ``` 268 | 269 | ### `forceOutputToSpeaker(): void` 270 | 271 | Temporarily forces the audio output to use the built-in **speaker**, overriding the current route. 272 | 273 | > **@platform iOS only** 274 | 275 | This change remains active until: 276 | - The audio route changes automatically, or 277 | - You call `cancelForcedOutputToSpeaker()` manually. 278 | 279 | - If you want to **permanently** prefer the speaker, you should set the `defaultToSpeaker` option when configuring your audio session category. 280 | 281 | 282 | ### `cancelForcedOutputToSpeaker(): void` 283 | 284 | > **@platform iOS only** 285 | 286 | Cancels the temporary forced routing to the speaker caused by `forceOutputToSpeaker()`. 287 | 288 | #### Example 289 | 290 | ```ts 291 | import { cancelForcedOutputToSpeaker } from 'react-native-nitro-audio-manager'; 292 | 293 | cancelForcedOutputToSpeaker(); 294 | ``` 295 | 296 | ## AudioSession (IOS) / AudioFocus (Android) Management 297 | 298 | On iOS, configuring audio sessions is done manually via configuring categories, modes, options, and preferences. 299 | 300 | Newer android phones / later SDK versions (version 33+) handle Android focus automatically within the phone so applying some of these settings may not function on later systems. 301 | 302 | Because of the uniqueness required for each platform and the occasional need to just set one platform (the other may work naturally), a decision was made to allow flexibility with configuring, activating, and deactivating for each platform. 303 | 304 | ### `configureAudio(params): void` 305 | 306 | Configures a very type-safe **platform-specific audio session**. 307 | 308 | - **iOS:** Configures the AVAudioSession with the params provided.. 309 | - **Android:** Configures the AudioManager with the params provided. 310 | 311 | *NOTE:* If one is not provided, it ignores that platform. 312 | 313 | --- 314 | 315 | #### Parameters 316 | 317 | ```ts 318 | type ConfigureAudioAndActivateParams = { 319 | ios?: { 320 | category: AudioSessionCategory; 321 | mode?: AudioSessionMode; 322 | policy?: AudioSessionRouteSharingPolicy; 323 | categoryOptions?: AudioSessionCategoryOption[]; 324 | prefersNoInterruptionFromSystemAlerts?: boolean; 325 | prefersInterruptionOnRouteDisconnect?: boolean; 326 | allowHapticsAndSystemSoundsDuringRecording?: boolean; 327 | prefersEchoCancelledInput?: boolean; 328 | }; 329 | android?: { 330 | focusGain: AudioFocusGainType; 331 | usage: AudioUsage; 332 | contentType: AudioContentType; 333 | willPauseWhenDucked: boolean; 334 | acceptsDelayedFocusGain: boolean; 335 | }; 336 | }; 337 | ``` 338 | 339 | --- 340 | 341 | #### Example 342 | 343 | ```ts 344 | import { 345 | configureAudio, 346 | AudioSessionCategory, 347 | AudioSessionMode, 348 | AudioSessionCategoryOptions, 349 | AudioContentTypes, 350 | AudioFocusGainTypes, 351 | AudioUsages 352 | } from 'react-native-nitro-audio-manager'; 353 | 354 | await configureAudio({ 355 | ios: { 356 | category: AudioSessionCategory.PlayAndRecord, 357 | mode: AudioSessionMode.VideoRecording, 358 | categoryOptions: [ 359 | AudioSessionCategoryOptions.MixWithOthers, 360 | AudioSessionCategoryOptions.AllowBluetooth 361 | ], 362 | prefersNoInterruptionFromSystemAlerts: true, 363 | prefersInterruptionOnRouteDisconnect: true, 364 | allowHapticsAndSystemSoundsDuringRecording: true, 365 | prefersEchoCancelledInput: true, // iOS 18.2+ 366 | }, 367 | android: { 368 | focusGain: AudioFocusGainTypes.GainTransientMayDuck, 369 | contentType: AudioContentTypes.Music, 370 | usage: AudioUsages.Media, 371 | willPauseWhenDucked: true, 372 | acceptsDelayedFocusGain: true, 373 | }, 374 | }); 375 | ``` 376 | 377 | ### `getAudioSessionStatus(): AudioSessionStatus | AudioManagerStatus | undefined` 378 | 379 | Retrieves the current audio session or audio manager configuration, depending on the platform. 380 | 381 | - **iOS:** Returns an `AudioSessionStatus` object containing details like category, mode, options, and audio routing preferences. 382 | - **Android:** Returns an `AudioManagerStatus` object containing details like focus gain, usage, content type, and ringer mode. 383 | 384 | #### Example 385 | 386 | ```ts 387 | import { Platform } from 'react-native' 388 | import { getAudioStatus, AudioSessionStatus } from 'react-native-nitro-audio-manager'; 389 | 390 | if(Platform.OS === "ios") { 391 | const status = getAudioStatus() as AudioSessionStatus; 392 | console.log('Audio Session Status:', status.category); 393 | } 394 | 395 | ``` 396 | 397 | #### AudioManagerStatus (Android Only) 398 | 399 | | Property | Description | 400 | |:--------------------------|:------------| 401 | | `mode` | Current audio mode (e.g., `NORMAL`, `IN_COMMUNICATION`). | 402 | | `ringerMode` | Current ringer state (`NORMAL`, `VIBRATE`, or `SILENT`). | 403 | | `focusGain` | Requested focus gain type (e.g., `GAIN`, `GAIN_TRANSIENT`). | 404 | | `usage` | Type of audio usage (e.g., `MEDIA`, `GAME`, `VOICE_COMMUNICATION`). | 405 | | `contentType` | Type of content (e.g., `MUSIC`, `MOVIE`, `SONIFICATION`). | 406 | | `willPauseWhenDucked` | Whether playback pauses automatically when ducked. | 407 | | `acceptsDelayedFocusGain` | Whether delayed audio focus gain is accepted. | 408 | 409 | --- 410 | 411 | #### AudioSessionStatus (iOS Only) 412 | 413 | | Property | Description | 414 | |:----------------------------------------|:------------| 415 | | `category` | Active AVAudioSession category (e.g., `playback`, `playAndRecord`). | 416 | | `mode` | Active AVAudioSession mode (e.g., `default`, `videoRecording`). | 417 | | `categoryOptions` | Array of enabled category options (e.g., `allowBluetooth`, `defaultToSpeaker`). | 418 | | `routeSharingPolicy` | Current route sharing policy (e.g., `default`, `longFormAudio`). | 419 | | `isOutputtingAudioElsewhere` | Whether audio output is being redirected to another app or device. | 420 | | `allowHapticsAndSystemSoundsDuringRecording` | Whether haptics and system sounds are allowed while recording. | 421 | | `prefersNoInterruptionsFromSystemAlerts` | Preference to avoid interruptions by system alerts. | 422 | | `prefersInterruptionOnRouteDisconnect` | Whether an interruption should occur when audio route is disconnected. | 423 | | `isEchoCancelledInputEnabled` | Whether echo-cancelled input is currently active. | 424 | | `isEchoCancelledInputAvailable` | Whether echo-cancelled input is supported by the current input device. | 425 | | `prefersEchoCancelledInput` | Whether echo-cancelled input is preferred when available. Can only be used with Category: 'PlayAndRecord' and Mode: 'Default' | 426 | 427 | 428 | ### `activate(options?): Promise` 429 | 430 | Activates the native audio session (iOS) or audio focus (Android). 431 | 432 | - **iOS:** Calls `AVAudioSession.setActive(true)`. 433 | - **Android:** Requests audio focus via `AudioManager`. 434 | - **Other platforms:** No-op. 435 | 436 | > ⚠️ On iOS, activating the audio session is a pretty cumbersome operation and can operate. 437 | > Consider deferring it slightly with `setTimeout(() => activate(), 100)` if needed. 438 | 439 | --- 440 | 441 | #### Parameters 442 | 443 | ```ts 444 | type ActivationOptions = { 445 | platform?: 'ios' | 'android' | 'both'; // Default: 'both' 446 | }; 447 | ``` 448 | 449 | - `platform`: 450 | - `'ios'` → Only activate iOS AVAudioSession. 451 | - `'android'` → Only activate Android audio focus. 452 | - `'both'` → (default) Activate both when applicable. 453 | 454 | --- 455 | 456 | #### Example 457 | 458 | ```ts 459 | import { activate } from 'react-native-nitro-audio-manager'; 460 | 461 | await activate(); // activates both iOS and Android 462 | 463 | await activate({ platform: 'ios' }); // activates only iOS 464 | await activate({ platform: 'android' }); // activates only Android 465 | ``` 466 | 467 | --- 468 | 469 | ### `deactivate(options?): Promise` 470 | 471 | Deactivates the native audio session (iOS) or abandons audio focus (Android). 472 | 473 | - **iOS:** Calls `AVAudioSession.setActive(false)`, optionally restoring previous audio sessions. 474 | - **Android:** Abandons audio focus via `AudioManager.abandonAudioFocus`. 475 | - **Other platforms:** No-op. 476 | 477 | --- 478 | 479 | #### Parameters 480 | 481 | ```ts 482 | type DeactivationOptions = { 483 | platform?: 'ios' | 'android' | 'both'; // Default: 'both' 484 | restorePreviousSessionOnDeactivation?: boolean; // Default: true 485 | fallbackToAmbientCategoryAndLeaveActiveForVolumeListener?: boolean; // Default: false 486 | }; 487 | ``` 488 | 489 | - `platform`: 490 | - `'ios'` → Only deactivate iOS session. 491 | - `'android'` → Only abandon Android focus. 492 | - `'both'` → (default) Deactivate both when applicable. 493 | - `restorePreviousSessionOnDeactivation` (iOS only): 494 | If `true`, resumes any previous audio (e.g., background music) after deactivation. Default: true. 495 | - `fallbackToAmbientCategoryAndLeaveActiveForVolumeListener` (iOS only): 496 | Used internally when listening for volume events after deactivation. Default: false 497 | 498 | --- 499 | 500 | #### Example 501 | 502 | ```ts 503 | import { deactivate } from 'react-native-nitro-audio-manager'; 504 | 505 | await deactivate(); // deactivates both iOS and Android 506 | 507 | await deactivate({ platform: 'ios' }); // deactivates only iOS 508 | await deactivate({ platform: 'android' }); // deactivates only Android 509 | 510 | await deactivate({ restorePreviousSessionOnDeactivation: false }); // deactivate without restoring music 511 | ``` 512 | 513 | 514 | ## Contributing 515 | 516 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 517 | 518 | ## License 519 | 520 | MIT 521 | 522 | --- 523 | 524 | Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) 525 | -------------------------------------------------------------------------------- /android/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(audiomanager) 2 | cmake_minimum_required(VERSION 3.9.0) 3 | 4 | set(PACKAGE_NAME audiomanager) 5 | set(CMAKE_VERBOSE_MAKEFILE ON) 6 | set(CMAKE_CXX_STANDARD 20) 7 | 8 | # Define C++ library and add all sources 9 | add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) 10 | 11 | # Add Nitrogen specs :) 12 | include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/audiomanager+autolinking.cmake) 13 | 14 | # Set up local includes 15 | include_directories("src/main/cpp" "../cpp") 16 | 17 | find_library(LOG_LIB log) 18 | 19 | # Link all libraries together 20 | target_link_libraries( 21 | ${PACKAGE_NAME} 22 | ${LOG_LIB} 23 | android # <-- Android core 24 | ) 25 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.getExtOrDefault = {name -> 3 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['AudioManager_' + name] 4 | } 5 | 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath "com.android.tools.build:gradle:8.0.0" 13 | classpath 'com.facebook.react:react-native-gradle-plugin:0.79.1' 14 | // noinspection DifferentKotlinGradleVersion 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" 16 | } 17 | } 18 | 19 | project.ext.react = [ 20 | nodeExecutableAndArgs: ["/Users/you/.nvm/versions/node/v23.4.0/bin/node"] 21 | ] 22 | 23 | def reactNativeArchitectures() { 24 | def value = rootProject.getProperties().get("reactNativeArchitectures") 25 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] 26 | } 27 | 28 | apply plugin: "com.android.library" 29 | apply plugin: "kotlin-android" 30 | apply from: '../nitrogen/generated/android/audiomanager+autolinking.gradle' 31 | 32 | apply plugin: "com.facebook.react" 33 | 34 | def getExtOrIntegerDefault(name) { 35 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["AudioManager_" + name]).toInteger() 36 | } 37 | 38 | def supportsNamespace() { 39 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') 40 | def major = parsed[0].toInteger() 41 | def minor = parsed[1].toInteger() 42 | 43 | // Namespace support was added in 7.3.0 44 | return (major == 7 && minor >= 3) || major >= 8 45 | } 46 | 47 | android { 48 | if (supportsNamespace()) { 49 | namespace "com.margelo.nitro.audiomanager" 50 | 51 | sourceSets { 52 | main { 53 | manifest.srcFile "src/main/AndroidManifestNew.xml" 54 | } 55 | } 56 | } 57 | 58 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") 59 | 60 | defaultConfig { 61 | minSdkVersion getExtOrIntegerDefault("minSdkVersion") 62 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") 63 | 64 | externalNativeBuild { 65 | cmake { 66 | cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" 67 | arguments "-DANDROID_STL=c++_shared" 68 | abiFilters (*reactNativeArchitectures()) 69 | 70 | buildTypes { 71 | debug { 72 | cppFlags "-O1 -g" 73 | } 74 | release { 75 | cppFlags "-O2" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | externalNativeBuild { 83 | cmake { 84 | path "CMakeLists.txt" 85 | } 86 | } 87 | 88 | packagingOptions { 89 | excludes = [ 90 | "META-INF", 91 | "META-INF/**", 92 | "**/libc++_shared.so", 93 | "**/libfbjni.so", 94 | "**/libjsi.so", 95 | "**/libfolly_json.so", 96 | "**/libfolly_runtime.so", 97 | "**/libglog.so", 98 | "**/libhermes.so", 99 | "**/libhermes-executor-debug.so", 100 | "**/libhermes_executor.so", 101 | "**/libreactnative.so", 102 | "**/libreactnativejni.so", 103 | "**/libturbomodulejsijni.so", 104 | "**/libreact_nativemodule_core.so", 105 | "**/libjscexecutor.so" 106 | ] 107 | } 108 | 109 | buildFeatures { 110 | buildConfig true 111 | prefab true 112 | } 113 | 114 | buildTypes { 115 | release { 116 | minifyEnabled false 117 | } 118 | } 119 | 120 | lintOptions { 121 | disable "GradleCompatible" 122 | } 123 | 124 | compileOptions { 125 | sourceCompatibility JavaVersion.VERSION_1_8 126 | targetCompatibility JavaVersion.VERSION_1_8 127 | } 128 | 129 | sourceSets { 130 | main { 131 | java.srcDirs += [ 132 | "generated/java", 133 | "generated/jni" 134 | ] 135 | } 136 | } 137 | } 138 | 139 | repositories { 140 | mavenCentral() 141 | google() 142 | } 143 | 144 | def kotlin_version = getExtOrDefault("kotlinVersion") 145 | 146 | dependencies { 147 | implementation "com.facebook.react:react-android" 148 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 149 | implementation project(":react-native-nitro-modules") 150 | } 151 | 152 | afterEvaluate { 153 | tasks.named("generateCodegenSchemaFromJavaScript") { 154 | dependsOn(":expo-dev-menu:copyAssets") 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | AudioManager_kotlinVersion=2.0.21 2 | AudioManager_minSdkVersion=24 3 | AudioManager_targetSdkVersion=34 4 | AudioManager_compileSdkVersion=35 5 | AudioManager_ndkVersion=27.1.12297006 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifestNew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/cpp/cpp-adapter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "audiomanagerOnLoad.hpp" 3 | 4 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { 5 | return margelo::nitro::audiomanager::initialize(vm); 6 | } 7 | -------------------------------------------------------------------------------- /android/src/main/java/com/audiomanager/AudioManagerPackage.kt: -------------------------------------------------------------------------------- 1 | package com.margelo.nitro.audiomanager 2 | 3 | import com.facebook.react.BaseReactPackage 4 | import com.facebook.react.bridge.NativeModule 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.module.model.ReactModuleInfoProvider 7 | 8 | class AudioManagerPackage : BaseReactPackage() { 9 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { 10 | return null 11 | } 12 | 13 | override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { 14 | return ReactModuleInfoProvider { HashMap() } 15 | } 16 | 17 | companion object { 18 | init { 19 | System.loadLibrary("audiomanager") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/java/com/margelo/nitro/audiomanager/AudioManager.kt: -------------------------------------------------------------------------------- 1 | package com.margelo.nitro.audiomanager 2 | 3 | import android.os.Build 4 | import com.facebook.proguard.annotations.DoNotStrip 5 | import android.media.AudioManager as SysAudioManager 6 | import android.media.AudioDeviceInfo 7 | import android.media.AudioDeviceCallback 8 | import android.media.AudioAttributes 9 | import android.media.AudioFocusRequest 10 | import com.margelo.nitro.NitroModules 11 | import android.content.Context 12 | import com.margelo.nitro.core.* 13 | import android.util.Log 14 | import kotlin.math.roundToInt 15 | 16 | data class Listener( 17 | val id: Double, 18 | val callback: T 19 | ) 20 | 21 | @DoNotStrip 22 | class AudioManager : HybridAudioManagerSpec() { 23 | 24 | companion object { 25 | private const val TAG = "AudioManager" 26 | } 27 | 28 | private lateinit var am: SysAudioManager 29 | 30 | init { 31 | NitroModules.applicationContext?.let { ctx -> 32 | am = ctx.getSystemService(Context.AUDIO_SERVICE) as SysAudioManager 33 | Log.d(TAG, "AudioManager initialized via init{}") 34 | } ?: run { 35 | Log.e(TAG, "AudioManager: applicationContext was null") 36 | } 37 | } 38 | 39 | private val interruptionListeners = mutableListOf Unit>>() 40 | private val routeChangeListeners = mutableListOf Unit>>() 41 | private val volumeListeners = mutableListOf Unit>>() 42 | private var volumeReceiver: VolumeReceiver? = null 43 | 44 | private var nextListenerId = 0.0 45 | 46 | private var lastRoute: Array = emptyArray() 47 | private var currentFocusRequest: AudioFocusRequest? = null 48 | private var hasAudioFocus: Boolean = false 49 | private var focusGainType: Int = SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 50 | private var usage: Int = AudioAttributes.USAGE_MEDIA 51 | private var contentType: Int = AudioAttributes.CONTENT_TYPE_MUSIC 52 | private var willPauseWhenDucked: Boolean = true 53 | private var acceptsDelayedFocusGain: Boolean = false 54 | 55 | 56 | 57 | private val focusCallback = SysAudioManager.OnAudioFocusChangeListener { focus -> 58 | val type = when (focus) { 59 | SysAudioManager.AUDIOFOCUS_LOSS, 60 | SysAudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { 61 | hasAudioFocus = false 62 | InterruptionType.BEGAN 63 | } 64 | SysAudioManager.AUDIOFOCUS_GAIN, 65 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT, 66 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, 67 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> { 68 | hasAudioFocus = true 69 | InterruptionType.ENDED 70 | } 71 | else -> { 72 | hasAudioFocus = false 73 | InterruptionType.BEGAN 74 | } 75 | } 76 | 77 | val reason = when (focus) { 78 | SysAudioManager.AUDIOFOCUS_LOSS, 79 | SysAudioManager.AUDIOFOCUS_LOSS_TRANSIENT, 80 | SysAudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> InterruptionReason.APPWASSUSPENDED 81 | else -> InterruptionReason.DEFAULT 82 | } 83 | 84 | val ev = InterruptionEvent(type, reason) 85 | interruptionListeners.forEach { it.callback(ev) } 86 | } 87 | 88 | private val deviceCallback = object : AudioDeviceCallback() { 89 | override fun onAudioDevicesAdded(added: Array) { 90 | dispatchRouteChange(RouteChangeReason.NEWDEVICEAVAILABLE) 91 | } 92 | override fun onAudioDevicesRemoved(removed: Array) { 93 | dispatchRouteChange(RouteChangeReason.OLDDEVICEUNAVAILABLE) 94 | } 95 | } 96 | 97 | override fun addInterruptionListener(callback: (InterruptionEvent) -> Unit): Double { 98 | if (interruptionListeners.isEmpty()) { 99 | am.requestAudioFocus( 100 | focusCallback, 101 | SysAudioManager.STREAM_MUSIC, 102 | SysAudioManager.AUDIOFOCUS_GAIN 103 | ) 104 | } 105 | val id = nextListenerId++ 106 | interruptionListeners += Listener(id, callback) 107 | return id 108 | } 109 | 110 | override fun removeInterruptionListener(id: Double) { 111 | interruptionListeners.removeAll { it.id == id } 112 | } 113 | 114 | override fun addRouteChangeListener(callback: (RouteChangeEvent) -> Unit): Double { 115 | if (routeChangeListeners.isEmpty()) { 116 | lastRoute = am 117 | .getDevices(SysAudioManager.GET_DEVICES_OUTPUTS) 118 | .map { mapToPort(it) } 119 | .toTypedArray() 120 | 121 | am.registerAudioDeviceCallback(deviceCallback, null) 122 | } 123 | val id = nextListenerId++ 124 | routeChangeListeners += Listener(id, callback) 125 | return id 126 | } 127 | 128 | override fun removeRouteChangeListener(id: Double) { 129 | routeChangeListeners.removeAll { it.id == id } 130 | if (routeChangeListeners.isEmpty()) { 131 | am.unregisterAudioDeviceCallback(deviceCallback) 132 | lastRoute = emptyArray() 133 | } 134 | } 135 | 136 | private fun dispatchRouteChange(reason: RouteChangeReason) { 137 | val currentRoute = am 138 | .getDevices(SysAudioManager.GET_DEVICES_OUTPUTS) 139 | .map { mapToPort(it) } 140 | .toTypedArray() 141 | 142 | val ev = RouteChangeEvent( 143 | prevRoute = lastRoute, 144 | currentRoute = currentRoute, 145 | reason = reason 146 | ) 147 | 148 | lastRoute = currentRoute 149 | 150 | routeChangeListeners.forEach { it.callback(ev) } 151 | } 152 | 153 | private inner class VolumeReceiver : android.content.BroadcastReceiver() { 154 | override fun onReceive(context: Context?, intent: android.content.Intent?) { 155 | if (intent?.action == "android.media.VOLUME_CHANGED_ACTION") { 156 | val current = am.getStreamVolume(SysAudioManager.STREAM_MUSIC) 157 | val max = am.getStreamMaxVolume(SysAudioManager.STREAM_MUSIC) 158 | val normalized = if (max > 0) current.toDouble() / max.toDouble() else 0.0 159 | volumeListeners.forEach { it.callback(normalized) } 160 | } 161 | } 162 | } 163 | 164 | 165 | override fun addVolumeListener(callback: (Double) -> Unit): Double { 166 | if (volumeListeners.isEmpty()) { 167 | volumeReceiver = VolumeReceiver() 168 | NitroModules.applicationContext?.registerReceiver( 169 | volumeReceiver, 170 | android.content.IntentFilter("android.media.VOLUME_CHANGED_ACTION") 171 | ) 172 | } 173 | val id = nextListenerId++ 174 | volumeListeners += Listener(id, callback) 175 | return id 176 | } 177 | 178 | 179 | override fun removeVolumeListener(id: Double) { 180 | volumeListeners.removeAll { it.id == id } 181 | if (volumeListeners.isEmpty()) { 182 | volumeReceiver?.let { receiver -> 183 | NitroModules.applicationContext?.unregisterReceiver(receiver) 184 | volumeReceiver = null 185 | } 186 | } 187 | } 188 | 189 | override fun getSystemVolume(): Promise { 190 | return Promise.async { 191 | val current = am.getStreamVolume(SysAudioManager.STREAM_MUSIC) 192 | val max = am.getStreamMaxVolume(SysAudioManager.STREAM_MUSIC) 193 | if (max > 0) current.toDouble() / max.toDouble() else 0.0 194 | } 195 | } 196 | 197 | override fun setSystemVolume(value: Double, showUI: Boolean): Promise = Promise.async { 198 | val maxVolume = am.getStreamMaxVolume(SysAudioManager.STREAM_MUSIC) 199 | val newVolume = (value.coerceIn(0.0, 1.0) * maxVolume).roundToInt() 200 | 201 | val flags = if (showUI) { 202 | SysAudioManager.FLAG_SHOW_UI 203 | } else { 204 | 0 205 | } 206 | 207 | am.setStreamVolume( 208 | SysAudioManager.STREAM_MUSIC, 209 | newVolume, 210 | flags 211 | ) 212 | } 213 | 214 | override fun isActive(): Boolean { 215 | return hasAudioFocus 216 | } 217 | 218 | override fun activate(warningCallback: (AudioSessionWarning) -> Unit): Promise = Promise.async { 219 | if (currentFocusRequest == null) { 220 | 221 | val attrs = AudioAttributes.Builder() 222 | .setUsage(usage) 223 | .setContentType(contentType) 224 | .build() 225 | 226 | currentFocusRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 227 | AudioFocusRequest.Builder(focusGainType) 228 | .setAudioAttributes(attrs) 229 | .setWillPauseWhenDucked(willPauseWhenDucked) 230 | .setAcceptsDelayedFocusGain(acceptsDelayedFocusGain) 231 | .setOnAudioFocusChangeListener(focusCallback) 232 | .build() 233 | } else null 234 | } 235 | 236 | val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentFocusRequest != null) { 237 | am.requestAudioFocus(currentFocusRequest!!) 238 | } else { 239 | // For older APIs, fallback to the deprecated method 240 | @Suppress("DEPRECATION") 241 | am.requestAudioFocus( 242 | focusCallback, 243 | SysAudioManager.STREAM_MUSIC, 244 | focusGainType 245 | ) 246 | } 247 | 248 | if (result != SysAudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 249 | throw Exception("Audio focus request failed: $result") 250 | } 251 | } 252 | 253 | override fun deactivate( 254 | restorePreviousSessionOnDeactivation: Boolean, 255 | fallbackToAmbientCategoryAndLeaveActiveForVolumeListener: Boolean, 256 | warningCallback: (AudioSessionWarning 257 | ) -> Unit): Promise = Promise.async { 258 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentFocusRequest != null) { 259 | am.abandonAudioFocusRequest(currentFocusRequest!!) 260 | } else { 261 | @Suppress("DEPRECATION") 262 | am.abandonAudioFocus(focusCallback) 263 | } 264 | } 265 | 266 | /** 267 | * Returns the buffer‑size / sample‑rate = seconds of latency per buffer. 268 | * E.g. 256 frames @ 48 000 Hz ≈ 5.3 ms = 0.0053 s 269 | */ 270 | override fun getOutputLatency(): Double { 271 | val srStr = am.getProperty(SysAudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) 272 | val bufStr = am.getProperty(SysAudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER) 273 | 274 | if (srStr == null || bufStr == null) { 275 | Log.w(TAG, "Output latency props unavailable") 276 | return -1.0 277 | } 278 | 279 | val sampleRate = srStr.toIntOrNull() ?: return -1.0 280 | val framesPerBuf = bufStr.toIntOrNull() ?: return -1.0 281 | 282 | return framesPerBuf.toDouble() / sampleRate.toDouble() 283 | } 284 | 285 | /** 286 | * On Android S+ you can query the input buffer size; on older releases it’s not exposed. 287 | */ 288 | override fun getInputLatency(): Double { 289 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 290 | val inFramesStr = am.getProperty("android.media.property.INPUT_FRAMES_PER_BUFFER") 291 | val srStr = am.getProperty(SysAudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) 292 | val inFrames = inFramesStr?.toIntOrNull() ?: return -1.0 293 | val sr = srStr?.toIntOrNull() ?: return -1.0 294 | return inFrames.toDouble() / sr.toDouble() 295 | } 296 | // Property not computable in older versions 297 | return -1.0 298 | } 299 | 300 | private fun mapToPort(device: AudioDeviceInfo): PortDescription { 301 | val type = when (device.type) { 302 | AudioDeviceInfo.TYPE_BUILTIN_MIC -> PortType.BUILTINMIC 303 | AudioDeviceInfo.TYPE_WIRED_HEADSET, 304 | AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> PortType.HEADSETMIC 305 | AudioDeviceInfo.TYPE_LINE_ANALOG, 306 | AudioDeviceInfo.TYPE_LINE_DIGITAL -> PortType.LINEIN 307 | 308 | AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> PortType.BLUETOOTHA2DP 309 | AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> PortType.BLUETOOTHHFP 310 | 311 | AudioDeviceInfo.TYPE_HDMI -> PortType.HDMI 312 | AudioDeviceInfo.TYPE_USB_DEVICE, 313 | AudioDeviceInfo.TYPE_USB_HEADSET -> PortType.USBAUDIO 314 | 315 | AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> PortType.BUILTINSPEAKER 316 | AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, 317 | AudioDeviceInfo.TYPE_TELEPHONY -> PortType.BUILTINRECEIVER 318 | 319 | else -> PortType.UNKNOWN 320 | } 321 | 322 | return PortDescription( 323 | portName = device.productName?.toString() ?: "Unknown", 324 | portType = type, 325 | uid = device.id.toString(), 326 | channels = device.channelCounts.takeIf { it.isNotEmpty() }?.map { it.toDouble() }?.toDoubleArray(), 327 | isDataSourceSupported = true, 328 | selectedDataSourceId = null 329 | ) 330 | } 331 | 332 | override fun getCategoryCompatibleInputs(): Array { 333 | // no op 334 | return arrayOf() 335 | } 336 | 337 | override fun getCurrentInputRoutes(): Array { 338 | 339 | val inputs = am.getDevices(SysAudioManager.GET_DEVICES_INPUTS) 340 | 341 | val active: AudioDeviceInfo? = when { 342 | @Suppress("DEPRECATION") 343 | am.isWiredHeadsetOn -> inputs.firstOrNull { 344 | it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || 345 | it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES 346 | } 347 | 348 | @Suppress("DEPRECATION") 349 | am.isBluetoothScoOn -> inputs.firstOrNull { 350 | it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO 351 | } 352 | 353 | else -> inputs.firstOrNull { 354 | it.type == AudioDeviceInfo.TYPE_BUILTIN_MIC 355 | } 356 | } 357 | 358 | val chosen = active ?: inputs.firstOrNull() 359 | 360 | return active 361 | ?.let { arrayOf(mapToPort(it)) } 362 | ?: arrayOf() 363 | } 364 | 365 | override fun getCurrentOutputRoutes(): Array { 366 | val outputs = am.getDevices(SysAudioManager.GET_DEVICES_OUTPUTS) 367 | 368 | val active: AudioDeviceInfo? = when { 369 | @Suppress("DEPRECATION") 370 | am.isBluetoothA2dpOn -> outputs.firstOrNull { 371 | it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP 372 | } 373 | 374 | @Suppress("DEPRECATION") 375 | am.isWiredHeadsetOn -> outputs.firstOrNull { 376 | it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || 377 | it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET 378 | } 379 | 380 | @Suppress("DEPRECATION") 381 | am.isSpeakerphoneOn -> outputs.firstOrNull { 382 | it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER 383 | } 384 | 385 | else -> outputs.firstOrNull { 386 | it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || 387 | it.type == AudioDeviceInfo.TYPE_TELEPHONY 388 | } 389 | } 390 | 391 | return active 392 | ?.let { arrayOf(mapToPort(it)) } 393 | ?: arrayOf() 394 | } 395 | 396 | override fun forceOutputToSpeaker(warningCallback: (AudioSessionWarning) -> Unit) { 397 | // no op 398 | } 399 | 400 | override fun cancelForcedOutputToSpeaker() { 401 | // no op 402 | } 403 | 404 | override fun isWiredHeadphonesConnected(): Boolean { 405 | val outputs: Array = am.getDevices(SysAudioManager.GET_DEVICES_OUTPUTS) 406 | return outputs.any { device -> 407 | device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || 408 | device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET 409 | } 410 | } 411 | 412 | override fun isBluetoothHeadphonesConnected(): Boolean { 413 | val outputs: Array = am.getDevices(SysAudioManager.GET_DEVICES_OUTPUTS) 414 | return outputs.any { device -> 415 | device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || 416 | device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO 417 | } 418 | } 419 | 420 | override fun getAudioSessionStatusIOS(): AudioSessionStatus? { 421 | // no-op 422 | return null 423 | } 424 | 425 | override fun getAudioManagerStatusAndroid(): AudioManagerStatus? { 426 | val mode = when { 427 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && 428 | am.mode == SysAudioManager.MODE_CALL_SCREENING -> 429 | AudioMode.CALLSCREENING 430 | am.mode == SysAudioManager.MODE_RINGTONE -> 431 | AudioMode.RINGTONE 432 | am.mode == SysAudioManager.MODE_IN_CALL -> 433 | AudioMode.INCALL 434 | am.mode == SysAudioManager.MODE_IN_COMMUNICATION -> 435 | AudioMode.INCOMMUNICATION 436 | else -> 437 | AudioMode.NORMAL 438 | } 439 | 440 | val ringer = when (am.ringerMode) { 441 | SysAudioManager.RINGER_MODE_SILENT -> RingerMode.SILENT 442 | SysAudioManager.RINGER_MODE_VIBRATE -> RingerMode.VIBRATE 443 | else -> RingerMode.NORMAL 444 | } 445 | 446 | val focusGainEnum = when (focusGainType) { 447 | SysAudioManager.AUDIOFOCUS_GAIN -> AudioFocusGainType.GAIN 448 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> AudioFocusGainType.GAINTRANSIENT 449 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> AudioFocusGainType.GAINTRANSIENTMAYDUCK 450 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> AudioFocusGainType.GAINTRANSIENTEXCLUSIVE 451 | else -> AudioFocusGainType.GAIN 452 | } 453 | 454 | val usageEnum = when (usage) { 455 | AudioAttributes.USAGE_ALARM -> AudioUsage.ALARM 456 | AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY -> AudioUsage.ASSISTANCEACCESSIBILITY 457 | AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE -> AudioUsage.ASSISTANCENAVIGATIONGUIDANCE 458 | AudioAttributes.USAGE_ASSISTANCE_SONIFICATION -> AudioUsage.ASSISTANCESONIFICATION 459 | AudioAttributes.USAGE_ASSISTANT -> AudioUsage.ASSISTANT 460 | AudioAttributes.USAGE_GAME -> AudioUsage.GAME 461 | AudioAttributes.USAGE_MEDIA -> AudioUsage.MEDIA 462 | AudioAttributes.USAGE_NOTIFICATION -> AudioUsage.NOTIFICATION 463 | AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED, 464 | AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT, 465 | AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST -> AudioUsage.NOTIFICATION 466 | AudioAttributes.USAGE_NOTIFICATION_EVENT -> AudioUsage.NOTIFICATIONEVENT 467 | AudioAttributes.USAGE_NOTIFICATION_RINGTONE -> AudioUsage.NOTIFICATIONRINGTONE 468 | AudioAttributes.USAGE_UNKNOWN -> AudioUsage.UNKNOWN 469 | AudioAttributes.USAGE_VOICE_COMMUNICATION -> AudioUsage.VOICECOMMUNICATION 470 | AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING -> AudioUsage.VOICECOMMUNICATIONSIGNALLING 471 | else -> AudioUsage.UNKNOWN 472 | } 473 | 474 | val contentEnum = when (contentType) { 475 | AudioAttributes.CONTENT_TYPE_MOVIE -> AudioContentType.MOVIE 476 | AudioAttributes.CONTENT_TYPE_MUSIC -> AudioContentType.MUSIC 477 | AudioAttributes.CONTENT_TYPE_SONIFICATION -> AudioContentType.SONIFICATION 478 | AudioAttributes.CONTENT_TYPE_SPEECH -> AudioContentType.SPEECH 479 | AudioAttributes.CONTENT_TYPE_UNKNOWN -> AudioContentType.UNKNOWN 480 | else -> AudioContentType.UNKNOWN 481 | } 482 | 483 | return AudioManagerStatus( 484 | mode = mode, 485 | ringerMode = ringer, 486 | focusGain = focusGainEnum, 487 | usage = usageEnum, 488 | contentType = contentEnum, 489 | willPauseWhenDucked = willPauseWhenDucked, 490 | acceptsDelayedFocusGain= acceptsDelayedFocusGain 491 | ) 492 | } 493 | 494 | override fun configureAudioSession( 495 | category: String, 496 | mode: String, 497 | policy: String, 498 | categoryOptions: Array, 499 | prefersNoInterruptionFromSystemAlerts: Boolean, 500 | prefersInterruptionOnRouteDisconnect: Boolean, 501 | allowHapticsAndSystemSoundsDuringRecording: Boolean, 502 | prefersEchoCancelledInput: Boolean, 503 | warningCallback: (AudioSessionWarning) -> Unit 504 | ) { 505 | // no-op 506 | } 507 | 508 | override fun configureAudioManager( 509 | focusGain: String, 510 | usage: String, 511 | contentType: String, 512 | willPauseWhenDucked: Boolean, 513 | acceptsDelayedFocusGain: Boolean 514 | ): Unit { 515 | focusGainType = when (focusGain) { 516 | "GAIN" -> SysAudioManager.AUDIOFOCUS_GAIN 517 | "GAIN_TRANSIENT" -> SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT 518 | "GAIN_TRANSIENT_MAY_DUCK" -> SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 519 | "GAIN_TRANSIENT_EXCLUSIVE"-> SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 520 | "GAIN_TRANSIENT_ALLOW_PAUSE" -> 521 | SysAudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 522 | else -> SysAudioManager.AUDIOFOCUS_GAIN 523 | } 524 | 525 | this.usage = when (usage) { 526 | "USAGE_GAME" -> AudioAttributes.USAGE_GAME 527 | "USAGE_VOICE_COMMUNICATION" -> AudioAttributes.USAGE_VOICE_COMMUNICATION 528 | "USAGE_ALARM" -> AudioAttributes.USAGE_ALARM 529 | "USAGE_NOTIFICATION" -> AudioAttributes.USAGE_NOTIFICATION 530 | else -> AudioAttributes.USAGE_MEDIA 531 | } 532 | 533 | this.contentType = when (contentType) { 534 | "CONTENT_TYPE_SPEECH" -> AudioAttributes.CONTENT_TYPE_SPEECH 535 | "CONTENT_TYPE_MOVIE" -> AudioAttributes.CONTENT_TYPE_MOVIE 536 | "CONTENT_TYPE_SONIFICATION"-> AudioAttributes.CONTENT_TYPE_SONIFICATION 537 | else -> AudioAttributes.CONTENT_TYPE_MUSIC 538 | } 539 | 540 | // direct assignment now that both sides are non-nullable 541 | this.willPauseWhenDucked = willPauseWhenDucked 542 | this.acceptsDelayedFocusGain = acceptsDelayedFocusGain 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:react-native-builder-bob/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherGabba/react-native-nitro-audio-manager/4c64ceb3f26f3a9ad27fc433664e267692e1c02b/banner.jpg -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import { FlatCompat } from '@eslint/eslintrc'; 3 | import js from '@eslint/js'; 4 | import prettier from 'eslint-plugin-prettier'; 5 | import { defineConfig } from 'eslint/config'; 6 | import path from 'node:path'; 7 | import { fileURLToPath } from 'node:url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default defineConfig([ 18 | { 19 | extends: fixupConfigRules(compat.extends('@react-native', 'prettier')), 20 | plugins: { prettier }, 21 | rules: { 22 | 'react/react-in-jsx-scope': 'off', 23 | 'prettier/prettier': [ 24 | 'error', 25 | { 26 | quoteProps: 'consistent', 27 | singleQuote: true, 28 | tabWidth: 2, 29 | trailingComma: 'es5', 30 | useTabs: false, 31 | }, 32 | ], 33 | }, 34 | }, 35 | { 36 | ignores: [ 37 | 'node_modules/', 38 | 'lib/' 39 | ], 40 | }, 41 | ]); 42 | -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '< 1.3.4' 11 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.2.1) 9 | base64 10 | benchmark (>= 0.3) 11 | bigdecimal 12 | concurrent-ruby (~> 1.0, >= 1.3.1) 13 | connection_pool (>= 2.2.5) 14 | drb 15 | i18n (>= 1.6, < 2) 16 | logger (>= 1.4.2) 17 | minitest (>= 5.1) 18 | securerandom (>= 0.3) 19 | tzinfo (~> 2.0, >= 2.0.5) 20 | addressable (2.8.7) 21 | public_suffix (>= 2.0.2, < 7.0) 22 | algoliasearch (1.27.5) 23 | httpclient (~> 2.8, >= 2.8.3) 24 | json (>= 1.5.1) 25 | atomos (0.1.3) 26 | base64 (0.2.0) 27 | benchmark (0.4.0) 28 | bigdecimal (3.1.9) 29 | claide (1.1.0) 30 | cocoapods (1.15.2) 31 | addressable (~> 2.8) 32 | claide (>= 1.0.2, < 2.0) 33 | cocoapods-core (= 1.15.2) 34 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 35 | cocoapods-downloader (>= 2.1, < 3.0) 36 | cocoapods-plugins (>= 1.0.0, < 2.0) 37 | cocoapods-search (>= 1.0.0, < 2.0) 38 | cocoapods-trunk (>= 1.6.0, < 2.0) 39 | cocoapods-try (>= 1.1.0, < 2.0) 40 | colored2 (~> 3.1) 41 | escape (~> 0.0.4) 42 | fourflusher (>= 2.3.0, < 3.0) 43 | gh_inspector (~> 1.0) 44 | molinillo (~> 0.8.0) 45 | nap (~> 1.0) 46 | ruby-macho (>= 2.3.0, < 3.0) 47 | xcodeproj (>= 1.23.0, < 2.0) 48 | cocoapods-core (1.15.2) 49 | activesupport (>= 5.0, < 8) 50 | addressable (~> 2.8) 51 | algoliasearch (~> 1.0) 52 | concurrent-ruby (~> 1.1) 53 | fuzzy_match (~> 2.0.4) 54 | nap (~> 1.0) 55 | netrc (~> 0.11) 56 | public_suffix (~> 4.0) 57 | typhoeus (~> 1.0) 58 | cocoapods-deintegrate (1.0.5) 59 | cocoapods-downloader (2.1) 60 | cocoapods-plugins (1.0.0) 61 | nap 62 | cocoapods-search (1.0.1) 63 | cocoapods-trunk (1.6.0) 64 | nap (>= 0.8, < 2.0) 65 | netrc (~> 0.11) 66 | cocoapods-try (1.2.0) 67 | colored2 (3.1.2) 68 | concurrent-ruby (1.3.3) 69 | connection_pool (2.5.1) 70 | drb (2.2.1) 71 | escape (0.0.4) 72 | ethon (0.16.0) 73 | ffi (>= 1.15.0) 74 | ffi (1.17.2) 75 | fourflusher (2.3.1) 76 | fuzzy_match (2.0.4) 77 | gh_inspector (1.1.3) 78 | httpclient (2.9.0) 79 | mutex_m 80 | i18n (1.14.7) 81 | concurrent-ruby (~> 1.0) 82 | json (2.10.2) 83 | logger (1.7.0) 84 | minitest (5.25.5) 85 | molinillo (0.8.0) 86 | mutex_m (0.3.0) 87 | nanaimo (0.3.0) 88 | nap (1.1.0) 89 | netrc (0.11.0) 90 | nkf (0.2.0) 91 | public_suffix (4.0.7) 92 | rexml (3.4.1) 93 | ruby-macho (2.5.1) 94 | securerandom (0.4.1) 95 | typhoeus (1.4.1) 96 | ethon (>= 0.9.0) 97 | tzinfo (2.0.6) 98 | concurrent-ruby (~> 1.0) 99 | xcodeproj (1.25.1) 100 | CFPropertyList (>= 2.3.3, < 4.0) 101 | atomos (~> 0.1.3) 102 | claide (>= 1.0.2, < 2.0) 103 | colored2 (~> 3.1) 104 | nanaimo (~> 0.3.0) 105 | rexml (>= 3.3.6, < 4.0) 106 | 107 | PLATFORMS 108 | ruby 109 | 110 | DEPENDENCIES 111 | activesupport (>= 6.1.7.5, != 7.1.0) 112 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0) 113 | concurrent-ruby (< 1.3.4) 114 | xcodeproj (< 1.26.0) 115 | 116 | RUBY VERSION 117 | ruby 3.4.3p32 118 | 119 | BUNDLED WITH 120 | 2.6.7 121 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). 2 | 3 | # Getting Started 4 | 5 | > **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. 6 | 7 | ## Step 1: Start Metro 8 | 9 | First, you will need to run **Metro**, the JavaScript build tool for React Native. 10 | 11 | To start the Metro dev server, run the following command from the root of your React Native project: 12 | 13 | ```sh 14 | # Using npm 15 | npm start 16 | 17 | # OR using Yarn 18 | yarn start 19 | ``` 20 | 21 | ## Step 2: Build and run your app 22 | 23 | With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: 24 | 25 | ### Android 26 | 27 | ```sh 28 | # Using npm 29 | npm run android 30 | 31 | # OR using Yarn 32 | yarn android 33 | ``` 34 | 35 | ### iOS 36 | 37 | For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). 38 | 39 | The first time you create a new project, run the Ruby bundler to install CocoaPods itself: 40 | 41 | ```sh 42 | bundle install 43 | ``` 44 | 45 | Then, and every time you update your native dependencies, run: 46 | 47 | ```sh 48 | bundle exec pod install 49 | ``` 50 | 51 | For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). 52 | 53 | ```sh 54 | # Using npm 55 | npm run ios 56 | 57 | # OR using Yarn 58 | yarn ios 59 | ``` 60 | 61 | If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. 62 | 63 | This is one way to run your app — you can also build it directly from Android Studio or Xcode. 64 | 65 | ## Step 3: Modify your app 66 | 67 | Now that you have successfully run the app, let's make changes! 68 | 69 | Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). 70 | 71 | When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: 72 | 73 | - **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). 74 | - **iOS**: Press R in iOS Simulator. 75 | 76 | ## Congratulations! :tada: 77 | 78 | You've successfully run and modified your React Native App. :partying_face: 79 | 80 | ### Now what? 81 | 82 | - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). 83 | - If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). 84 | 85 | # Troubleshooting 86 | 87 | If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. 88 | 89 | # Learn More 90 | 91 | To learn more about React Native, take a look at the following resources: 92 | 93 | - [React Native Website](https://reactnative.dev) - learn more about React Native. 94 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. 95 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. 96 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. 97 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. 98 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "AudioManagerExample", 4 | "slug": "audiomanagerexample", 5 | "version": "1.0.0", 6 | "platforms": [ 7 | "ios", 8 | "android" 9 | ], 10 | "orientation": "portrait", 11 | "userInterfaceStyle": "light", 12 | "splash": { 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0 18 | }, 19 | "assetBundlePatterns": [ 20 | "**/*" 21 | ], 22 | "ios": { 23 | "bundleIdentifier": "audiomanager.example", 24 | "appleTeamId": "32U9AXHQSK" 25 | }, 26 | "android": { 27 | "package": "audiomanager.example" 28 | }, 29 | "plugins": [ 30 | "expo-build-properties" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getConfig } = require('react-native-builder-bob/babel-config'); 3 | const pkg = require('../package.json'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | module.exports = getConfig( 8 | { 9 | presets: ['babel-preset-expo'], 10 | }, 11 | { root, pkg } 12 | ); 13 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | import App from './src/App'; 3 | 4 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 5 | // It also ensures that whether you load the app in Expo Go or in a native build, 6 | // the environment is set up appropriately 7 | registerRootComponent(App); 8 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getDefaultConfig } = require('@expo/metro-config'); 3 | const { getConfig } = require('react-native-builder-bob/metro-config'); 4 | const pkg = require('../package.json'); 5 | 6 | const root = path.resolve(__dirname, '..'); 7 | 8 | /** 9 | * Metro configuration 10 | * https://facebook.github.io/metro/docs/configuration 11 | * 12 | * @type {import('metro-config').MetroConfig} 13 | */ 14 | module.exports = getConfig(getDefaultConfig(__dirname), { 15 | root, 16 | pkg, 17 | project: __dirname, 18 | }); 19 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-nitro-audio-manager-example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "expo run:android", 7 | "ios": "expo run:ios", 8 | "start": "react-native start", 9 | "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", 10 | "build:ios": "react-native build-ios --scheme AudioManagerExample --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\"" 11 | }, 12 | "dependencies": { 13 | "expo": "~53.0.0-preview.12", 14 | "expo-build-properties": "~0.14.5", 15 | "expo-dev-client": "~5.1.5", 16 | "expo-device": "~7.1.3", 17 | "react": "19.0.0", 18 | "react-native": "0.79.1", 19 | "react-native-element-dropdown": "^2.12.4", 20 | "react-native-nitro-modules": "^0.25.2" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.25.2", 24 | "@babel/preset-env": "^7.25.3", 25 | "@babel/runtime": "^7.25.0", 26 | "@react-native-community/cli": "15.0.1", 27 | "@react-native-community/cli-platform-android": "15.0.1", 28 | "@react-native-community/cli-platform-ios": "15.0.1", 29 | "@react-native/babel-preset": "0.78.2", 30 | "@react-native/typescript-config": "0.78.2", 31 | "@types/react": "~19.0.10", 32 | "metro": "0.82.0", 33 | "metro-config": "0.82.0", 34 | "metro-resolver": "0.82.0", 35 | "react-native-builder-bob": "^0.40.6" 36 | }, 37 | "engines": { 38 | "node": ">=18" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('../package.json'); 3 | 4 | module.exports = { 5 | project: { 6 | ios: { 7 | automaticPodsInstallation: true, 8 | }, 9 | }, 10 | dependencies: { 11 | [pkg.name]: { 12 | root: path.join(__dirname, '..'), 13 | platforms: { 14 | // Codegen script incorrectly fails without this 15 | // So we explicitly specify the platforms with empty object 16 | ios: {}, 17 | android: {}, 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | // App.tsx 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import { 4 | SafeAreaView, 5 | ScrollView, 6 | StyleSheet, 7 | Text, 8 | View, 9 | Button, 10 | Switch, 11 | Platform, 12 | } from 'react-native'; 13 | import * as Device from 'expo-device'; 14 | import { 15 | getSystemVolume, 16 | getOutputLatency, 17 | getInputLatency, 18 | getCurrentInputRoutes, 19 | getCurrentOutputRoutes, 20 | forceOutputToSpeaker, 21 | cancelForcedOutputToSpeaker, 22 | activate, 23 | deactivate, 24 | getAudioStatus, 25 | addListener, 26 | useIsHeadphonesConnected, 27 | PortDescription, 28 | setSystemVolume, 29 | configureAudio, 30 | AudioSessionCategory, 31 | AudioSessionMode, 32 | AudioSessionCategoryOptions, 33 | } from 'react-native-nitro-audio-manager'; 34 | import { appendWithLimit } from './utils'; 35 | import { 36 | iosTestCombinations, 37 | runIOSCategoryTests, 38 | TestResult, 39 | } from './iosCombinations'; 40 | import { IOSSessionPicker, IOSSessionPickerValue } from './IOSSessionPicker'; 41 | // import Slider from '@react-native-community/slider'; 42 | 43 | export default function App() { 44 | // MARK: State Variables 45 | const [volume, setVolume] = useState(0); 46 | const [liveUpdateVolume, setLiveUpdateVolume] = useState(0); 47 | const [outLatency, setOutLatency] = useState(getOutputLatency()); 48 | const [inLatency, setInLatency] = useState(getInputLatency()); 49 | const [inRoutes, setInRoutes] = useState([]); 50 | const [outRoutes, setOutRoutes] = useState([]); 51 | const [sessionStatus, setSessionStatus] = useState(null); 52 | const { wired, wireless } = useIsHeadphonesConnected(); 53 | const [isActivated, setIsActivated] = useState(false); 54 | const [session, setSession] = useState({ 55 | category: AudioSessionCategory.Ambient, 56 | mode: AudioSessionMode.Default, 57 | option: [AudioSessionCategoryOptions.MixWithOthers], 58 | }); 59 | const [lastFiveRouteChangeEvents, setLastFiveRouteChangeEvents] = useState< 60 | string[] 61 | >(['']); 62 | const [lastFiveInterruptionEvents, setLastFiveInterruptionEvents] = useState< 63 | string[] 64 | >(['']); 65 | 66 | const manageFiveMostRecentRouteChangeEvents = (event: string) => { 67 | setLastFiveRouteChangeEvents((events) => appendWithLimit(events, event, 5)); 68 | }; 69 | 70 | const manageFiveMostRecentInterruptionEvents = (event: string) => { 71 | setLastFiveInterruptionEvents((events) => 72 | appendWithLimit(events, event, 5) 73 | ); 74 | }; 75 | 76 | // helper to pretty‑print routes 77 | const routesToString = (arr: PortDescription[]) => 78 | arr.map((r) => `${r.portType}:${r.uid}`).join('\n') || 'none'; 79 | 80 | // MARK: Listeners 81 | const routeChangeEventCounter = useRef(1); 82 | useEffect(() => { 83 | const unsub = addListener('routeChange', (evt) => { 84 | manageFiveMostRecentRouteChangeEvents( 85 | `${routeChangeEventCounter.current}: ${evt.reason}\noldDevice: ${routesToString(evt.prevRoute)}\nnewDevice: ${routesToString(evt.currentRoute)}` 86 | ); 87 | routeChangeEventCounter.current++; 88 | }); 89 | return unsub; 90 | }, []); 91 | 92 | const audioInterruptionEventCounter = useRef(1); 93 | useEffect(() => { 94 | const unsub = addListener('audioInterruption', (evt) => { 95 | manageFiveMostRecentInterruptionEvents( 96 | `${audioInterruptionEventCounter.current}: Interruption ${evt.type}: ${evt.reason}` 97 | ); 98 | audioInterruptionEventCounter.current++; 99 | }); 100 | return unsub; 101 | }, []); 102 | 103 | useEffect(() => { 104 | getSystemVolume().then((value) => { 105 | setVolume(value); 106 | setLiveUpdateVolume(value); 107 | }); 108 | const unsub = addListener('volume', (value) => { 109 | setLiveUpdateVolume(value); 110 | }); 111 | return unsub; 112 | }, []); 113 | 114 | const [testResults, setTestResults] = useState([]); 115 | const addToTestArray = (result: TestResult) => { 116 | setTestResults((existing) => { 117 | return [ 118 | ...existing, 119 | `Test ${result.testId}: ${result.passResult ? '✅' : '❌'}`, 120 | ]; 121 | }); 122 | }; 123 | // MARK: UI 124 | return ( 125 | 126 | 127 | Audio Manager Example App 128 | 129 | Device Info: 130 | 131 | Audio Control Can Vary by System Version and Phone Model 132 | 133 | 134 | Model: {Device.manufacturer}: {Device.modelName} 135 | 136 | Sys Version: {Device.osVersion} 137 | 138 | For example: New Android SDKs handle audio session on their own, and 139 | newer iPhones have an EchoCancelledInput available for use. 140 | 141 | Volume 142 | 143 | Test #1: Raise volume up and down on side of phone and tap "Get System 144 | Volume Manually" button to check value. 145 | 146 | 147 |