├── .asf.yaml ├── .eslintrc.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── SUPPORT_QUESTION.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── android.yml │ ├── chrome.yml │ ├── ios.yml │ └── lint.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── RELEASENOTES.md ├── package-lock.json ├── package.json ├── plugin.xml ├── src ├── android │ ├── AudioHandler.java │ ├── AudioPlayer.java │ └── FileHelper.java └── ios │ ├── CDVSound.h │ └── CDVSound.m ├── tests ├── package.json ├── plugin.xml └── tests.js ├── types └── index.d.ts └── www ├── Media.js ├── MediaError.js └── browser └── Media.js /.asf.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | github: 19 | description: Apache Cordova Media Plugin 20 | homepage: https://cordova.apache.org/ 21 | 22 | labels: 23 | - android 24 | - cordova 25 | - hacktoberfest 26 | - ios 27 | - java 28 | - javascript 29 | - library 30 | - mobile 31 | - nodejs 32 | - objective-c 33 | 34 | features: 35 | wiki: false 36 | issues: true 37 | projects: true 38 | 39 | enabled_merge_buttons: 40 | squash: true 41 | merge: false 42 | rebase: false 43 | 44 | notifications: 45 | commits: commits@cordova.apache.org 46 | issues: issues@cordova.apache.org 47 | pullrequests_status: issues@cordova.apache.org 48 | pullrequests_comment: issues@cordova.apache.org 49 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | root: true 19 | extends: "@cordova/eslint-config/browser" 20 | rules: 21 | no-var: 0 22 | 23 | overrides: 24 | - files: [tests/**/*.js] 25 | extends: "@cordova/eslint-config/node-tests" 26 | rules: 27 | no-var: 0 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ### Issue Type 7 | 8 | 9 | - [ ] Bug Report 10 | - [ ] Feature Request 11 | - [ ] Support Question 12 | 13 | ## Description 14 | 15 | ## Information 16 | 17 | 18 | ### Command or Code 19 | 20 | 21 | ### Environment, Platform, Device 22 | 23 | 24 | 25 | 26 | ### Version information 27 | 34 | 35 | 36 | 37 | ## Checklist 38 | 39 | 40 | - [ ] I searched for already existing GitHub issues about this 41 | - [ ] I updated all Cordova tooling to their most recent version 42 | - [ ] I included all the necessary information above 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: If something isn't working as expected. 4 | 5 | --- 6 | 7 | # Bug Report 8 | 9 | ## Problem 10 | 11 | ### What is expected to happen? 12 | 13 | 14 | 15 | ### What does actually happen? 16 | 17 | 18 | 19 | ## Information 20 | 21 | 22 | 23 | 24 | ### Command or Code 25 | 26 | 27 | 28 | 29 | ### Environment, Platform, Device 30 | 31 | 32 | 33 | 34 | ### Version information 35 | 42 | 43 | 44 | 45 | ## Checklist 46 | 47 | 48 | - [ ] I searched for existing GitHub issues 49 | - [ ] I updated all Cordova tooling to most recent version 50 | - [ ] I included all the necessary information above 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | about: A suggestion for a new functionality 4 | 5 | --- 6 | 7 | # Feature Request 8 | 9 | ## Motivation Behind Feature 10 | 11 | 12 | 13 | 14 | ## Feature Description 15 | 20 | 21 | 22 | 23 | ## Alternatives or Workarounds 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💬 Support Question 3 | about: If you have a question, please check out our Slack or StackOverflow! 4 | 5 | --- 6 | 7 | 8 | 9 | Apache Cordova uses GitHub Issues as a feature request and bug tracker _only_. 10 | For usage and support questions, please check out the resources below. Thanks! 11 | 12 | --- 13 | 14 | You can get answers to your usage and support questions about **Apache Cordova** on: 15 | 16 | * Slack Community Chat: https://cordova.slack.com (you can sign-up at http://slack.cordova.io/) 17 | * StackOverflow: https://stackoverflow.com/questions/tagged/cordova using the tag `cordova` 18 | 19 | --- 20 | 21 | If you are using a tool that uses Cordova internally, like e.g. Ionic, check their support channels: 22 | 23 | * **Ionic Framework** 24 | * [Ionic Community Forum](https://forum.ionicframework.com/) 25 | * [Ionic Worldwide Slack](https://ionicworldwide.herokuapp.com/) 26 | * **PhoneGap** 27 | * [PhoneGap Developer Community](https://forums.adobe.com/community/phonegap) 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Platforms affected 10 | 11 | 12 | 13 | ### Motivation and Context 14 | 15 | 16 | 17 | 18 | 19 | ### Description 20 | 21 | 22 | 23 | 24 | ### Testing 25 | 26 | 27 | 28 | 29 | ### Checklist 30 | 31 | - [ ] I've run the tests to see all new and existing tests pass 32 | - [ ] I added automated test coverage as appropriate for this change 33 | - [ ] Commit is prefixed with `(platform)` if this change only applies to one platform (e.g. `(android)`) 34 | - [ ] If this Pull Request resolves an issue, I linked to the issue in the text above (and used the correct [keyword to close issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/)) 35 | - [ ] I've updated the documentation if necessary 36 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Android Testsuite 19 | 20 | on: 21 | push: 22 | paths-ignore: 23 | - '**.md' 24 | - 'LICENSE' 25 | - '.eslint*' 26 | 27 | pull_request: 28 | paths-ignore: 29 | - '**.md' 30 | - 'LICENSE' 31 | - '.eslint*' 32 | 33 | jobs: 34 | test: 35 | name: Android ${{ matrix.versions.android }} Test 36 | runs-on: ubuntu-latest 37 | continue-on-error: true 38 | 39 | # hoist configurations to top that are expected to be updated 40 | env: 41 | # Storing a copy of the repo 42 | repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} 43 | 44 | node-version: 20 45 | 46 | # These are the default Java configurations used by most tests. 47 | # To customize these options, add "java-distro" or "java-version" to the strategy matrix with its overriding value. 48 | default_java-distro: temurin 49 | default_java-version: 17 50 | 51 | # These are the default Android System Image configurations used by most tests. 52 | # To customize these options, add "system-image-arch" or "system-image-target" to the strategy matrix with its overriding value. 53 | default_system-image-arch: x86_64 54 | default_system-image-target: google_apis # Most system images have a google_api option. Set this as default. 55 | 56 | # configurations for each testing strategy (test matrix) 57 | strategy: 58 | matrix: 59 | versions: 60 | - android: 7 61 | android-api: 24 62 | 63 | - android: 7.1 64 | android-api: 25 65 | 66 | - android: 8 67 | android-api: 26 68 | 69 | - android: 8.1 70 | android-api: 27 71 | system-image-arch: x86 72 | 73 | - android: 9 74 | android-api: 28 75 | 76 | - android: 10 77 | android-api: 29 78 | 79 | - android: 11 80 | android-api: 30 81 | 82 | - android: 12 83 | android-api: 31 84 | 85 | - android: 12L 86 | android-api: 32 87 | 88 | - android: 13 89 | android-api: 33 90 | 91 | - android: 14 92 | android-api: 34 93 | 94 | timeout-minutes: 60 95 | 96 | steps: 97 | - uses: actions/checkout@v4 98 | - uses: actions/setup-node@v4 99 | with: 100 | node-version: ${{ env.node-version }} 101 | - uses: actions/setup-java@v4 102 | env: 103 | java-version: ${{ matrix.versions.java-version == '' && env.default_java-version || matrix.versions.java-version }} 104 | java-distro: ${{ matrix.versions.java-distro == '' && env.default_java-distro || matrix.versions.java-distro }} 105 | with: 106 | distribution: ${{ env.java-distro }} 107 | java-version: ${{ env.java-version }} 108 | 109 | - name: Enable KVM group perms 110 | run: | 111 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 112 | sudo udevadm control --reload-rules 113 | sudo udevadm trigger --name-match=kvm 114 | 115 | - name: Run Environment Information 116 | run: | 117 | node --version 118 | npm --version 119 | java -version 120 | 121 | - name: Run npm install 122 | run: | 123 | export PATH="/usr/local/lib/android/sdk/platform-tools":$PATH 124 | export JAVA_HOME=$JAVA_HOME_11_X64 125 | npm i -g cordova@latest 126 | npm ci 127 | 128 | - name: Run paramedic install 129 | if: ${{ endswith(env.repo, '/cordova-paramedic') != true }} 130 | run: npm i -g github:apache/cordova-paramedic 131 | 132 | - uses: reactivecircus/android-emulator-runner@v2 133 | env: 134 | system-image-arch: ${{ matrix.versions.system-image-arch == '' && env.default_system-image-arch || matrix.versions.system-image-arch }} 135 | system-image-target: ${{ matrix.versions.system-image-target == '' && env.default_system-image-target || matrix.versions.system-image-target }} 136 | with: 137 | api-level: ${{ matrix.versions.android-api }} 138 | target: ${{ env.system-image-target }} 139 | arch: ${{ env.system-image-arch }} 140 | force-avd-creation: false 141 | disable-animations: false 142 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim 143 | script: echo "Pregenerate the AVD before running Paramedic" 144 | 145 | - name: Run paramedic tests 146 | uses: reactivecircus/android-emulator-runner@v2 147 | env: 148 | system-image-arch: ${{ matrix.versions.system-image-arch == '' && env.default_system-image-arch || matrix.versions.system-image-arch }} 149 | system-image-target: ${{ matrix.versions.system-image-target == '' && env.default_system-image-target || matrix.versions.system-image-target }} 150 | test_config: 'android-${{ matrix.versions.android }}.config.json' 151 | # Generally, this should automatically work for cordova-paramedic & plugins. If the path is unique, this can be manually changed. 152 | test_plugin_path: ${{ endswith(env.repo, '/cordova-paramedic') && './spec/testable-plugin/' || './' }} 153 | paramedic: ${{ endswith(env.repo, '/cordova-paramedic') && 'node main.js' || 'cordova-paramedic' }} 154 | with: 155 | api-level: ${{ matrix.versions.android-api }} 156 | target: ${{ env.system-image-target }} 157 | arch: ${{ env.system-image-arch }} 158 | force-avd-creation: false 159 | disable-animations: false 160 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim 161 | script: ${{ env.paramedic }} --config ./pr/local/${{ env.test_config }} --plugin ${{ env.test_plugin_path }} 162 | -------------------------------------------------------------------------------- /.github/workflows/chrome.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Chrome Testsuite 19 | 20 | on: 21 | push: 22 | paths-ignore: 23 | - '**.md' 24 | - 'LICENSE' 25 | - '.eslint*' 26 | pull_request: 27 | paths-ignore: 28 | - '**.md' 29 | - 'LICENSE' 30 | - '.eslint*' 31 | 32 | jobs: 33 | test: 34 | name: Chrome Latest Test 35 | runs-on: ubuntu-latest 36 | 37 | # hoist configurations to top that are expected to be updated 38 | env: 39 | # Storing a copy of the repo 40 | repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} 41 | 42 | node-version: 20 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions/setup-node@v4 47 | with: 48 | node-version: ${{ env.node-version }} 49 | 50 | - name: Run install xvfb 51 | run: sudo apt-get install xvfb 52 | 53 | - name: Run Environment Information 54 | run: | 55 | node --version 56 | npm --version 57 | 58 | - name: Run npm install 59 | run: | 60 | npm i -g cordova@latest 61 | npm ci 62 | 63 | - name: Run paramedic install 64 | if: ${{ endswith(env.repo, '/cordova-paramedic') != true }} 65 | run: npm i -g github:apache/cordova-paramedic 66 | 67 | - name: Run paramedic tests 68 | env: 69 | test_config: 'browser.config.json' 70 | # Generally, this should automatically work for cordova-paramedic & plugins. If the path is unique, this can be manually changed. 71 | test_plugin_path: ${{ endswith(env.repo, '/cordova-paramedic') && './spec/testable-plugin/' || './' }} 72 | paramedic: ${{ endswith(env.repo, '/cordova-paramedic') && 'node main.js' || 'cordova-paramedic' }} 73 | run: xvfb-run --auto-servernum ${{ env.paramedic }} --config ./pr/local/${{ env.test_config }} --plugin ${{ env.test_plugin_path }} 74 | -------------------------------------------------------------------------------- /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: iOS Testsuite 19 | 20 | on: 21 | push: 22 | paths-ignore: 23 | - '**.md' 24 | - 'LICENSE' 25 | - '.eslint*' 26 | pull_request: 27 | paths-ignore: 28 | - '**.md' 29 | - 'LICENSE' 30 | - '.eslint*' 31 | 32 | jobs: 33 | test: 34 | name: iOS ${{ matrix.versions.ios-version }} Test 35 | runs-on: ${{ matrix.versions.os-version }} 36 | continue-on-error: true 37 | 38 | # hoist configurations to top that are expected to be updated 39 | env: 40 | # Storing a copy of the repo 41 | repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} 42 | 43 | node-version: 20 44 | 45 | # > Starting April 26, 2021, all iOS and iPadOS apps submitted to the App Store must be built with Xcode 12 and the iOS 14 SDK. 46 | # Because of Apple's requirement, listed above, We will only be using the latest Xcode release for testing. 47 | # To customize these options, add "xcode-version" to the strategy matrix with its overriding value. 48 | default_xcode-version: latest-stable 49 | 50 | strategy: 51 | matrix: 52 | versions: 53 | - os-version: macos-12 54 | ios-version: 15.x 55 | xcode-version: 13.x 56 | 57 | - os-version: macos-14 58 | ios-version: 16.x 59 | xcode-version: 14.x 60 | 61 | - os-version: macos-14 62 | ios-version: 17.x 63 | xcode-version: 15.x 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: actions/setup-node@v4 68 | with: 69 | node-version: ${{ env.node-version }} 70 | - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd 71 | env: 72 | xcode-version: ${{ matrix.versions.xcode-version == '' && env.default_xcode-version || matrix.versions.xcode-version }} 73 | with: 74 | xcode-version: ${{ env.xcode-version }} 75 | 76 | - name: Run Environment Information 77 | run: | 78 | node --version 79 | npm --version 80 | xcodebuild -version 81 | 82 | - name: Run npm install 83 | run: | 84 | npm i -g cordova@latest ios-deploy@latest 85 | npm ci 86 | 87 | - name: Run paramedic install 88 | if: ${{ endswith(env.repo, '/cordova-paramedic') != true }} 89 | run: npm i -g github:apache/cordova-paramedic 90 | 91 | - name: Run paramedic tests 92 | env: 93 | test_config: 'ios-${{ matrix.versions.ios-version }}.config.json' 94 | # Generally, this should automatically work for cordova-paramedic & plugins. If the path is unique, this can be manually changed. 95 | test_plugin_path: ${{ endswith(env.repo, '/cordova-paramedic') && './spec/testable-plugin/' || './' }} 96 | paramedic: ${{ endswith(env.repo, '/cordova-paramedic') && 'node main.js' || 'cordova-paramedic' }} 97 | run: ${{ env.paramedic }} --config ./pr/local/${{ env.test_config }} --plugin ${{ env.test_plugin_path }} 98 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | name: Lint Test 19 | 20 | on: 21 | push: 22 | paths: 23 | - '**.js' 24 | - '.eslint*' 25 | - '.github/workflow/lint.yml' 26 | pull_request: 27 | paths: 28 | - '**.js' 29 | - '.eslint*' 30 | - '.github/workflow/lint.yml' 31 | 32 | jobs: 33 | test: 34 | name: Lint Test 35 | runs-on: ubuntu-latest 36 | env: 37 | node-version: 20 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-node@v4 42 | with: 43 | node-version: ${{ env.node-version }} 44 | 45 | - name: Run Environment Information 46 | run: | 47 | node --version 48 | npm --version 49 | 50 | - name: Run npm install 51 | run: | 52 | npm ci 53 | 54 | - name: Run lint test 55 | run: | 56 | npm run lint 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #If ignorance is bliss, then somebody knock the smile off my face 2 | 3 | *.csproj.user 4 | *.suo 5 | *.cache 6 | Thumbs.db 7 | *.DS_Store 8 | 9 | *.bak 10 | *.cache 11 | *.log 12 | *.swp 13 | *.user 14 | 15 | node_modules 16 | 17 | #eclipse 18 | bin 19 | .project 20 | .classpath 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | tests 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 21 | 22 | # Contributing to Apache Cordova 23 | 24 | Anyone can contribute to Cordova. And we need your contributions. 25 | 26 | There are multiple ways to contribute: report bugs, improve the docs, and 27 | contribute code. 28 | 29 | For instructions on this, start with the 30 | [contribution overview](http://cordova.apache.org/contribute/). 31 | 32 | The details are explained there, but the important items are: 33 | - Check for Github issues that corresponds to your contribution and link or create them if necessary. 34 | - Run the tests so your patch doesn't break existing functionality. 35 | 36 | We look forward to your contributions! 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache Cordova 2 | Copyright 2012 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Media 3 | description: Record and play audio on the device. 4 | --- 5 | 23 | 24 | # cordova-plugin-media 25 | 26 | [![Android Testsuite](https://github.com/apache/cordova-plugin-media/actions/workflows/android.yml/badge.svg)](https://github.com/apache/cordova-plugin-media/actions/workflows/android.yml) [![Chrome Testsuite](https://github.com/apache/cordova-plugin-media/actions/workflows/chrome.yml/badge.svg)](https://github.com/apache/cordova-plugin-media/actions/workflows/chrome.yml) [![iOS Testsuite](https://github.com/apache/cordova-plugin-media/actions/workflows/ios.yml/badge.svg)](https://github.com/apache/cordova-plugin-media/actions/workflows/ios.yml) [![Lint Test](https://github.com/apache/cordova-plugin-media/actions/workflows/lint.yml/badge.svg)](https://github.com/apache/cordova-plugin-media/actions/workflows/lint.yml) 27 | 28 | 29 | This plugin provides the ability to record and play back audio files on a device. 30 | 31 | __NOTE__: The current implementation does not adhere to a W3C 32 | specification for media capture, and is provided for convenience only. 33 | A future implementation will adhere to the latest W3C specification 34 | and may deprecate the current APIs. 35 | 36 | This plugin defines a global `Media` Constructor. 37 | 38 | Although in the global scope, it is not available until after the `deviceready` event. 39 | 40 | ```js 41 | document.addEventListener("deviceready", onDeviceReady, false); 42 | function onDeviceReady() { 43 | console.log(Media); 44 | } 45 | ``` 46 | 47 | ## Installation 48 | 49 | ```bash 50 | cordova plugin add cordova-plugin-media 51 | ``` 52 | 53 | ## Supported Platforms 54 | 55 | - Android 56 | - iOS 57 | - Browser 58 | 59 | ### Android Quirks 60 | 61 | #### SDK Target Less Than 29 62 | 63 | From the official [Storage updates in Android 11](https://developer.android.com/about/versions/11/privacy/storage) documentation, the [`WRITE_EXTERNAL_STORAGE`](https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE) permission is no longer operational and does not provide access. 64 | 65 | > If this permission is not allowlisted for an app that targets an API level before [`Build.VERSION_CODES.Q`](https://developer.android.com/reference/android/os/Build.VERSION_CODES#Q) (SDK 29) this permission cannot be granted to apps. 66 | 67 | If you need to add this permission, please add the following to your `config.xml`. 68 | 69 | ```xml 70 | 71 | 72 | 73 | ``` 74 | 75 | ## Media 76 | 77 | ```js 78 | var media = new Media(src, mediaSuccess, [mediaError], [mediaStatus]); 79 | ``` 80 | 81 | ### Parameters 82 | 83 | - __src__: A URI containing the audio content. _(DOMString)_ 84 | 85 | - __mediaSuccess__: (Optional) The callback that executes after a `Media` object has completed the current play, record, or stop action. _(Function)_ 86 | 87 | - __mediaError__: (Optional) The callback that executes if an error occurs. It takes an integer error code. _(Function)_ 88 | 89 | - __mediaStatus__: (Optional) The callback that executes to indicate status changes. It takes a integer status code. _(Function)_ 90 | 91 | - __mediaDurationUpdate__: (Optional) the callback that executes when the file's duration updates and is available. _(Function)_ 92 | 93 | __NOTE__: `cdvfile` path is supported as `src` parameter: 94 | ```javascript 95 | var my_media = new Media('cdvfile://localhost/temporary/recording.mp3', ...); 96 | ``` 97 | 98 | ### Constants 99 | 100 | The following constants are reported as the only parameter to the 101 | `mediaStatus` callback: 102 | 103 | - `Media.MEDIA_NONE` = 0; 104 | - `Media.MEDIA_STARTING` = 1; 105 | - `Media.MEDIA_RUNNING` = 2; 106 | - `Media.MEDIA_PAUSED` = 3; 107 | - `Media.MEDIA_STOPPED` = 4; 108 | 109 | ### Methods 110 | 111 | - `media.getCurrentAmplitude`: Returns the current amplitude within an audio file. 112 | 113 | - `media.getCurrentPosition`: Returns the current position within an audio file. 114 | 115 | - `media.getDuration`: Returns the duration of an audio file. 116 | 117 | - `media.play`: Start or resume playing an audio file. 118 | 119 | - `media.pause`: Pause playback of an audio file. 120 | 121 | - `media.pauseRecord`: Pause recording of an audio file. 122 | 123 | - `media.release`: Releases the underlying operating system's audio resources. 124 | 125 | - `media.resumeRecord`: Resume recording of an audio file. 126 | 127 | - `media.seekTo`: Moves the position within the audio file. 128 | 129 | - `media.setVolume`: Set the volume for audio playback. 130 | 131 | - `media.startRecord`: Start recording an audio file. 132 | 133 | - `media.stopRecord`: Stop recording an audio file. 134 | 135 | - `media.stop`: Stop playing an audio file. 136 | 137 | - `media.setRate`: Set the playback rate for the audio file. 138 | 139 | ### Additional ReadOnly Parameters 140 | 141 | - __position__: The position within the audio playback, in seconds. 142 | - Not automatically updated during play; call `getCurrentPosition` to update. 143 | 144 | - __duration__: The duration of the media, in seconds. 145 | 146 | 147 | ## media.getCurrentAmplitude 148 | 149 | Returns the current amplitude within an audio file. 150 | 151 | media.getCurrentAmplitude(mediaSuccess, [mediaError]); 152 | 153 | ### Supported Platforms 154 | 155 | - Android 156 | - iOS 157 | 158 | ### Parameters 159 | 160 | - __mediaSuccess__: The callback that is passed the current amplitude (0.0 - 1.0). 161 | 162 | - __mediaError__: (Optional) The callback to execute if an error occurs. 163 | 164 | ### Quick Example 165 | 166 | ```js 167 | // Audio player 168 | // 169 | var my_media = new Media(src, onSuccess, onError); 170 | 171 | // Record audio 172 | my_media.startRecord(); 173 | 174 | mediaTimer = setInterval(function () { 175 | // get media amplitude 176 | my_media.getCurrentAmplitude( 177 | // success callback 178 | function (amp) { 179 | console.log(amp + "%"); 180 | }, 181 | // error callback 182 | function (e) { 183 | console.log("Error getting amp=" + e); 184 | } 185 | ); 186 | }, 1000); 187 | ``` 188 | 189 | ## media.getCurrentPosition 190 | 191 | Returns the current position within an audio file. Also updates the `Media` object's `position` parameter. 192 | 193 | media.getCurrentPosition(mediaSuccess, [mediaError]); 194 | 195 | ### Parameters 196 | 197 | - __mediaSuccess__: The callback that is passed the current position in seconds. 198 | 199 | - __mediaError__: (Optional) The callback to execute if an error occurs. 200 | 201 | ### Quick Example 202 | 203 | ```js 204 | // Audio player 205 | // 206 | var my_media = new Media(src, onSuccess, onError); 207 | 208 | // Update media position every second 209 | var mediaTimer = setInterval(function () { 210 | // get media position 211 | my_media.getCurrentPosition( 212 | // success callback 213 | function (position) { 214 | if (position > -1) { 215 | console.log((position) + " sec"); 216 | } 217 | }, 218 | // error callback 219 | function (e) { 220 | console.log("Error getting pos=" + e); 221 | } 222 | ); 223 | }, 1000); 224 | ``` 225 | 226 | ## media.getDuration 227 | 228 | Returns the duration of an audio file in seconds. If the duration is unknown, it returns a value of -1. 229 | 230 | 231 | media.getDuration(); 232 | 233 | ### Quick Example 234 | 235 | ```js 236 | // Audio player 237 | // 238 | var my_media = new Media(src, onSuccess, onError); 239 | 240 | // Get duration 241 | var counter = 0; 242 | var timerDur = setInterval(function() { 243 | counter = counter + 100; 244 | if (counter > 2000) { 245 | clearInterval(timerDur); 246 | } 247 | var dur = my_media.getDuration(); 248 | if (dur > 0) { 249 | clearInterval(timerDur); 250 | document.getElementById('audio_duration').innerHTML = (dur) + " sec"; 251 | } 252 | }, 100); 253 | ``` 254 | 255 | #### Android Quirk 256 | 257 | Newer versions of Android have aggressive routines that limit background processing. If you are trying to get the duration while your app is in the background longer than roughly 5 minutes, you will get more consistent results by using the [`mediaDurationUpdate` callback of the constructor](#parameters). 258 | 259 | ## media.pause 260 | 261 | Pauses playing an audio file. 262 | 263 | media.pause(); 264 | 265 | 266 | ### Quick Example 267 | 268 | ```js 269 | // Play audio 270 | // 271 | function playAudio(url) { 272 | // Play the audio file at url 273 | var my_media = new Media(url, 274 | // success callback 275 | function () { console.log("playAudio():Audio Success"); }, 276 | // error callback 277 | function (err) { console.log("playAudio():Audio Error: " + err); } 278 | ); 279 | 280 | // Play audio 281 | my_media.play(); 282 | 283 | // Pause after 10 seconds 284 | setTimeout(function () { 285 | my_media.pause(); 286 | }, 10000); 287 | } 288 | ``` 289 | 290 | ## media.pauseRecord 291 | 292 | Pauses recording an audio file. 293 | 294 | media.pauseRecord(); 295 | 296 | 297 | ### Supported Platforms 298 | 299 | - iOS 300 | 301 | 302 | ### Quick Example 303 | 304 | ```js 305 | // Record audio 306 | // 307 | function recordAudio() { 308 | var src = "myrecording.mp3"; 309 | var mediaRec = new Media(src, 310 | // success callback 311 | function() { 312 | console.log("recordAudio():Audio Success"); 313 | }, 314 | 315 | // error callback 316 | function(err) { 317 | console.log("recordAudio():Audio Error: "+ err.code); 318 | }); 319 | 320 | // Record audio 321 | mediaRec.startRecord(); 322 | 323 | // Pause Recording after 5 seconds 324 | setTimeout(function() { 325 | mediaRec.pauseRecord(); 326 | }, 5000); 327 | } 328 | ``` 329 | 330 | ## media.play 331 | 332 | Starts or resumes playing an audio file. 333 | 334 | ```js 335 | media.play(); 336 | ``` 337 | 338 | ### Quick Example 339 | 340 | ```js 341 | // Play audio 342 | // 343 | function playAudio(url) { 344 | // Play the audio file at url 345 | var my_media = new Media(url, 346 | // success callback 347 | function () { 348 | console.log("playAudio():Audio Success"); 349 | }, 350 | // error callback 351 | function (err) { 352 | console.log("playAudio():Audio Error: " + err); 353 | } 354 | ); 355 | // Play audio 356 | my_media.play(); 357 | } 358 | ``` 359 | 360 | ### iOS Quirks 361 | 362 | - __numberOfLoops__: Pass this option to the `play` method to specify 363 | the number of times you want the media file to play, e.g.: 364 | 365 | var myMedia = new Media("http://audio.ibeat.org/content/p1rj1s/p1rj1s_-_rockGuitar.mp3") 366 | myMedia.play({ numberOfLoops: 2 }) 367 | 368 | - __playAudioWhenScreenIsLocked__: Pass in this option to the `play` 369 | method to specify whether you want to allow playback when the screen 370 | is locked. If set to `true` (the default value), the state of the 371 | hardware mute button is ignored, e.g.: 372 | 373 | var myMedia = new Media("http://audio.ibeat.org/content/p1rj1s/p1rj1s_-_rockGuitar.mp3"); 374 | myMedia.play({ playAudioWhenScreenIsLocked : true }); 375 | myMedia.setVolume('1.0'); 376 | 377 | > Note: To allow playback with the screen locked or background audio you have to add `audio` to `UIBackgroundModes` in the `info.plist` file. See [Apple documentation](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html#//apple_ref/doc/uid/TP40007072-CH4-SW23). Also note that the audio has to be started before going to background. 378 | 379 | - __order of file search__: When only a file name or simple path is 380 | provided, iOS searches in the `www` directory for the file, then in 381 | the application's `documents/tmp` directory: 382 | 383 | var myMedia = new Media("audio/beer.mp3") 384 | myMedia.play() // first looks for file in www/audio/beer.mp3 then in /documents/tmp/audio/beer.mp3 385 | 386 | ## media.release 387 | 388 | Releases the underlying operating system's audio resources. 389 | This is particularly important for Android, since there are a finite amount of 390 | OpenCore instances for media playback. Applications should call the `release` 391 | function for any `Media` resource that is no longer needed. 392 | 393 | media.release(); 394 | 395 | 396 | ### Quick Example 397 | 398 | ```js 399 | // Audio player 400 | // 401 | var my_media = new Media(src, onSuccess, onError); 402 | 403 | my_media.play(); 404 | my_media.stop(); 405 | my_media.release(); 406 | ``` 407 | 408 | ## media.resumeRecord 409 | 410 | Resume recording an audio file. 411 | 412 | media.resumeRecord(); 413 | 414 | 415 | ### Supported Platforms 416 | 417 | - iOS 418 | 419 | 420 | ### Quick Example 421 | 422 | ```js 423 | // Record audio 424 | // 425 | function recordAudio() { 426 | var src = "myrecording.mp3"; 427 | var mediaRec = new Media(src, 428 | // success callback 429 | function() { 430 | console.log("recordAudio():Audio Success"); 431 | }, 432 | 433 | // error callback 434 | function(err) { 435 | console.log("recordAudio():Audio Error: "+ err.code); 436 | }); 437 | 438 | // Record audio 439 | mediaRec.startRecord(); 440 | 441 | // Pause Recording after 5 seconds 442 | setTimeout(function() { 443 | mediaRec.pauseRecord(); 444 | }, 5000); 445 | 446 | // Resume Recording after 10 seconds 447 | setTimeout(function() { 448 | mediaRec.resumeRecord(); 449 | }, 10000); 450 | } 451 | ``` 452 | 453 | ## media.seekTo 454 | 455 | Sets the current position within an audio file. 456 | 457 | media.seekTo(milliseconds); 458 | 459 | ### Parameters 460 | 461 | - __milliseconds__: The position to set the playback position within the audio, in milliseconds. 462 | 463 | 464 | ### Quick Example 465 | 466 | ```js 467 | // Audio player 468 | // 469 | var my_media = new Media(src, onSuccess, onError); 470 | my_media.play(); 471 | // SeekTo to 10 seconds after 5 seconds 472 | setTimeout(function() { 473 | my_media.seekTo(10000); 474 | }, 5000); 475 | ``` 476 | 477 | ## media.setVolume 478 | 479 | Set the volume for an audio file. 480 | 481 | media.setVolume(volume); 482 | 483 | ### Parameters 484 | 485 | - __volume__: The volume to set for playback. The value must be within the range of 0.0 to 1.0. 486 | 487 | ### Supported Platforms 488 | 489 | - Android 490 | - iOS 491 | 492 | ### Quick Example 493 | 494 | ```js 495 | // Play audio 496 | // 497 | function playAudio(url) { 498 | // Play the audio file at url 499 | var my_media = new Media(url, 500 | // success callback 501 | function() { 502 | console.log("playAudio():Audio Success"); 503 | }, 504 | // error callback 505 | function(err) { 506 | console.log("playAudio():Audio Error: "+err); 507 | }); 508 | 509 | // Play audio 510 | my_media.play(); 511 | 512 | // Mute volume after 2 seconds 513 | setTimeout(function() { 514 | my_media.setVolume('0.0'); 515 | }, 2000); 516 | 517 | // Set volume to 1.0 after 5 seconds 518 | setTimeout(function() { 519 | my_media.setVolume('1.0'); 520 | }, 5000); 521 | } 522 | ``` 523 | 524 | ## media.startRecord 525 | 526 | Starts recording an audio file. 527 | 528 | media.startRecord(); 529 | 530 | ### Supported Platforms 531 | 532 | - Android 533 | - iOS 534 | 535 | ### Quick Example 536 | 537 | ```js 538 | // Record audio 539 | // 540 | function recordAudio() { 541 | var src = "myrecording.mp3"; 542 | var mediaRec = new Media(src, 543 | // success callback 544 | function() { 545 | console.log("recordAudio():Audio Success"); 546 | }, 547 | 548 | // error callback 549 | function(err) { 550 | console.log("recordAudio():Audio Error: "+ err.code); 551 | }); 552 | 553 | // Record audio 554 | mediaRec.startRecord(); 555 | } 556 | ``` 557 | 558 | ### Android Quirks 559 | 560 | - Android devices record audio in AAC ADTS file format. The specified file should end with a _.aac_ extension. 561 | - The hardware volume controls are wired up to the media volume while any Media objects are alive. Once the last created Media object has `release()` called on it, the volume controls revert to their default behaviour. The controls are also reset on page navigation, as this releases all Media objects. 562 | 563 | ### iOS Quirks 564 | 565 | - iOS only records to files of type _.wav_ and _.m4a_ and returns an error if the file name extension is not correct. 566 | 567 | - If a full path is not provided, the recording is placed in the application's `documents/tmp` directory. This can be accessed via the `File` API using `LocalFileSystem.TEMPORARY`. Any subdirectory specified at record time must already exist. 568 | 569 | - Files can be recorded and played back using the documents URI: 570 | 571 | var myMedia = new Media("documents://beer.mp3") 572 | 573 | - Since iOS 10 it's mandatory to provide an usage description in the `info.plist` if trying to access privacy-sensitive data. When the system prompts the user to allow access, this usage description string will displayed as part of the permission dialog box, but if you didn't provide the usage description, the app will crash before showing the dialog. Also, Apple will reject apps that access private data but don't provide an usage description. 574 | 575 | This plugins requires the following usage description: 576 | 577 | * `NSMicrophoneUsageDescription` describes the reason that the app accesses the user's microphone. 578 | 579 | To add this entry into the `info.plist`, you can use the `edit-config` tag in the `config.xml` like this: 580 | 581 | ``` 582 | 583 | need microphone access to record sounds 584 | 585 | ``` 586 | 587 | ## media.stop 588 | 589 | Stops playing an audio file. 590 | 591 | media.stop(); 592 | 593 | ### Quick Example 594 | 595 | ```js 596 | // Play audio 597 | // 598 | function playAudio(url) { 599 | // Play the audio file at url 600 | var my_media = new Media(url, 601 | // success callback 602 | function() { 603 | console.log("playAudio():Audio Success"); 604 | }, 605 | // error callback 606 | function(err) { 607 | console.log("playAudio():Audio Error: "+err); 608 | } 609 | ); 610 | 611 | // Play audio 612 | my_media.play(); 613 | 614 | // Pause after 10 seconds 615 | setTimeout(function() { 616 | my_media.stop(); 617 | }, 10000); 618 | } 619 | ``` 620 | 621 | ## media.stopRecord 622 | 623 | Stops recording an audio file. 624 | 625 | media.stopRecord(); 626 | 627 | ### Supported Platforms 628 | 629 | - Android 630 | - iOS 631 | 632 | ### Quick Example 633 | 634 | ```js 635 | // Record audio 636 | // 637 | function recordAudio() { 638 | var src = "myrecording.mp3"; 639 | var mediaRec = new Media(src, 640 | // success callback 641 | function() { 642 | console.log("recordAudio():Audio Success"); 643 | }, 644 | 645 | // error callback 646 | function(err) { 647 | console.log("recordAudio():Audio Error: "+ err.code); 648 | } 649 | ); 650 | 651 | // Record audio 652 | mediaRec.startRecord(); 653 | 654 | // Stop recording after 10 seconds 655 | setTimeout(function() { 656 | mediaRec.stopRecord(); 657 | }, 10000); 658 | } 659 | ``` 660 | 661 | ## media.setRate 662 | 663 | Stops recording an audio file. 664 | 665 | media.setRate(rate); 666 | 667 | ### Supported Platforms 668 | 669 | - iOS 670 | - Android (API 23+) 671 | 672 | ### Parameters 673 | 674 | - __rate__: The rate to set for playback. 675 | 676 | ### Quick Example 677 | 678 | ```js 679 | // Audio player 680 | // 681 | var my_media = new Media(src, onSuccess, onError); 682 | my_media.play(); 683 | 684 | // Set playback rate to 2.0x after 10 seconds 685 | setTimeout(function() { 686 | my_media.setRate(2.0); 687 | }, 5000); 688 | ``` 689 | 690 | ## MediaError 691 | 692 | A `MediaError` object is returned to the `mediaError` callback 693 | function when an error occurs. 694 | 695 | ### Properties 696 | 697 | - __code__: One of the predefined error codes listed below. 698 | 699 | - __message__: An error message describing the details of the error. 700 | 701 | ### Constants 702 | 703 | - `MediaError.MEDIA_ERR_ABORTED` = 1 704 | - `MediaError.MEDIA_ERR_NETWORK` = 2 705 | - `MediaError.MEDIA_ERR_DECODE` = 3 706 | - `MediaError.MEDIA_ERR_NONE_SUPPORTED` = 4 707 | -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | 21 | # Release Notes 22 | 23 | ### 7.0.0 (Sep 07, 2023) 24 | 25 | **Breaking Changes:** 26 | 27 | * [GH-384](https://github.com/apache/cordova-plugin-media/pull/384) fix!: remove deprecated `windows` platform 28 | * [GH-378](https://github.com/apache/cordova-plugin-media/pull/378) feat(android)!: bump file & **Android** requirements 29 | 30 | **Features:** 31 | 32 | * [GH-362](https://github.com/apache/cordova-plugin-media/pull/362) feat(ios): load media files with custom scheme+hostname and leading directory paths 33 | * [GH-383](https://github.com/apache/cordova-plugin-media/pull/383) feat(android): increase audio encoding bitrate and sampling rate 34 | * [GH-382](https://github.com/apache/cordova-plugin-media/pull/382) feat(android): support Android 13 permission checks and requests 35 | 36 | **Others:** 37 | 38 | * [GH-381](https://github.com/apache/cordova-plugin-media/pull/381) dep(dev)!: bump `@cordova/eslint-config@5.0.0` 39 | * [GH-377](https://github.com/apache/cordova-plugin-media/pull/377) ci: sync github action workflow w/ paramedic base configs 40 | 41 | ### 6.1.0 (Sep 06, 2022) 42 | 43 | * [GH-357](https://github.com/apache/cordova-plugin-media/pull/357) feat(android): add '`message`' field to media error [CB-11641](https://issues.apache.org/jira/browse/CB-11641) 44 | * [GH-352](https://github.com/apache/cordova-plugin-media/pull/352) feat(ios): support `file` scheme 45 | * [GH-354](https://github.com/apache/cordova-plugin-media/pull/354) fix(ios): Reset default audio session category when release ([CB-13243](https://issues.apache.org/jira/browse/CB-13243)) 46 | * [GH-353](https://github.com/apache/cordova-plugin-media/pull/353) fix(ios): error on `getPosition` when time is `nan` 47 | * [GH-355](https://github.com/apache/cordova-plugin-media/pull/355) test(spec.22): pause media before calling `getCurrentPosition` 48 | * [GH-356](https://github.com/apache/cordova-plugin-media/pull/356) test(spec.timeout): try to improve against timeout failures 49 | * [GH-351](https://github.com/apache/cordova-plugin-media/pull/351) ci: sync workflow with paramedic 50 | * [GH-349](https://github.com/apache/cordova-plugin-media/pull/349) ci(android): update java requirement for `cordova-android@11` 51 | 52 | ### 6.0.0 (May 25, 2022) 53 | 54 | * [GH-344](https://github.com/apache/cordova-plugin-media/pull/344) feat(android): drop `WRITE_EXTERNAL_STORAGE` permission 55 | * [GH-195](https://github.com/apache/cordova-plugin-media/pull/195) feat(ios): Add error call for `stalled_playback` 56 | * [GH-341](https://github.com/apache/cordova-plugin-media/pull/341) feat(android): add `setRate` 57 | * [GH-340](https://github.com/apache/cordova-plugin-media/pull/340) fix(ios): set rate with current playback rate 58 | * [GH-197](https://github.com/apache/cordova-plugin-media/pull/197) feat: add `durationUpdate` callback 59 | * [GH-232](https://github.com/apache/cordova-plugin-media/pull/232) fix(android): remove `READ_PHONE_STATE` permission 60 | * [GH-285](https://github.com/apache/cordova-plugin-media/pull/285) fix: remove deprecated platform code snippets 61 | * [GH-338](https://github.com/apache/cordova-plugin-media/pull/338) fix: missing parenthesis from #328 62 | * [GH-328](https://github.com/apache/cordova-plugin-media/pull/328) fix(android): issue #325 63 | * [GH-334](https://github.com/apache/cordova-plugin-media/pull/334) dep!: bump `cordova-plugin-file@^7.0.0` 64 | * [GH-337](https://github.com/apache/cordova-plugin-media/pull/337) chore: bump `cordovaDependencies` next next major cordova requirement 65 | * [GH-336](https://github.com/apache/cordova-plugin-media/pull/336) chore: rebuilt `package-lock` 66 | 67 | ### 5.0.4 (Jan 21, 2022) 68 | 69 | * [GH-329](https://github.com/apache/cordova-plugin-media/pull/329) dep: bump `@cordova/eslint-config@4.0.0` w/ fix & `package-lock` rebuild 70 | * [GH-317](https://github.com/apache/cordova-plugin-media/pull/317) fix(android): get external files directory for **Android** 10+ 71 | * [GH-320](https://github.com/apache/cordova-plugin-media/pull/320) ci(ios): update workflow w/ **iOS** 15 72 | * [GH-318](https://github.com/apache/cordova-plugin-media/pull/318) test(browser): disable test cases w/ play() due to Chrome's Autoplay Policy 73 | * [GH-313](https://github.com/apache/cordova-plugin-media/pull/313) ci: add action-badge 74 | * [GH-312](https://github.com/apache/cordova-plugin-media/pull/312) ci: remove travis & appveyor 75 | * [GH-311](https://github.com/apache/cordova-plugin-media/pull/311) ci: add gh-actions workflows 76 | * [GH-298](https://github.com/apache/cordova-plugin-media/pull/298) ci: add node-14.x to workflow 77 | * [GH-292](https://github.com/apache/cordova-plugin-media/pull/292) ci(travis): update osx xcode image 78 | * [GH-291](https://github.com/apache/cordova-plugin-media/pull/291) ci(travis): updates **Android** API level 79 | * [GH-284](https://github.com/apache/cordova-plugin-media/pull/284) chore: adds `package-lock` file 80 | * [GH-283](https://github.com/apache/cordova-plugin-media/pull/283) refactor(eslint): use `cordova-eslint` /w fix 81 | * [GH-282](https://github.com/apache/cordova-plugin-media/pull/282) chore(npm): use short notation in `package.json` 82 | * chore(asf): update git notification settings 83 | * Update CONTRIBUTING.md 84 | * [GH-249](https://github.com/apache/cordova-plugin-media/pull/249) Fix #248 delete javascript reference to released media 85 | * [GH-274](https://github.com/apache/cordova-plugin-media/pull/274) ci: updates Node.js versions 86 | * [GH-275](https://github.com/apache/cordova-plugin-media/pull/275) chore(npm): improve ignore list 87 | * ci(travis): upgrade to node8 88 | * [GH-241](https://github.com/apache/cordova-plugin-media/pull/241) fix(types): Add type definition for getCurrentAmplitude 89 | 90 | ### 5.0.3 (Jun 27, 2019) 91 | 92 | - chore: fix repo and issue urls and license in package.json and plugin.xml ([`784ac7b`](https://github.com/apache/cordova-plugin-media/commit/784ac7b)) 93 | - build: add `.gitattributes` to force LF (instead of possible CRLF on Windows) ([`5244c4a`](https://github.com/apache/cordova-plugin-media/commit/5244c4a)) 94 | - build: add `.npmignore` to remove unneeded files from npm package ([`aa1586d`](https://github.com/apache/cordova-plugin-media/commit/aa1586d)) 95 | - ci(travis): Update Travis CI configuration for new paramedic ([#227](https://github.com/apache/cordova-plugin-media/issues/227)) ([`b0ed6bd`](https://github.com/apache/cordova-plugin-media/commit/b0ed6bd)) 96 | - chore(github): Add or update GitHub pull request and issue template ([`b1c1353`](https://github.com/apache/cordova-plugin-media/commit/b1c1353)) 97 | - docs: remove JIRA link ([`2acd3c2`](https://github.com/apache/cordova-plugin-media/commit/2acd3c2)) 98 | - ci(travis): also accept terms for android sdk `android-27` ([`74772c3`](https://github.com/apache/cordova-plugin-media/commit/74772c3)) 99 | - docs: remove outdated docs that haven't been updated for 3 years ([`a006da3`](https://github.com/apache/cordova-plugin-media/commit/a006da3)) 100 | - fix(types): add types for callback in constructor ([#90](https://github.com/apache/cordova-plugin-media/issues/90)) ([`7094582`](https://github.com/apache/cordova-plugin-media/commit/7094582)) 101 | - docs: (iOS) document setRate method ([#142](https://github.com/apache/cordova-plugin-media/issues/142)) ([`5f18902`](https://github.com/apache/cordova-plugin-media/commit/5f18902)) 102 | - fix(ios): [CB-13445](https://issues.apache.org/jira/browse/CB-13445) (iOS) Streaming media can take up to 8-10 seconds to start ([#169](https://github.com/apache/cordova-plugin-media/issues/169)) ([`43d57ca`](https://github.com/apache/cordova-plugin-media/commit/43d57ca)) 103 | - fix(android): [CB-12849](https://issues.apache.org/jira/browse/CB-12849) checking mediaState in destroy method, and moving file by stream when renameTo failing ([#168](https://github.com/apache/cordova-plugin-media/issues/168)) ([`86660dd`](https://github.com/apache/cordova-plugin-media/commit/86660dd)) 104 | - tests: [CB-14091](https://issues.apache.org/jira/browse/CB-14091) fix tests code for stream url and remove browser ([#166](https://github.com/apache/cordova-plugin-media/issues/166)) ([`524c337`](https://github.com/apache/cordova-plugin-media/commit/524c337)) 105 | 106 | 107 | ### 5.0.2 (Jan 24, 2018) 108 | * [CB-13751](https://issues.apache.org/jira/browse/CB-13751) Add build-tools-26.0.2 to travis (#163) 109 | * Fix for [CB-11513](https://issues.apache.org/jira/browse/CB-11513) 110 | * [CB-7684](https://issues.apache.org/jira/browse/CB-7684) (#143) 111 | 112 | ### 5.0.1 (Dec 27, 2017) 113 | * [CB-13706](https://issues.apache.org/jira/browse/CB-13706) Fix to allow 5.0.0 version install (#160) 114 | * Bump cordova-plugin-file dependency to 6.0.0 115 | 116 | ### 5.0.0 (Dec 15, 2017) 117 | * [CB-13678](https://issues.apache.org/jira/browse/CB-13678) Remove deprecated platforms 118 | 119 | ### 4.0.0 (Nov 06, 2017) 120 | * [CB-12264](https://issues.apache.org/jira/browse/CB-12264) (README): fix `media.getCurrentAmplitude` definition 121 | * [CB-13265](https://issues.apache.org/jira/browse/CB-13265) Remove **iOS** usage description from media plugin 122 | * [CB-13517](https://issues.apache.org/jira/browse/CB-13517) (all): Add 'protective' entry to `cordovaDependencies` 123 | * [CB-13473](https://issues.apache.org/jira/browse/CB-13473) (CI) Removed **Browser** builds from AppVeyor 124 | * [CB-13294](https://issues.apache.org/jira/browse/CB-13294) Remove `cordova-plugin-compat` 125 | * [CB-13299](https://issues.apache.org/jira/browse/CB-13299) (CI) Fix **Android** builds 126 | * [CB-13028](https://issues.apache.org/jira/browse/CB-13028) (CI) **Browser** builds on Travis and AppVeyor 127 | * [CB-12671](https://issues.apache.org/jira/browse/CB-12671) **iOS**: Fix auto-test with stopping media that is in starting state 128 | * [CB-12847](https://issues.apache.org/jira/browse/CB-12847) added `bugs` entry to `package.json`. 129 | 130 | ### 3.0.1 (Apr 27, 2017) 131 | * [CB-12542](https://issues.apache.org/jira/browse/CB-12542) (ios) Fix wav file recording, add m4a extension. make **iOS** status handling compatible with **Android**/Windows 132 | * [CB-12622](https://issues.apache.org/jira/browse/CB-12622) Added **Android 6.0** build badges to `README` 133 | * [CB-12685](https://issues.apache.org/jira/browse/CB-12685) added `package.json` to tests folder 134 | 135 | ### 3.0.0 (Feb 28, 2017) 136 | * **Android:** fix `NullPointerException` in `AudioPlayer.readyPlayer` 137 | * **Android:** fix `java.lang.NullPointerException` on `resumeAllGainedFocus` 138 | * [CB-12493](https://issues.apache.org/jira/browse/CB-12493) (Tests) Fixed spec.21 flakyness 139 | * major version bump, added `cordovaDependencies` requirement for `cordova-android>=6.1.0` 140 | * Add engine tag for checking `cordova-android` 141 | * Make the output file of **Android** an `acc` file. 142 | * [CB-12422](https://issues.apache.org/jira/browse/CB-12422) **iOS:** Fix readme issue on background needed plist modification 143 | * [CB-12434](https://issues.apache.org/jira/browse/CB-12434) **Android:** fix Stoping a Paused Recording throws exception 144 | * [CB-12411](https://issues.apache.org/jira/browse/CB-12411) Stoping a Paused Recording throws illegal state exception 145 | * [CB-1187](https://issues.apache.org/jira/browse/CB-1187) **iOS:** Fix unused recording settings 146 | * [CB-12353](https://issues.apache.org/jira/browse/CB-12353) Corrected merges usage in `plugin.xml` 147 | * [CB-12369](https://issues.apache.org/jira/browse/CB-12369) Add plugin typings from DefinitelyTyped 148 | * [CB-12363](https://issues.apache.org/jira/browse/CB-12363) Added build badges for **iOS 9.3** and **iOS 10.0** 149 | * [CB-12230](https://issues.apache.org/jira/browse/CB-12230) Removed **Windows 8.1** build badges 150 | 151 | ### 2.4.1 (Dec 07, 2016) 152 | * [CB-12224](https://issues.apache.org/jira/browse/CB-12224) Updated version and RELEASENOTES.md for release 2.4.1 153 | * [CB-12034](https://issues.apache.org/jira/browse/CB-12034) (ios) Add mandatory iOS 10 privacy description 154 | * [CB-11917](https://issues.apache.org/jira/browse/CB-11917) - Remove pull request template checklist item: "iCLA has been submitted…" 155 | * [CB-11529](https://issues.apache.org/jira/browse/CB-11529) ios: Make available setting volume for player on ios device 156 | * [CB-11832](https://issues.apache.org/jira/browse/CB-11832) Incremented plugin version. 157 | 158 | ### 2.4.0 (Sep 08, 2016) 159 | * [CB-11795](https://issues.apache.org/jira/browse/CB-11795) Add 'protective' entry to cordovaDependencies 160 | * [CB-11793](https://issues.apache.org/jira/browse/CB-11793) fixed **android** build issue with last commit 161 | * [CB-11085](https://issues.apache.org/jira/browse/CB-11085) Fix error output using `println` to `LOG.e` 162 | * [CB-11757](https://issues.apache.org/jira/browse/CB-11757) (**ios**) Error out if trying to stop playback while in a wrong state 163 | * [CB-11380](https://issues.apache.org/jira/browse/CB-11380) (**ios**) Overloaded `audioFileForResource` method instead of modifying its signature 164 | * [CB-11380](https://issues.apache.org/jira/browse/CB-11380) (**ios**) Updated modified method signature in the .h file 165 | * [CB-11380](https://issues.apache.org/jira/browse/CB-11380) (**ios**) Fixed an unexpected error callback when initializing Media with file that doesn't exist 166 | * [CB-10849](https://issues.apache.org/jira/browse/CB-10849) (ios) Fixed a crash when playing soundfiles consecutively 167 | * [CB-11754](https://issues.apache.org/jira/browse/CB-11754) (**Android**) Fixed the build error 168 | * [CB-11086](https://issues.apache.org/jira/browse/CB-11086) (**Android**) Fixed a crash when `setVolume()` is called on unitialized audio This closes #93 169 | * Plugin uses `Android Log class` and not `Cordova LOG class` 170 | * [CB-11655](https://issues.apache.org/jira/browse/CB-11655) (**Android**) Enabled asynchronous error handling 171 | * [CB-11430](https://issues.apache.org/jira/browse/CB-11430) Report duration NaN value to JS properly 172 | * [CB-11429](https://issues.apache.org/jira/browse/CB-11429) Update test stream URL 173 | * [CB-11430](https://issues.apache.org/jira/browse/CB-11430) Skip audio playback tests on Saucelabs 174 | * [CB-11458](https://issues.apache.org/jira/browse/CB-11458) - `media.spec.25` 'should be able to play an audio stream' fails on **iOS** platform 175 | * Add badges for paramedic builds on Jenkins 176 | * [CB-11313](https://issues.apache.org/jira/browse/CB-11313) Can't start media streaming on **Android 6.0** 177 | * Add pull request template. 178 | * Readme: Add fenced code blocks with langauage hints 179 | * [CB-11165](https://issues.apache.org/jira/browse/CB-11165) removed peer dependency 180 | * [CB-10776](https://issues.apache.org/jira/browse/CB-10776) Add the ability to pause and resume an audio recording (**Android**) 181 | * [CB-10776](https://issues.apache.org/jira/browse/CB-10776) Add the ability to pause and resume an audio recording (**iOS**) 182 | * [CB-9487](https://issues.apache.org/jira/browse/CB-9487) Don't update position when getting amplitude 183 | * [CB-10996](https://issues.apache.org/jira/browse/CB-10996) Adding front matter to README.md 184 | 185 | ### 2.3.0 (Apr 15, 2016) 186 | * Request audio focus when playing; Pause audio when audio focus is lost; resume playing when audio focus is granted again. 187 | * Replace `PermissionHelper.java` with `cordova-plugin-compat` 188 | * [CB-10783](https://issues.apache.org/jira/browse/CB-10783) Modify expected position to be in a proper range. 189 | * [CB-9487](https://issues.apache.org/jira/browse/CB-9487) Support getting amplitude for recording 190 | * **iOS** audio should handle naked local file sources 191 | * [CB-10720](https://issues.apache.org/jira/browse/CB-10720) Fixing README for display on Cordova website 192 | * [CB-10636](https://issues.apache.org/jira/browse/CB-10636) Add `JSHint` for plugins 193 | * [CB-10535](https://issues.apache.org/jira/browse/CB-10535) Fix CI crash caused by media plugin 194 | 195 | ### 2.2.0 (Feb 09, 2016) 196 | * [CB-10476](https://issues.apache.org/jira/browse/CB-10476) Fix problem where callbacks were not invoked on android due to messageChannel being overridden by callbackContext in every execute() call 197 | * Edit package.json license to match SPDX id 198 | * [CB-10455](https://issues.apache.org/jira/browse/CB-10455) android: Adding permission helper to remove cordova-android 5.0.0 constraint 199 | * [CB-57](https://issues.apache.org/jira/browse/CB-57) Updated to use avplayer when url starts with http:// or https:// for full streaming support 200 | * [CB-8222](https://issues.apache.org/jira/browse/CB-8222) Background thread on play to prevent locking during initial load of media 201 | 202 | ### 2.1.0 (Jan 15, 2016) 203 | * Fixed example referencing non-existent variable 204 | * [CB-9452](https://issues.apache.org/jira/browse/CB-9452) Treat `RTSP streams` as `remote URLs` 205 | * add JIRA issue tracker link 206 | * fix [CB-9884](https://issues.apache.org/jira/browse/CB-9884) & [CB-9885](https://issues.apache.org/jira/browse/CB-9885) 207 | * [CB-10100](https://issues.apache.org/jira/browse/CB-10100) updated file dependency to not grab new majors 208 | * Fix block usage of self 209 | 210 | ### 2.0.0 (Nov 18, 2015) 211 | * [CB-10035](https://issues.apache.org/jira/browse/CB-10035) Updated `RELEASENOTES` to be newest to oldest 212 | * Media now supports new permissions for **Android 6.0** aka **Marshmallow** 213 | * Fixing contribute link. 214 | * [CB-9619](https://issues.apache.org/jira/browse/CB-9619) Fixed tests waiting for precise position 215 | * [CB-9606](https://issues.apache.org/jira/browse/CB-9606) Fixes arguments parsing in `seekAudio` 216 | * [CB-9605](https://issues.apache.org/jira/browse/CB-9605) Fixes issue with playback resume after pause on **WP8** 217 | * fix record and play `NullPointerException` 218 | * [CB-9237](https://issues.apache.org/jira/browse/CB-9237) Add `cdvfile://` support to media plugin on **Windows** platform 219 | * [CB-9238](https://issues.apache.org/jira/browse/CB-9238) Media plugin cannot record audio on **Windows** 220 | * Added **iOS** platform `media.setRate` auto test 221 | * Add **iOS** platform check in `Media.prototype.setRate` 222 | * Add `Media.prototype.setRate` method (only for **iOS**) 223 | 224 | ### 1.0.1 (Jun 17, 2015) 225 | * [CB-9128](https://issues.apache.org/jira/browse/CB-9128) cordova-plugin-media documentation translation: cordova-plugin-media 226 | * fix npm md issue 227 | * [CB-9079](https://issues.apache.org/jira/browse/CB-9079) Increased timeout for playback tests 228 | * [CB-8888](https://issues.apache.org/jira/browse/CB-8888) Makes media status reporting on windows more precise 229 | * [CB-8793](https://issues.apache.org/jira/browse/CB-8793) Increased playback timeout in tests 230 | 231 | ### 1.0.0 (Apr 15, 2015) 232 | * [CB-8793](https://issues.apache.org/jira/browse/CB-8793) Fixed tests to pass on wp8 and windows 233 | * [CB-8746](https://issues.apache.org/jira/browse/CB-8746) bumped version of file dependency 234 | * [CB-8746](https://issues.apache.org/jira/browse/CB-8746) gave plugin major version bump 235 | * [CB-8779](https://issues.apache.org/jira/browse/CB-8779) Fixed media status reporting on wp8 236 | * [CB-8747](https://issues.apache.org/jira/browse/CB-8747) added missing comma 237 | * [CB-8747](https://issues.apache.org/jira/browse/CB-8747) updated dependency, added peer dependency 238 | * [CB-8683](https://issues.apache.org/jira/browse/CB-8683) changed plugin-id to pacakge-name 239 | * [CB-8653](https://issues.apache.org/jira/browse/CB-8653) properly updated translated docs to use new id 240 | * [CB-8653](https://issues.apache.org/jira/browse/CB-8653) updated translated docs to use new id 241 | * [CB-8541](https://issues.apache.org/jira/browse/CB-8541) Adds information about available recording formats on Windows 242 | * Use TRAVIS_BUILD_DIR, install paramedic by npm 243 | * [CB-8686](https://issues.apache.org/jira/browse/CB-8686) - remove musicLibrary capability 244 | * [CB-7962](https://issues.apache.org/jira/browse/CB-7962) Adds browser platform support 245 | * [CB-8653](https://issues.apache.org/jira/browse/CB-8653) Updated Readme 246 | * [CB-8659](https://issues.apache.org/jira/browse/CB-8659): ios: 4.0.x Compatibility: Remove use of deprecated headers 247 | * [CB-8572](https://issues.apache.org/jira/browse/CB-8572) Integrate TravisCI 248 | * [CB-8438](https://issues.apache.org/jira/browse/CB-8438) cordova-plugin-media documentation translation: cordova-plugin-media 249 | * [CB-8538](https://issues.apache.org/jira/browse/CB-8538) Added package.json file 250 | * [CB-8428](https://issues.apache.org/jira/browse/CB-8428) Fix tests on Windows if no audio playback hardware is available 251 | * [CB-8428](https://issues.apache.org/jira/browse/CB-8428) Fix multiple `done()` calls in media plugin test on devices where audio is not configured 252 | * [CB-8426](https://issues.apache.org/jira/browse/CB-8426) Add Windows platform section to Media plugin 253 | * [CB-8425](https://issues.apache.org/jira/browse/CB-8425) Media plugin .ctr: make src param required as per spec 254 | 255 | ### 0.2.16 (Feb 04, 2015) 256 | * [CB-8351](https://issues.apache.org/jira/browse/CB-8351) ios: Stop using (newly) deprecated CDVJSON.h 257 | * [CB-8351](https://issues.apache.org/jira/browse/CB-8351) ios: Use argumentForIndex rather than NSArray extension 258 | * [CB-8252](https://issues.apache.org/jira/browse/CB-8252) android: Fire audio events from native via message channel 259 | * [CB-8152](https://issues.apache.org/jira/browse/CB-8152) ios: Remove deprecated methods in Media plugin (deprecated since 2.5) 260 | 261 | ### 0.2.15 (Dec 02, 2014) 262 | * [CB-6153](https://issues.apache.org/jira/browse/CB-6153) **Android**: Add docs for volume control behaviour, and fix controls not being reset on page navigation 263 | * [CB-6153](https://issues.apache.org/jira/browse/CB-6153) **Android**: Make volume buttons control music stream while any audio players are created 264 | * [CB-7977](https://issues.apache.org/jira/browse/CB-7977) Mention `deviceready` in plugin docs 265 | * [CB-7945](https://issues.apache.org/jira/browse/CB-7945) Made media.spec.15 and media.spec.16 auto tests green 266 | * [CB-7700](https://issues.apache.org/jira/browse/CB-7700) cordova-plugin-media documentation translation: cordova-plugin-media 267 | 268 | ### 0.2.14 (Oct 03, 2014) 269 | * Amazon Specific changes: Added READ_PHONE_STATE permission same as done in Android 270 | * make possible plays wav file 271 | * [CB-7638](https://issues.apache.org/jira/browse/CB-7638) Get audio duration properly on windows 272 | * [CB-7454](https://issues.apache.org/jira/browse/CB-7454) Adds support for m4a audio format for Windows 273 | * [CB-7547](https://issues.apache.org/jira/browse/CB-7547) Fixes audio recording on windows platform 274 | * [CB-7531](https://issues.apache.org/jira/browse/CB-7531) Fixes play() failure after release() call 275 | 276 | ### 0.2.13 (Sep 17, 2014) 277 | * [CB-6963](https://issues.apache.org/jira/browse/CB-6963) renamed folder to tests + added nested plugin.xml 278 | * added documentation for manual tests 279 | * [CB-6963](https://issues.apache.org/jira/browse/CB-6963) Port Media manual & automated tests 280 | * [CB-6963](https://issues.apache.org/jira/browse/CB-6963) Port media tests to plugin-test-framework 281 | 282 | ### 0.2.12 (Aug 06, 2014) 283 | * [CB-6127](https://issues.apache.org/jira/browse/CB-6127) Updated translations for docs 284 | * ios: Make it easier to play media and record audio simultaneously 285 | * code #s for MediaError 286 | 287 | ### 0.2.11 (Jun 05, 2014) 288 | * [CB-6127](https://issues.apache.org/jira/browse/CB-6127) Spanish and French Translations added. Github close #13 289 | * [CB-6807](https://issues.apache.org/jira/browse/CB-6807) Add license 290 | * [CB-6706](https://issues.apache.org/jira/browse/CB-6706): Relax dependency on file plugin 291 | * [CB-6478](https://issues.apache.org/jira/browse/CB-6478): Fix exception when try to record audio file on windows 8 292 | * [CB-6477](https://issues.apache.org/jira/browse/CB-6477): Add musicLibrary and microphone capabilities to windows 8 platform 293 | * [CB-6491](https://issues.apache.org/jira/browse/CB-6491) add CONTRIBUTING.md 294 | 295 | ### 0.2.10 (Apr 17, 2014) 296 | * [CB-6422](https://issues.apache.org/jira/browse/CB-6422): [windows8] use cordova/exec/proxy 297 | * [CB-6212](https://issues.apache.org/jira/browse/CB-6212): [iOS] fix warnings compiled under arm64 64-bit 298 | * [CB-6225](https://issues.apache.org/jira/browse/CB-6225): Specify plugin dependency on File plugin 1.0.1 299 | * [CB-6460](https://issues.apache.org/jira/browse/CB-6460): Update license headers 300 | * [CB-6465](https://issues.apache.org/jira/browse/CB-6465): Add license headers to Tizen code 301 | * Add NOTICE file 302 | 303 | ### 0.2.9 (Feb 26, 2014) 304 | * [CB-6051](https://issues.apache.org/jira/browse/CB-6051) Update media plugin to work with new cdvfile:// urls 305 | * [CB-5748](https://issues.apache.org/jira/browse/CB-5748) Make sure that Media.onStatus is called when recording is started. 306 | 307 | ### 0.2.8 (Feb 05, 2014) 308 | * Add preliminary support for Tizen. 309 | * [CB-4755](https://issues.apache.org/jira/browse/CB-4755) Fix crash in Media.setVolume on iOS 310 | 311 | ### 0.2.7 (Jan 02, 2014) 312 | * [CB-5658](https://issues.apache.org/jira/browse/CB-5658) Add doc/index.md for Media plugin 313 | * Adding READ_PHONE_STATE to the plugin permissions 314 | 315 | ### 0.2.6 (Dec 4, 2013) 316 | * [ubuntu] specify policy_group 317 | * add ubuntu platform 318 | * Added amazon-fireos platform. Change to use amazon-fireos as a platform if the user agent string contains 'cordova-amazon-fireos' 319 | 320 | ### 0.2.5 (Oct 28, 2013) 321 | * [CB-5128](https://issues.apache.org/jira/browse/CB-5128): add repo + issue tag to plugin.xml for media plugin 322 | * [CB-5010](https://issues.apache.org/jira/browse/CB-5010) Incremented plugin version on dev branch. 323 | 324 | ### 0.2.4 (Oct 9, 2013) 325 | * [CB-4928](https://issues.apache.org/jira/browse/CB-4928) plugin-media doesn't load on windows8 326 | * [CB-4915](https://issues.apache.org/jira/browse/CB-4915) Incremented plugin version on dev branch. 327 | 328 | ### 0.2.3 (Sept 25, 2013) 329 | * [CB-4889](https://issues.apache.org/jira/browse/CB-4889) bumping&resetting version 330 | * [windows8] commandProxy was moved 331 | * [CB-4889](https://issues.apache.org/jira/browse/CB-4889) renaming references 332 | * [CB-4889](https://issues.apache.org/jira/browse/CB-4889) renaming org.apache.cordova.core.media to org.apache.cordova.media 333 | * [CB-4847](https://issues.apache.org/jira/browse/CB-4847) iOS 7 microphone access requires user permission - if denied, CDVCapture, CDVSound does not handle it properly 334 | * Rename CHANGELOG.md -> RELEASENOTES.md 335 | * [CB-4799](https://issues.apache.org/jira/browse/CB-4799) Fix incorrect JS references within native code on Android & iOS 336 | * Fix compiler/lint warnings 337 | * Rename plugin id from AudioHandler -> media 338 | * [CB-4763](https://issues.apache.org/jira/browse/CB-4763) Remove reference to cordova-android's FileHelper. 339 | * [CB-4752](https://issues.apache.org/jira/browse/CB-4752) Incremented plugin version on dev branch. 340 | 341 | ### 0.2.1 (Sept 5, 2013) 342 | * [CB-4432](https://issues.apache.org/jira/browse/CB-4432) copyright notice change 343 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-media", 3 | "version": "7.0.1-dev", 4 | "description": "Cordova Media Plugin", 5 | "types": "./types/index.d.ts", 6 | "cordova": { 7 | "id": "cordova-plugin-media", 8 | "platforms": [ 9 | "android", 10 | "browser", 11 | "ios" 12 | ] 13 | }, 14 | "repository": "github:apache/cordova-plugin-media", 15 | "bugs": "https://github.com/apache/cordova-plugin-media/issues", 16 | "keywords": [ 17 | "cordova", 18 | "media", 19 | "ecosystem:cordova", 20 | "cordova-android", 21 | "cordova-browser", 22 | "cordova-ios" 23 | ], 24 | "scripts": { 25 | "test": "npm run lint", 26 | "lint": "eslint ." 27 | }, 28 | "author": "Apache Software Foundation", 29 | "license": "Apache-2.0", 30 | "engines": { 31 | "cordovaDependencies": { 32 | "3.0.0": { 33 | "cordova-android": ">=6.1.0" 34 | }, 35 | "4.0.0": { 36 | "cordova-android": ">=6.3.0" 37 | }, 38 | "6.0.0": { 39 | "cordova-android": ">=10.0.0" 40 | }, 41 | "7.0.0": { 42 | "cordova-android": ">=12.0.0" 43 | }, 44 | "8.0.0": { 45 | "cordova": ">100" 46 | } 47 | } 48 | }, 49 | "devDependencies": { 50 | "@cordova/eslint-config": "^5.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | 26 | Media 27 | Cordova Media Plugin 28 | Apache 2.0 29 | cordova,media 30 | https://github.com/apache/cordova-plugin-media 31 | https://github.com/apache/cordova-plugin-media/issues 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/android/AudioHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | package org.apache.cordova.media; 20 | 21 | import org.apache.cordova.CallbackContext; 22 | import org.apache.cordova.CordovaPlugin; 23 | import org.apache.cordova.CordovaResourceApi; 24 | import org.apache.cordova.PermissionHelper; 25 | 26 | import android.Manifest; 27 | import android.content.Context; 28 | import android.content.pm.PackageManager; 29 | import android.media.AudioManager; 30 | import android.media.AudioManager.OnAudioFocusChangeListener; 31 | import android.net.Uri; 32 | import android.os.Build; 33 | 34 | import java.util.ArrayList; 35 | 36 | import org.apache.cordova.LOG; 37 | import org.apache.cordova.PluginResult; 38 | import org.json.JSONArray; 39 | import org.json.JSONException; 40 | import org.json.JSONObject; 41 | 42 | import java.util.HashMap; 43 | 44 | /** 45 | * This class called by CordovaActivity to play and record audio. 46 | * The file can be local or over a network using http. 47 | * 48 | * Audio formats supported (tested): 49 | * .mp3, .wav 50 | * 51 | * Local audio files must reside in one of two places: 52 | * android_asset: file name must start with /android_asset/sound.mp3 53 | * sdcard: file name is just sound.mp3 54 | */ 55 | public class AudioHandler extends CordovaPlugin { 56 | 57 | public static String TAG = "AudioHandler"; 58 | HashMap players; // Audio player object 59 | ArrayList pausedForPhone; // Audio players that were paused when phone call came in 60 | ArrayList pausedForFocus; // Audio players that were paused when focus was lost 61 | private int origVolumeStream = -1; 62 | private CallbackContext messageChannel; 63 | 64 | // Permission Request Codes 65 | public static int RECORD_AUDIO = 0; 66 | public static int WRITE_EXTERNAL_STORAGE = 1; 67 | 68 | public static final int PERMISSION_DENIED_ERROR = 20; 69 | 70 | private String recordId; 71 | private String fileUriStr; 72 | 73 | /** 74 | * Constructor. 75 | */ 76 | public AudioHandler() { 77 | this.players = new HashMap(); 78 | this.pausedForPhone = new ArrayList(); 79 | this.pausedForFocus = new ArrayList(); 80 | } 81 | 82 | public Context getApplicationContext() { 83 | return cordova.getActivity().getApplicationContext(); 84 | } 85 | 86 | 87 | /** 88 | * Executes the request and returns PluginResult. 89 | * @param action The action to execute. 90 | * @param args JSONArry of arguments for the plugin. 91 | * @param callbackContext The callback context used when calling back into JavaScript. 92 | * @return A PluginResult object with a status and message. 93 | */ 94 | public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { 95 | CordovaResourceApi resourceApi = webView.getResourceApi(); 96 | PluginResult.Status status = PluginResult.Status.OK; 97 | String result = ""; 98 | 99 | if (action.equals("startRecordingAudio")) { 100 | recordId = args.getString(0); 101 | String target = args.getString(1); 102 | try { 103 | Uri targetUri = resourceApi.remapUri(Uri.parse(target)); 104 | fileUriStr = targetUri.toString(); 105 | } catch (IllegalArgumentException e) { 106 | fileUriStr = target; 107 | } 108 | promptForRecord(); 109 | } 110 | else if (action.equals("stopRecordingAudio")) { 111 | this.stopRecordingAudio(args.getString(0), true); 112 | } 113 | else if (action.equals("pauseRecordingAudio")) { 114 | this.stopRecordingAudio(args.getString(0), false); 115 | } 116 | else if (action.equals("resumeRecordingAudio")) { 117 | this.resumeRecordingAudio(args.getString(0)); 118 | } 119 | else if (action.equals("startPlayingAudio")) { 120 | String target = args.getString(1); 121 | String fileUriStr; 122 | try { 123 | Uri targetUri = resourceApi.remapUri(Uri.parse(target)); 124 | fileUriStr = targetUri.toString(); 125 | } catch (IllegalArgumentException e) { 126 | fileUriStr = target; 127 | } 128 | this.startPlayingAudio(args.getString(0), FileHelper.stripFileProtocol(fileUriStr)); 129 | } 130 | else if (action.equals("seekToAudio")) { 131 | this.seekToAudio(args.getString(0), args.getInt(1)); 132 | } 133 | else if (action.equals("pausePlayingAudio")) { 134 | this.pausePlayingAudio(args.getString(0)); 135 | } 136 | else if (action.equals("stopPlayingAudio")) { 137 | this.stopPlayingAudio(args.getString(0)); 138 | } else if (action.equals("setVolume")) { 139 | try { 140 | this.setVolume(args.getString(0), Float.parseFloat(args.getString(1))); 141 | } catch (NumberFormatException nfe) { 142 | //no-op 143 | } 144 | } else if (action.equals("getCurrentPositionAudio")) { 145 | float f = this.getCurrentPositionAudio(args.getString(0)); 146 | callbackContext.sendPluginResult(new PluginResult(status, f)); 147 | return true; 148 | } 149 | else if (action.equals("getDurationAudio")) { 150 | float f = this.getDurationAudio(args.getString(0), args.getString(1)); 151 | callbackContext.sendPluginResult(new PluginResult(status, f)); 152 | return true; 153 | } 154 | else if (action.equals("create")) { 155 | String id = args.getString(0); 156 | String src = FileHelper.stripFileProtocol(args.getString(1)); 157 | getOrCreatePlayer(id, src); 158 | } 159 | else if (action.equals("release")) { 160 | boolean b = this.release(args.getString(0)); 161 | callbackContext.sendPluginResult(new PluginResult(status, b)); 162 | return true; 163 | } 164 | else if (action.equals("messageChannel")) { 165 | messageChannel = callbackContext; 166 | return true; 167 | } else if (action.equals("getCurrentAmplitudeAudio")) { 168 | float f = this.getCurrentAmplitudeAudio(args.getString(0)); 169 | callbackContext.sendPluginResult(new PluginResult(status, f)); 170 | return true; 171 | } 172 | else if (action.equals("setRate")) { 173 | this.setRate(args.getString(0), Float.parseFloat(args.getString(1))); 174 | return true; 175 | } 176 | else { // Unrecognized action. 177 | return false; 178 | } 179 | 180 | callbackContext.sendPluginResult(new PluginResult(status, result)); 181 | 182 | return true; 183 | } 184 | 185 | /** 186 | * Stop all audio players and recorders. 187 | */ 188 | public void onDestroy() { 189 | if (!players.isEmpty()) { 190 | onLastPlayerReleased(); 191 | } 192 | for (AudioPlayer audio : this.players.values()) { 193 | audio.destroy(); 194 | } 195 | this.players.clear(); 196 | } 197 | 198 | /** 199 | * Stop all audio players and recorders on navigate. 200 | */ 201 | @Override 202 | public void onReset() { 203 | onDestroy(); 204 | } 205 | 206 | /** 207 | * Called when a message is sent to plugin. 208 | * 209 | * @param id The message id 210 | * @param data The message data 211 | * @return Object to stop propagation or null 212 | */ 213 | public Object onMessage(String id, Object data) { 214 | 215 | // If phone message 216 | if (id.equals("telephone")) { 217 | 218 | // If phone ringing, then pause playing 219 | if ("ringing".equals(data) || "offhook".equals(data)) { 220 | 221 | // Get all audio players and pause them 222 | for (AudioPlayer audio : this.players.values()) { 223 | if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) { 224 | this.pausedForPhone.add(audio); 225 | audio.pausePlaying(); 226 | } 227 | } 228 | 229 | } 230 | 231 | // If phone idle, then resume playing those players we paused 232 | else if ("idle".equals(data)) { 233 | for (AudioPlayer audio : this.pausedForPhone) { 234 | audio.startPlaying(null); 235 | } 236 | this.pausedForPhone.clear(); 237 | } 238 | } 239 | return null; 240 | } 241 | 242 | //-------------------------------------------------------------------------- 243 | // LOCAL METHODS 244 | //-------------------------------------------------------------------------- 245 | 246 | private AudioPlayer getOrCreatePlayer(String id, String file) { 247 | AudioPlayer ret = players.get(id); 248 | if (ret == null) { 249 | if (players.isEmpty()) { 250 | onFirstPlayerCreated(); 251 | } 252 | ret = new AudioPlayer(this, id, file); 253 | players.put(id, ret); 254 | } 255 | return ret; 256 | } 257 | 258 | /** 259 | * Release the audio player instance to save memory. 260 | * @param id The id of the audio player 261 | */ 262 | private boolean release(String id) { 263 | AudioPlayer audio = players.remove(id); 264 | if (audio == null) { 265 | return false; 266 | } 267 | if (players.isEmpty()) { 268 | onLastPlayerReleased(); 269 | } 270 | audio.destroy(); 271 | return true; 272 | } 273 | 274 | /** 275 | * Start recording and save the specified file. 276 | * @param id The id of the audio player 277 | * @param file The name of the file 278 | */ 279 | public void startRecordingAudio(String id, String file) { 280 | AudioPlayer audio = getOrCreatePlayer(id, file); 281 | audio.startRecording(file); 282 | } 283 | 284 | /** 285 | * Stop/Pause recording and save to the file specified when recording started. 286 | * @param id The id of the audio player 287 | * @param stop If true stop recording, if false pause recording 288 | */ 289 | public void stopRecordingAudio(String id, boolean stop) { 290 | AudioPlayer audio = this.players.get(id); 291 | if (audio != null) { 292 | audio.stopRecording(stop); 293 | } 294 | } 295 | 296 | /** 297 | * Resume recording 298 | * @param id The id of the audio player 299 | */ 300 | public void resumeRecordingAudio(String id) { 301 | AudioPlayer audio = players.get(id); 302 | if (audio != null) { 303 | audio.resumeRecording(); 304 | } 305 | } 306 | 307 | /** 308 | * Start or resume playing audio file. 309 | * @param id The id of the audio player 310 | * @param file The name of the audio file. 311 | */ 312 | public void startPlayingAudio(String id, String file) { 313 | AudioPlayer audio = getOrCreatePlayer(id, file); 314 | audio.startPlaying(file); 315 | getAudioFocus(); 316 | } 317 | 318 | /** 319 | * Seek to a location. 320 | * @param id The id of the audio player 321 | * @param milliseconds int: number of milliseconds to skip 1000 = 1 second 322 | */ 323 | public void seekToAudio(String id, int milliseconds) { 324 | AudioPlayer audio = this.players.get(id); 325 | if (audio != null) { 326 | audio.seekToPlaying(milliseconds); 327 | } 328 | } 329 | 330 | /** 331 | * Pause playing. 332 | * @param id The id of the audio player 333 | */ 334 | public void pausePlayingAudio(String id) { 335 | AudioPlayer audio = this.players.get(id); 336 | if (audio != null) { 337 | audio.pausePlaying(); 338 | } 339 | } 340 | 341 | /** 342 | * Stop playing the audio file. 343 | * @param id The id of the audio player 344 | */ 345 | public void stopPlayingAudio(String id) { 346 | AudioPlayer audio = this.players.get(id); 347 | if (audio != null) { 348 | audio.stopPlaying(); 349 | } 350 | } 351 | 352 | /** 353 | * Get current position of playback. 354 | * @param id The id of the audio player 355 | * @return position in msec 356 | */ 357 | public float getCurrentPositionAudio(String id) { 358 | AudioPlayer audio = this.players.get(id); 359 | if (audio != null) { 360 | return (audio.getCurrentPosition() / 1000.0f); 361 | } 362 | return -1; 363 | } 364 | 365 | /** 366 | * Get the duration of the audio file. 367 | * @param id The id of the audio player 368 | * @param file The name of the audio file. 369 | * @return The duration in msec. 370 | */ 371 | public float getDurationAudio(String id, String file) { 372 | AudioPlayer audio = getOrCreatePlayer(id, file); 373 | return audio.getDuration(file); 374 | } 375 | 376 | /** 377 | * Set the audio device to be used for playback. 378 | * 379 | * @param output 1=earpiece, 2=speaker 380 | */ 381 | @SuppressWarnings("deprecation") 382 | public void setAudioOutputDevice(int output) { 383 | String TAG1 = "AudioHandler.setAudioOutputDevice(): Error : "; 384 | 385 | AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE); 386 | if (output == 2) { 387 | audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_SPEAKER, AudioManager.ROUTE_ALL); 388 | } 389 | else if (output == 1) { 390 | audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_EARPIECE, AudioManager.ROUTE_ALL); 391 | } 392 | else { 393 | LOG.e(TAG1," Unknown output device"); 394 | } 395 | } 396 | 397 | public void pauseAllLostFocus() { 398 | for (AudioPlayer audio : this.players.values()) { 399 | if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) { 400 | this.pausedForFocus.add(audio); 401 | audio.pausePlaying(); 402 | } 403 | } 404 | } 405 | 406 | public void resumeAllGainedFocus() { 407 | for (AudioPlayer audio : this.pausedForFocus) { 408 | audio.resumePlaying(); 409 | } 410 | this.pausedForFocus.clear(); 411 | } 412 | 413 | /** 414 | * Get the the audio focus 415 | */ 416 | private OnAudioFocusChangeListener focusChangeListener = new OnAudioFocusChangeListener() { 417 | public void onAudioFocusChange(int focusChange) { 418 | switch (focusChange) { 419 | case (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) : 420 | case (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) : 421 | case (AudioManager.AUDIOFOCUS_LOSS) : 422 | pauseAllLostFocus(); 423 | break; 424 | case (AudioManager.AUDIOFOCUS_GAIN): 425 | resumeAllGainedFocus(); 426 | break; 427 | default: 428 | break; 429 | } 430 | } 431 | }; 432 | 433 | public void getAudioFocus() { 434 | String TAG2 = "AudioHandler.getAudioFocus(): Error : "; 435 | 436 | AudioManager am = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE); 437 | int result = am.requestAudioFocus(focusChangeListener, 438 | AudioManager.STREAM_MUSIC, 439 | AudioManager.AUDIOFOCUS_GAIN); 440 | 441 | if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 442 | LOG.e(TAG2,result + " instead of " + AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 443 | } 444 | 445 | } 446 | 447 | 448 | /** 449 | * Get the audio device to be used for playback. 450 | * 451 | * @return 1=earpiece, 2=speaker 452 | */ 453 | @SuppressWarnings("deprecation") 454 | public int getAudioOutputDevice() { 455 | AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE); 456 | if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_EARPIECE) { 457 | return 1; 458 | } 459 | else if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_SPEAKER) { 460 | return 2; 461 | } 462 | else { 463 | return -1; 464 | } 465 | } 466 | 467 | /** 468 | * Set the volume for an audio device 469 | * 470 | * @param id The id of the audio player 471 | * @param volume Volume to adjust to 0.0f - 1.0f 472 | */ 473 | public void setVolume(String id, float volume) { 474 | String TAG3 = "AudioHandler.setVolume(): Error : "; 475 | 476 | AudioPlayer audio = this.players.get(id); 477 | if (audio != null) { 478 | audio.setVolume(volume); 479 | } else { 480 | LOG.e(TAG3,"Unknown Audio Player " + id); 481 | } 482 | } 483 | 484 | /** 485 | * Set the playback rate of an audio file 486 | * 487 | * @param id The id of the audio player 488 | * @param rate The playback rate 489 | */ 490 | public void setRate(String id, float rate) { 491 | String TAG3 = "AudioHandler.setRate(): Error : "; 492 | AudioPlayer audio = this.players.get(id); 493 | if (audio != null) { 494 | audio.setRate(rate); 495 | } else { 496 | LOG.e(TAG3, "Unknown Audio Player " + id); 497 | } 498 | } 499 | 500 | 501 | private void onFirstPlayerCreated() { 502 | origVolumeStream = cordova.getActivity().getVolumeControlStream(); 503 | cordova.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC); 504 | } 505 | 506 | private void onLastPlayerReleased() { 507 | if (origVolumeStream != -1) { 508 | cordova.getActivity().setVolumeControlStream(origVolumeStream); 509 | origVolumeStream = -1; 510 | } 511 | } 512 | 513 | void sendEventMessage(String action, JSONObject actionData) { 514 | JSONObject message = new JSONObject(); 515 | try { 516 | message.put("action", action); 517 | if (actionData != null) { 518 | message.put(action, actionData); 519 | } 520 | } catch (JSONException e) { 521 | LOG.e(TAG, "Failed to create event message", e); 522 | } 523 | 524 | PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, message); 525 | pluginResult.setKeepCallback(true); 526 | if (messageChannel != null) { 527 | messageChannel.sendPluginResult(pluginResult); 528 | } 529 | } 530 | 531 | public void onRequestPermissionResult(int requestCode, String[] permissions, 532 | int[] grantResults) throws JSONException 533 | { 534 | for(int r:grantResults) 535 | { 536 | if(r == PackageManager.PERMISSION_DENIED) 537 | { 538 | this.messageChannel.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); 539 | return; 540 | } 541 | } 542 | promptForRecord(); 543 | } 544 | 545 | /* 546 | * This little utility method catch-all work great for multi-permission stuff. 547 | * 548 | */ 549 | 550 | private void promptForRecord() 551 | { 552 | // If Android < 33, check for WRITE_EXTERNAL_STORAGE permission 553 | if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 554 | if (!PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 555 | PermissionHelper.requestPermission(this, WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE); 556 | return; 557 | } 558 | } 559 | 560 | // For all Android versions, check for RECORD_AUDIO permission 561 | if (!PermissionHelper.hasPermission(this, Manifest.permission.RECORD_AUDIO)) { 562 | PermissionHelper.requestPermission(this, RECORD_AUDIO, Manifest.permission.RECORD_AUDIO); 563 | return; 564 | } 565 | 566 | // Start recording if all necessary permissions were granted. 567 | this.startRecordingAudio(recordId, FileHelper.stripFileProtocol(fileUriStr)); 568 | } 569 | 570 | /** 571 | * Get current amplitude of recording. 572 | * @param id The id of the audio player 573 | * @return amplitude 574 | */ 575 | public float getCurrentAmplitudeAudio(String id) { 576 | AudioPlayer audio = this.players.get(id); 577 | if (audio != null) { 578 | return (audio.getCurrentAmplitude()); 579 | } 580 | return 0; 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /src/android/AudioPlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | package org.apache.cordova.media; 20 | 21 | import android.content.Context; 22 | import android.media.AudioManager; 23 | import android.media.MediaPlayer; 24 | import android.media.MediaPlayer.OnCompletionListener; 25 | import android.media.MediaPlayer.OnErrorListener; 26 | import android.media.MediaPlayer.OnPreparedListener; 27 | import android.media.MediaRecorder; 28 | import android.os.Environment; 29 | import android.os.Build; 30 | 31 | import org.apache.cordova.LOG; 32 | 33 | import org.json.JSONException; 34 | import org.json.JSONObject; 35 | 36 | import java.io.File; 37 | import java.io.FileInputStream; 38 | import java.io.FileOutputStream; 39 | import java.io.InputStream; 40 | import java.io.OutputStream; 41 | import java.io.IOException; 42 | import java.util.LinkedList; 43 | 44 | /** 45 | * This class implements the audio playback and recording capabilities used by Cordova. 46 | * It is called by the AudioHandler Cordova class. 47 | * Only one file can be played or recorded per class instance. 48 | * 49 | * Local audio files must reside in one of two places: 50 | * android_asset: file name must start with /android_asset/sound.mp3 51 | * sdcard: file name is just sound.mp3 52 | */ 53 | public class AudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener { 54 | 55 | // AudioPlayer modes 56 | public enum MODE { NONE, PLAY, RECORD }; 57 | 58 | // AudioPlayer states 59 | public enum STATE { MEDIA_NONE, 60 | MEDIA_STARTING, 61 | MEDIA_RUNNING, 62 | MEDIA_PAUSED, 63 | MEDIA_STOPPED, 64 | MEDIA_LOADING 65 | }; 66 | 67 | private static final String LOG_TAG = "AudioPlayer"; 68 | 69 | // AudioPlayer message ids 70 | private static int MEDIA_STATE = 1; 71 | private static int MEDIA_DURATION = 2; 72 | private static int MEDIA_POSITION = 3; 73 | private static int MEDIA_ERROR = 9; 74 | 75 | // Media error codes 76 | private static int MEDIA_ERR_NONE_ACTIVE = 0; 77 | private static int MEDIA_ERR_ABORTED = 1; 78 | // private static int MEDIA_ERR_NETWORK = 2; 79 | // private static int MEDIA_ERR_DECODE = 3; 80 | // private static int MEDIA_ERR_NONE_SUPPORTED = 4; 81 | 82 | private AudioHandler handler; // The AudioHandler object 83 | private Context context; // The Application Context object 84 | private String id; // The id of this player (used to identify Media object in JavaScript) 85 | private MODE mode = MODE.NONE; // Playback or Recording mode 86 | private STATE state = STATE.MEDIA_NONE; // State of recording or playback 87 | 88 | private String audioFile = null; // File name to play or record to 89 | private float duration = -1; // Duration of audio 90 | 91 | private MediaRecorder recorder = null; // Audio recording object 92 | private LinkedList tempFiles = null; // Temporary recording file name 93 | private String tempFile = null; 94 | 95 | private MediaPlayer player = null; // Audio player object 96 | private boolean prepareOnly = true; // playback after file prepare flag 97 | private int seekOnPrepared = 0; // seek to this location once media is prepared 98 | private float setRateOnPrepared = -1; 99 | 100 | /** 101 | * Constructor. 102 | * 103 | * @param handler The audio handler object 104 | * @param id The id of this audio player 105 | */ 106 | public AudioPlayer(AudioHandler handler, String id, String file) { 107 | this.handler = handler; 108 | context = handler.getApplicationContext(); 109 | this.id = id; 110 | this.audioFile = file; 111 | this.tempFiles = new LinkedList(); 112 | 113 | } 114 | 115 | /** 116 | * Creates an audio file path from the provided fileName or creates a new temporary file path. 117 | * 118 | * @param fileName the audio file name, if null a temporary 3gp file name is provided 119 | * @return String 120 | */ 121 | private String createAudioFilePath(String fileName) { 122 | File dir = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) 123 | ? context.getExternalFilesDir(null) 124 | : context.getCacheDir(); 125 | 126 | fileName = (fileName == null || fileName.isEmpty()) 127 | ? String.format("tmprecording-%d.3gp", System.currentTimeMillis()) 128 | : fileName; 129 | 130 | return dir.getAbsolutePath() + File.separator + fileName; 131 | } 132 | 133 | /** 134 | * Destroy player and stop audio playing or recording. 135 | */ 136 | public void destroy() { 137 | // Stop any play or record 138 | if (this.player != null) { 139 | if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { 140 | this.player.stop(); 141 | this.setState(STATE.MEDIA_STOPPED); 142 | } 143 | this.player.release(); 144 | this.player = null; 145 | } 146 | if (this.recorder != null) { 147 | if (this.state != STATE.MEDIA_STOPPED) { 148 | this.stopRecording(true); 149 | } 150 | this.recorder.release(); 151 | this.recorder = null; 152 | } 153 | } 154 | 155 | /** 156 | * Start recording the specified file. 157 | * 158 | * @param file The name of the file 159 | */ 160 | public void startRecording(String file) { 161 | String errorMessage; 162 | switch (this.mode) { 163 | case PLAY: 164 | errorMessage = "AudioPlayer Error: Can't record in play mode."; 165 | sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage); 166 | break; 167 | case NONE: 168 | this.audioFile = file; 169 | this.recorder = new MediaRecorder(); 170 | this.recorder.setAudioSource(MediaRecorder.AudioSource.MIC); 171 | this.recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS); // RAW_AMR); 172 | this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); //AMR_NB); 173 | this.recorder.setAudioEncodingBitRate(96000); 174 | this.recorder.setAudioSamplingRate(44100); 175 | this.tempFile = createAudioFilePath(null); 176 | this.recorder.setOutputFile(this.tempFile); 177 | try { 178 | this.recorder.prepare(); 179 | this.recorder.start(); 180 | this.setState(STATE.MEDIA_RUNNING); 181 | return; 182 | } catch (IllegalStateException e) { 183 | e.printStackTrace(); 184 | } catch (IOException e) { 185 | e.printStackTrace(); 186 | } 187 | 188 | sendErrorStatus(MEDIA_ERR_ABORTED, null); 189 | break; 190 | case RECORD: 191 | errorMessage = "AudioPlayer Error: Already recording."; 192 | sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage); 193 | } 194 | } 195 | 196 | /** 197 | * Save temporary recorded file to specified name 198 | * 199 | * @param file 200 | */ 201 | public void moveFile(String file) { 202 | /* this is a hack to save the file as the specified name */ 203 | 204 | if (!file.startsWith("/")) { 205 | file = createAudioFilePath(file); 206 | } 207 | 208 | int size = this.tempFiles.size(); 209 | LOG.d(LOG_TAG, "size = " + size); 210 | 211 | // only one file so just copy it 212 | if (size == 1) { 213 | String logMsg = "renaming " + this.tempFile + " to " + file; 214 | LOG.d(LOG_TAG, logMsg); 215 | 216 | File f = new File(this.tempFile); 217 | if (!f.renameTo(new File(file))) { 218 | 219 | FileOutputStream outputStream = null; 220 | File outputFile = null; 221 | try { 222 | outputFile = new File(file); 223 | outputStream = new FileOutputStream(outputFile); 224 | FileInputStream inputStream = null; 225 | File inputFile = null; 226 | try { 227 | inputFile = new File(this.tempFile); 228 | LOG.d(LOG_TAG, "INPUT FILE LENGTH: " + String.valueOf(inputFile.length()) ); 229 | inputStream = new FileInputStream(inputFile); 230 | copy(inputStream, outputStream, false); 231 | } catch (Exception e) { 232 | LOG.e(LOG_TAG, e.getLocalizedMessage(), e); 233 | } finally { 234 | if (inputStream != null) try { 235 | inputStream.close(); 236 | inputFile.delete(); 237 | inputFile = null; 238 | } catch (Exception e) { 239 | LOG.e(LOG_TAG, e.getLocalizedMessage(), e); 240 | } 241 | } 242 | } catch (Exception e) { 243 | e.printStackTrace(); 244 | } finally { 245 | if (outputStream != null) try { 246 | outputStream.close(); 247 | LOG.d(LOG_TAG, "OUTPUT FILE LENGTH: " + String.valueOf(outputFile.length()) ); 248 | } catch (Exception e) { 249 | LOG.e(LOG_TAG, e.getLocalizedMessage(), e); 250 | } 251 | } 252 | } 253 | } 254 | // more than one file so the user must have pause recording. We'll need to concat files. 255 | else { 256 | FileOutputStream outputStream = null; 257 | try { 258 | outputStream = new FileOutputStream(new File(file)); 259 | FileInputStream inputStream = null; 260 | File inputFile = null; 261 | for (int i = 0; i < size; i++) { 262 | try { 263 | inputFile = new File(this.tempFiles.get(i)); 264 | inputStream = new FileInputStream(inputFile); 265 | copy(inputStream, outputStream, (i>0)); 266 | } catch(Exception e) { 267 | LOG.e(LOG_TAG, e.getLocalizedMessage(), e); 268 | } finally { 269 | if (inputStream != null) try { 270 | inputStream.close(); 271 | inputFile.delete(); 272 | inputFile = null; 273 | } catch (Exception e) { 274 | LOG.e(LOG_TAG, e.getLocalizedMessage(), e); 275 | } 276 | } 277 | } 278 | } catch(Exception e) { 279 | e.printStackTrace(); 280 | } finally { 281 | if (outputStream != null) try { 282 | outputStream.close(); 283 | } catch (Exception e) { 284 | LOG.e(LOG_TAG, e.getLocalizedMessage(), e); 285 | } 286 | } 287 | } 288 | } 289 | 290 | private static long copy(InputStream from, OutputStream to, boolean skipHeader) 291 | throws IOException { 292 | byte[] buf = new byte[8096]; 293 | long total = 0; 294 | if (skipHeader) { 295 | from.skip(6); 296 | } 297 | while (true) { 298 | int r = from.read(buf); 299 | if (r == -1) { 300 | break; 301 | } 302 | to.write(buf, 0, r); 303 | total += r; 304 | } 305 | return total; 306 | } 307 | 308 | /** 309 | * Stop/Pause recording and save to the file specified when recording started. 310 | */ 311 | public void stopRecording(boolean stop) { 312 | if (this.recorder != null) { 313 | try{ 314 | if (this.state == STATE.MEDIA_RUNNING) { 315 | this.recorder.stop(); 316 | } 317 | this.recorder.reset(); 318 | if (!this.tempFiles.contains(this.tempFile)) { 319 | this.tempFiles.add(this.tempFile); 320 | } 321 | if (stop) { 322 | LOG.d(LOG_TAG, "stopping recording"); 323 | this.setState(STATE.MEDIA_STOPPED); 324 | this.moveFile(this.audioFile); 325 | } else { 326 | LOG.d(LOG_TAG, "pause recording"); 327 | this.setState(STATE.MEDIA_PAUSED); 328 | } 329 | } 330 | catch (Exception e) { 331 | e.printStackTrace(); 332 | } 333 | } 334 | } 335 | 336 | /** 337 | * Resume recording and save to the file specified when recording started. 338 | */ 339 | public void resumeRecording() { 340 | startRecording(this.audioFile); 341 | } 342 | 343 | //========================================================================== 344 | // Playback 345 | //========================================================================== 346 | 347 | /** 348 | * Start or resume playing audio file. 349 | * 350 | * @param file The name of the audio file. 351 | */ 352 | public void startPlaying(String file) { 353 | if (this.readyPlayer(file) && this.player != null) { 354 | this.player.start(); 355 | this.setState(STATE.MEDIA_RUNNING); 356 | this.seekOnPrepared = 0; //insures this is always reset 357 | } else { 358 | this.prepareOnly = false; 359 | } 360 | } 361 | 362 | /** 363 | * Seek or jump to a new time in the track. 364 | */ 365 | public void seekToPlaying(int milliseconds) { 366 | if (this.readyPlayer(this.audioFile)) { 367 | if (milliseconds > 0) { 368 | this.player.seekTo(milliseconds); 369 | } 370 | LOG.d(LOG_TAG, "Send a onStatus update for the new seek"); 371 | sendStatusChange(MEDIA_POSITION, null, (milliseconds / 1000.0f), null); 372 | } 373 | else { 374 | this.seekOnPrepared = milliseconds; 375 | } 376 | } 377 | 378 | /** 379 | * Pause playing. 380 | */ 381 | public void pausePlaying() { 382 | 383 | // If playing, then pause 384 | if (this.state == STATE.MEDIA_RUNNING && this.player != null) { 385 | this.player.pause(); 386 | this.setState(STATE.MEDIA_PAUSED); 387 | } 388 | else { 389 | String errorMessage = "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal(); 390 | sendErrorStatus(MEDIA_ERR_NONE_ACTIVE, errorMessage); 391 | } 392 | } 393 | 394 | /** 395 | * Stop playing the audio file. 396 | */ 397 | public void stopPlaying() { 398 | if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { 399 | this.player.pause(); 400 | this.player.seekTo(0); 401 | LOG.d(LOG_TAG, "stopPlaying is calling stopped"); 402 | this.setState(STATE.MEDIA_STOPPED); 403 | } 404 | else { 405 | String errorMessage = "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal(); 406 | sendErrorStatus(MEDIA_ERR_NONE_ACTIVE, errorMessage); 407 | } 408 | } 409 | 410 | /** 411 | * Resume playing. 412 | */ 413 | public void resumePlaying() { 414 | this.startPlaying(this.audioFile); 415 | } 416 | 417 | /** 418 | * Callback to be invoked when playback of a media source has completed. 419 | * 420 | * @param player The MediaPlayer that reached the end of the file 421 | */ 422 | public void onCompletion(MediaPlayer player) { 423 | LOG.d(LOG_TAG, "on completion is calling stopped"); 424 | this.setState(STATE.MEDIA_STOPPED); 425 | } 426 | 427 | /** 428 | * Get current position of playback. 429 | * 430 | * @return position in msec or -1 if not playing 431 | */ 432 | public long getCurrentPosition() { 433 | if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { 434 | int curPos = this.player.getCurrentPosition(); 435 | sendStatusChange(MEDIA_POSITION, null, (curPos / 1000.0f), null); 436 | return curPos; 437 | } 438 | else { 439 | return -1; 440 | } 441 | } 442 | 443 | /** 444 | * Determine if playback file is streaming or local. 445 | * It is streaming if file name starts with "http://" 446 | * 447 | * @param file The file name 448 | * @return T=streaming, F=local 449 | */ 450 | public boolean isStreaming(String file) { 451 | if (file.contains("http://") || file.contains("https://") || file.contains("rtsp://")) { 452 | return true; 453 | } 454 | else { 455 | return false; 456 | } 457 | } 458 | 459 | /** 460 | * Get the duration of the audio file. 461 | * 462 | * @param file The name of the audio file. 463 | * @return The duration in msec. 464 | * -1=can't be determined 465 | * -2=not allowed 466 | */ 467 | public float getDuration(String file) { 468 | 469 | // Can't get duration of recording 470 | if (this.recorder != null) { 471 | return (-2); // not allowed 472 | } 473 | 474 | // If audio file already loaded and started, then return duration 475 | if (this.player != null) { 476 | return this.duration; 477 | } 478 | 479 | // If no player yet, then create one 480 | else { 481 | this.prepareOnly = true; 482 | this.startPlaying(file); 483 | 484 | // This will only return value for local, since streaming 485 | // file hasn't been read yet. 486 | return this.duration; 487 | } 488 | } 489 | 490 | /** 491 | * Callback to be invoked when the media source is ready for playback. 492 | * 493 | * @param player The MediaPlayer that is ready for playback 494 | */ 495 | public void onPrepared(MediaPlayer player) { 496 | // Listen for playback completion 497 | this.player.setOnCompletionListener(this); 498 | // seek to any location received while not prepared 499 | this.seekToPlaying(this.seekOnPrepared); 500 | // apply any playback rate received while not prepared 501 | if (setRateOnPrepared >= 0) 502 | this.player.setPlaybackParams (this.player.getPlaybackParams().setSpeed(setRateOnPrepared)); 503 | // If start playing after prepared 504 | if (!this.prepareOnly) { 505 | this.player.start(); 506 | this.setState(STATE.MEDIA_RUNNING); 507 | this.seekOnPrepared = 0; //reset only when played 508 | } else { 509 | this.setState(STATE.MEDIA_STARTING); 510 | } 511 | // Save off duration 512 | this.duration = getDurationInSeconds(); 513 | // reset prepare only flag 514 | this.prepareOnly = true; 515 | 516 | // Send status notification to JavaScript 517 | sendStatusChange(MEDIA_DURATION, null, this.duration, null); 518 | } 519 | 520 | /** 521 | * By default Android returns the length of audio in mills but we want seconds 522 | * 523 | * @return length of clip in seconds 524 | */ 525 | private float getDurationInSeconds() { 526 | return (this.player.getDuration() / 1000.0f); 527 | } 528 | 529 | /** 530 | * Callback to be invoked when there has been an error during an asynchronous operation 531 | * (other errors will throw exceptions at method call time). 532 | * 533 | * @param player the MediaPlayer the error pertains to 534 | * @param arg1 the type of error that has occurred: (MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_SERVER_DIED) 535 | * @param arg2 an extra code, specific to the error. 536 | */ 537 | public boolean onError(MediaPlayer player, int arg1, int arg2) { 538 | String errorMessage = "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")"; 539 | 540 | // we don't want to send success callback 541 | // so we don't call setState() here 542 | this.state = STATE.MEDIA_STOPPED; 543 | this.destroy(); 544 | // Send error notification to JavaScript 545 | sendErrorStatus(arg1, errorMessage); 546 | 547 | return false; 548 | } 549 | 550 | /** 551 | * Set the state and send it to JavaScript. 552 | * 553 | * @param state 554 | */ 555 | private void setState(STATE state) { 556 | if (this.state != state) { 557 | sendStatusChange(MEDIA_STATE, null, (float)state.ordinal(), null); 558 | } 559 | this.state = state; 560 | } 561 | 562 | /** 563 | * Set the mode and send it to JavaScript. 564 | * 565 | * @param mode 566 | */ 567 | private void setMode(MODE mode) { 568 | if (this.mode != mode) { 569 | //mode is not part of the expected behavior, so no notification 570 | //this.handler.webView.sendJavascript("cordova.require('cordova-plugin-media.Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");"); 571 | } 572 | this.mode = mode; 573 | } 574 | 575 | /** 576 | * Get the audio state. 577 | * 578 | * @return int 579 | */ 580 | public int getState() { 581 | return this.state.ordinal(); 582 | } 583 | 584 | /** 585 | * Set the volume for audio player 586 | * 587 | * @param volume 588 | */ 589 | public void setVolume(float volume) { 590 | if (this.player != null) { 591 | this.player.setVolume(volume, volume); 592 | } else { 593 | String errorMessage = "AudioPlayer Error: Cannot set volume until the audio file is initialized."; 594 | sendErrorStatus(MEDIA_ERR_NONE_ACTIVE, errorMessage); 595 | } 596 | } 597 | 598 | /** 599 | * attempts to put the player in play mode 600 | * @return true if in playmode, false otherwise 601 | */ 602 | private boolean playMode() { 603 | switch(this.mode) { 604 | case NONE: 605 | this.setMode(MODE.PLAY); 606 | break; 607 | case PLAY: 608 | break; 609 | case RECORD: 610 | String errorMessage = "AudioPlayer Error: Can't play in record mode."; 611 | sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage); 612 | return false; //player is not ready 613 | } 614 | return true; 615 | } 616 | 617 | /** 618 | * attempts to initialize the media player for playback 619 | * @param file the file to play 620 | * @return false if player not ready, reports if in wrong mode or state 621 | */ 622 | private boolean readyPlayer(String file) { 623 | if (playMode()) { 624 | switch (this.state) { 625 | case MEDIA_NONE: 626 | if (this.player == null) { 627 | this.player = new MediaPlayer(); 628 | this.player.setOnErrorListener(this); 629 | } 630 | try { 631 | this.loadAudioFile(file); 632 | } catch (Exception e) { 633 | sendErrorStatus(MEDIA_ERR_ABORTED, e.getMessage()); 634 | } 635 | return false; 636 | case MEDIA_LOADING: 637 | //cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING instead 638 | LOG.d(LOG_TAG, "AudioPlayer Loading: startPlaying() called during media preparation: " + STATE.MEDIA_STARTING.ordinal()); 639 | this.prepareOnly = false; 640 | return false; 641 | case MEDIA_STARTING: 642 | case MEDIA_RUNNING: 643 | case MEDIA_PAUSED: 644 | return true; 645 | case MEDIA_STOPPED: 646 | //if we are readying the same file 647 | if (file!=null && this.audioFile.compareTo(file) == 0) { 648 | //maybe it was recording? 649 | if (player == null) { 650 | this.player = new MediaPlayer(); 651 | this.player.setOnErrorListener(this); 652 | this.prepareOnly = false; 653 | 654 | try { 655 | this.loadAudioFile(file); 656 | } catch (Exception e) { 657 | sendErrorStatus(MEDIA_ERR_ABORTED, e.getMessage()); 658 | } 659 | return false;//we´re not ready yet 660 | } 661 | else { 662 | //reset the audio file 663 | player.seekTo(0); 664 | player.pause(); 665 | return true; 666 | } 667 | } else { 668 | //reset the player 669 | this.player.reset(); 670 | try { 671 | this.loadAudioFile(file); 672 | } catch (Exception e) { 673 | sendErrorStatus(MEDIA_ERR_ABORTED, e.getMessage()); 674 | } 675 | //if we had to prepare the file, we won't be in the correct state for playback 676 | return false; 677 | } 678 | default: 679 | String errorMessage = "AudioPlayer Error: startPlaying() called during invalid state: " + this.state; 680 | sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage); 681 | } 682 | } 683 | return false; 684 | } 685 | 686 | /** 687 | * load audio file 688 | * @throws IOException 689 | * @throws IllegalStateException 690 | * @throws SecurityException 691 | * @throws IllegalArgumentException 692 | */ 693 | private void loadAudioFile(String file) throws IllegalArgumentException, SecurityException, IllegalStateException, IOException { 694 | if (this.isStreaming(file)) { 695 | this.player.setDataSource(file); 696 | this.player.setAudioStreamType(AudioManager.STREAM_MUSIC); 697 | //if it's a streaming file, play mode is implied 698 | this.setMode(MODE.PLAY); 699 | this.setState(STATE.MEDIA_STARTING); 700 | this.player.setOnPreparedListener(this); 701 | this.player.prepareAsync(); 702 | } 703 | else { 704 | if (file.startsWith("/android_asset/")) { 705 | String f = file.substring(15); 706 | android.content.res.AssetFileDescriptor fd = this.handler.cordova.getActivity().getAssets().openFd(f); 707 | this.player.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength()); 708 | } 709 | else { 710 | File fp = new File(file); 711 | if (fp.exists()) { 712 | FileInputStream fileInputStream = new FileInputStream(file); 713 | this.player.setDataSource(fileInputStream.getFD()); 714 | fileInputStream.close(); 715 | } 716 | else { 717 | this.player.setDataSource(createAudioFilePath(file)); 718 | } 719 | } 720 | this.setState(STATE.MEDIA_STARTING); 721 | this.player.setOnPreparedListener(this); 722 | this.player.prepare(); 723 | 724 | // Get duration 725 | this.duration = getDurationInSeconds(); 726 | } 727 | } 728 | 729 | private void sendErrorStatus(int errorCode, String errorMessage) { 730 | sendStatusChange(MEDIA_ERROR, errorCode, null, errorMessage); 731 | } 732 | 733 | private void sendStatusChange(int messageType, Integer additionalCode, Float value, String errorMessage) { 734 | if (additionalCode != null && value != null) { 735 | throw new IllegalArgumentException("Only one of additionalCode or value can be specified, not both"); 736 | } 737 | 738 | if (errorMessage != null) { 739 | LOG.d(LOG_TAG, errorMessage); 740 | } 741 | 742 | JSONObject statusDetails = new JSONObject(); 743 | try { 744 | statusDetails.put("id", this.id); 745 | statusDetails.put("msgType", messageType); 746 | if (additionalCode != null) { 747 | JSONObject code = new JSONObject(); 748 | code.put("code", additionalCode.intValue()); 749 | 750 | if (errorMessage != null) { 751 | code.put("message", errorMessage); 752 | } 753 | 754 | statusDetails.put("value", code); 755 | } 756 | else if (value != null) { 757 | statusDetails.put("value", value.floatValue()); 758 | } 759 | } catch (JSONException e) { 760 | LOG.e(LOG_TAG, "Failed to create status details", e); 761 | } 762 | 763 | this.handler.sendEventMessage("status", statusDetails); 764 | } 765 | 766 | /** 767 | * Get current amplitude of recording. 768 | * 769 | * @return amplitude or 0 if not recording 770 | */ 771 | public float getCurrentAmplitude() { 772 | if (this.recorder != null) { 773 | try{ 774 | if (this.state == STATE.MEDIA_RUNNING) { 775 | return (float) this.recorder.getMaxAmplitude() / 32762; 776 | } 777 | } 778 | catch (Exception e) { 779 | e.printStackTrace(); 780 | } 781 | } 782 | return 0; 783 | } 784 | 785 | /** 786 | * Set the playback rate for the player (ignored on API < 23) 787 | * 788 | * @param volume 789 | */ 790 | public void setRate(float rate) { 791 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 792 | LOG.d(LOG_TAG, "AudioPlayer Warning: Request to set playback rate not supported on current OS version"); 793 | return; 794 | } 795 | 796 | if (this.player != null) { 797 | try { 798 | boolean wasPlaying = this.player.isPlaying(); 799 | 800 | this.player.setPlaybackParams(this.player.getPlaybackParams().setSpeed(rate)); 801 | 802 | if (!wasPlaying && this.player.isPlaying()) { 803 | this.player.pause(); 804 | } 805 | } catch(Exception e) { 806 | e.printStackTrace(); 807 | } 808 | } else { 809 | setRateOnPrepared = rate; 810 | } 811 | } 812 | } 813 | -------------------------------------------------------------------------------- /src/android/FileHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | package org.apache.cordova.media; 20 | 21 | import android.net.Uri; 22 | 23 | public class FileHelper { 24 | 25 | /** 26 | * Removes the "file://" prefix from the given URI string, if applicable. 27 | * If the given URI string doesn't have a "file://" prefix, it is returned unchanged. 28 | * 29 | * @param uriString the URI string to operate on 30 | * @return a path without the "file://" prefix 31 | */ 32 | public static String stripFileProtocol(String uriString) { 33 | if (uriString.startsWith("file://")) { 34 | return Uri.parse(uriString).getPath(); 35 | } 36 | return uriString; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ios/CDVSound.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an 12 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | KIND, either express or implied. See the License for the 14 | specific language governing permissions and limitations 15 | under the License. 16 | */ 17 | 18 | @import WebKit; 19 | #import 20 | #import 21 | #import 22 | 23 | #import 24 | 25 | enum CDVMediaError { 26 | MEDIA_ERR_NONE_ACTIVE = 0, 27 | MEDIA_ERR_ABORTED = 1, 28 | MEDIA_ERR_NETWORK = 2, 29 | MEDIA_ERR_DECODE = 3, 30 | MEDIA_ERR_NONE_SUPPORTED = 4 31 | }; 32 | typedef NSUInteger CDVMediaError; 33 | 34 | enum CDVMediaStates { 35 | MEDIA_NONE = 0, 36 | MEDIA_STARTING = 1, 37 | MEDIA_RUNNING = 2, 38 | MEDIA_PAUSED = 3, 39 | MEDIA_STOPPED = 4 40 | }; 41 | typedef NSUInteger CDVMediaStates; 42 | 43 | enum CDVMediaMsg { 44 | MEDIA_STATE = 1, 45 | MEDIA_DURATION = 2, 46 | MEDIA_POSITION = 3, 47 | MEDIA_ERROR = 9 48 | }; 49 | typedef NSUInteger CDVMediaMsg; 50 | 51 | @interface CDVAudioPlayer : AVAudioPlayer 52 | { 53 | NSString* mediaId; 54 | } 55 | @property (nonatomic, copy) NSString* mediaId; 56 | @end 57 | 58 | @interface CDVAudioRecorder : AVAudioRecorder 59 | { 60 | NSString* mediaId; 61 | } 62 | @property (nonatomic, copy) NSString* mediaId; 63 | @end 64 | 65 | @interface CDVAudioFile : NSObject 66 | { 67 | NSString* resourcePath; 68 | NSURL* resourceURL; 69 | CDVAudioPlayer* player; 70 | CDVAudioRecorder* recorder; 71 | NSNumber* volume; 72 | NSNumber* rate; 73 | } 74 | 75 | @property (nonatomic, strong) NSString* resourcePath; 76 | @property (nonatomic, strong) NSURL* resourceURL; 77 | @property (nonatomic, strong) CDVAudioPlayer* player; 78 | @property (nonatomic, strong) NSNumber* volume; 79 | @property (nonatomic, strong) NSNumber* rate; 80 | 81 | @property (nonatomic, strong) CDVAudioRecorder* recorder; 82 | 83 | @end 84 | 85 | @interface CDVSound : CDVPlugin 86 | { 87 | NSMutableDictionary* soundCache; 88 | NSString* currMediaId; 89 | AVAudioSession* avSession; 90 | AVPlayer* avPlayer; 91 | NSString* statusCallbackId; 92 | } 93 | @property (nonatomic, strong) NSMutableDictionary* soundCache; 94 | @property (nonatomic, strong) AVAudioSession* avSession; 95 | @property (nonatomic, strong) NSString* currMediaId; 96 | @property (nonatomic, strong) NSString* statusCallbackId; 97 | 98 | - (void)startPlayingAudio:(CDVInvokedUrlCommand*)command; 99 | - (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command; 100 | - (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command; 101 | - (void)seekToAudio:(CDVInvokedUrlCommand*)command; 102 | - (void)release:(CDVInvokedUrlCommand*)command; 103 | - (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command; 104 | - (void)resumeRecordingAudio:(CDVInvokedUrlCommand*)command; 105 | - (void)pauseRecordingAudio:(CDVInvokedUrlCommand*)command; 106 | 107 | - (BOOL)hasAudioSession; 108 | 109 | // helper methods 110 | - (NSURL*)urlForRecording:(NSString*)resourcePath; 111 | - (NSURL*)urlForPlaying:(NSString*)resourcePath; 112 | 113 | - (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord; 114 | - (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord suppressValidationErrors:(BOOL)bSuppress; 115 | - (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId; 116 | - (NSDictionary*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message; 117 | 118 | - (void)startRecordingAudio:(CDVInvokedUrlCommand*)command; 119 | - (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command; 120 | - (void)getCurrentAmplitudeAudio:(CDVInvokedUrlCommand*)command; 121 | 122 | - (void)setVolume:(CDVInvokedUrlCommand*)command; 123 | - (void)setRate:(CDVInvokedUrlCommand*)command; 124 | 125 | @end 126 | -------------------------------------------------------------------------------- /src/ios/CDVSound.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an 12 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | KIND, either express or implied. See the License for the 14 | specific language governing permissions and limitations 15 | under the License. 16 | */ 17 | 18 | #import "CDVSound.h" 19 | #import "CDVFile.h" 20 | #import 21 | #include 22 | 23 | #define DOCUMENTS_SCHEME_PREFIX @"documents://" 24 | #define HTTP_SCHEME_PREFIX @"http://" 25 | #define HTTPS_SCHEME_PREFIX @"https://" 26 | #define CDVFILE_PREFIX @"cdvfile://" 27 | #define FILE_PREFIX @"file://" 28 | 29 | @implementation CDVSound 30 | 31 | BOOL keepAvAudioSessionAlwaysActive = NO; 32 | 33 | @synthesize soundCache, avSession, currMediaId, statusCallbackId; 34 | 35 | -(void) pluginInitialize 36 | { 37 | NSDictionary* settings = self.commandDelegate.settings; 38 | keepAvAudioSessionAlwaysActive = [[settings objectForKey:[@"KeepAVAudioSessionAlwaysActive" lowercaseString]] boolValue]; 39 | if (keepAvAudioSessionAlwaysActive) { 40 | if ([self hasAudioSession]) { 41 | NSError* error = nil; 42 | if(![self.avSession setActive:YES error:&error]) { 43 | NSLog(@"Unable to activate session: %@", [error localizedFailureReason]); 44 | } 45 | } 46 | } 47 | } 48 | 49 | // Maps a url for a resource path for recording 50 | - (NSURL*)urlForRecording:(NSString*)resourcePath 51 | { 52 | NSURL* resourceURL = nil; 53 | NSString* filePath = nil; 54 | NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; 55 | 56 | // first check for correct extension 57 | NSString* ext=[resourcePath pathExtension]; 58 | if ([ext caseInsensitiveCompare:@"wav"] != NSOrderedSame && 59 | [ext caseInsensitiveCompare:@"m4a"] != NSOrderedSame) { 60 | resourceURL = nil; 61 | NSLog(@"Resource for recording must have wav or m4a extension"); 62 | } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { 63 | // try to find Documents:// resources 64 | filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; 65 | NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); 66 | } else if ([resourcePath hasPrefix:CDVFILE_PREFIX]) { 67 | CDVFile *filePlugin = [self.commandDelegate getCommandInstance:@"File"]; 68 | CDVFilesystemURL *url = [CDVFilesystemURL fileSystemURLWithString:resourcePath]; 69 | filePath = [filePlugin filesystemPathForURL:url]; 70 | if (filePath == nil) { 71 | resourceURL = [NSURL URLWithString:resourcePath]; 72 | } 73 | } else { 74 | if ([resourcePath hasPrefix:FILE_PREFIX]) { // Support file scheme 75 | resourcePath = [resourcePath substringFromIndex:[FILE_PREFIX length]]; 76 | } 77 | // if resourcePath is not from FileSystem put in tmp dir, else attempt to use provided resource path 78 | NSString* tmpPath = [NSTemporaryDirectory()stringByStandardizingPath]; 79 | BOOL isTmp = [resourcePath rangeOfString:tmpPath].location != NSNotFound; 80 | BOOL isDoc = [resourcePath rangeOfString:docsPath].location != NSNotFound; 81 | if (!isTmp && !isDoc) { 82 | // put in temp dir 83 | filePath = [NSString stringWithFormat:@"%@/%@", tmpPath, resourcePath]; 84 | } else { 85 | filePath = resourcePath; 86 | } 87 | } 88 | 89 | if (filePath != nil) { 90 | // create resourceURL 91 | resourceURL = [NSURL fileURLWithPath:filePath]; 92 | } 93 | return resourceURL; 94 | } 95 | 96 | - (NSString*)getCdvCustomSchemeAssetPrefix { 97 | NSDictionary* settings = self.commandDelegate.settings; 98 | NSString *scheme = [settings objectForKey:[@"scheme" lowercaseString]]; 99 | // If scheme is file or nil, then default to file scheme 100 | Boolean isCdvFileScheme = [scheme isEqualToString: @"file"] || scheme == nil; 101 | NSString *hostname = @""; 102 | 103 | if (!isCdvFileScheme) { 104 | if (scheme == nil || [WKWebView handlesURLScheme:scheme]) { 105 | scheme = @"app"; 106 | } 107 | 108 | hostname = [settings objectForKey:[@"hostname" lowercaseString]]; 109 | if(hostname == nil){ 110 | hostname = @"localhost"; 111 | } 112 | 113 | return [NSString stringWithFormat:@"%@://%@/", scheme, hostname]; 114 | } 115 | 116 | return nil; 117 | } 118 | 119 | - (Boolean)isCdvCustomSchemeUrl:(NSString*)resourcePath 120 | { 121 | NSString *assetUrl = [self getCdvCustomSchemeAssetPrefix]; 122 | 123 | if (assetUrl != nil) { 124 | return [resourcePath hasPrefix:assetUrl]; 125 | } 126 | 127 | return false; 128 | } 129 | 130 | // Attempts to find the file path in the "www" or "LocalFileSystem.TEMPORARY" directory. 131 | // Used for "file://" & "custom-scheme://hostname/" schemes and leading "/" directory paths. 132 | - (NSString*)attemptFindFilePath:(NSString*)resourcePath prefix:(NSString*)prefix 133 | { 134 | resourcePath = [resourcePath substringFromIndex:[prefix length]]; 135 | 136 | NSString *filePath = [self.commandDelegate pathForResource:resourcePath]; 137 | 138 | if (filePath == nil) { 139 | // see if this exists in the documents/temp directory from a previous recording 140 | NSString* testPath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], resourcePath]; 141 | if ([[NSFileManager defaultManager] fileExistsAtPath:testPath]) { 142 | // inefficient as existence will be checked again below but only way to determine if file exists from previous recording 143 | filePath = testPath; 144 | NSLog(@"Will attempt to use file resource from LocalFileSystem.TEMPORARY directory"); 145 | } else { 146 | // attempt to use path provided 147 | filePath = resourcePath; 148 | NSLog(@"Will attempt to use file resource '%@'", filePath); 149 | } 150 | } else { 151 | NSLog(@"Found resource '%@' in the web folder.", filePath); 152 | } 153 | 154 | return filePath; 155 | } 156 | 157 | // Maps a url for a resource path for playing 158 | // "Naked" resource paths are assumed to be from the www folder as its base 159 | - (NSURL*)urlForPlaying:(NSString*)resourcePath 160 | { 161 | NSURL* resourceURL = nil; 162 | NSString* filePath = nil; 163 | 164 | // The order of checking if url path: 165 | // 1. http:// or https:// 166 | // 2. documents:// 167 | // 3. cdvfile:// 168 | // 4. file:// 169 | // 5. custom app scheme+hostname (e.g. app://localhost/) 170 | // 6. starts with a "/" 171 | // 172 | // For use case 4-6, it will attempt to find file path in "www" or "LocalFileSystem.TEMPORARY" directory 173 | if ([resourcePath hasPrefix:HTTP_SCHEME_PREFIX] || [resourcePath hasPrefix:HTTPS_SCHEME_PREFIX]) { 174 | // if it is a http url, use it 175 | NSLog(@"Will use resource '%@' from the Internet.", resourcePath); 176 | resourceURL = [NSURL URLWithString:resourcePath]; 177 | } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { 178 | NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; 179 | filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; 180 | NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); 181 | } else if ([resourcePath hasPrefix:CDVFILE_PREFIX]) { 182 | CDVFile *filePlugin = [self.commandDelegate getCommandInstance:@"File"]; 183 | CDVFilesystemURL *url = [CDVFilesystemURL fileSystemURLWithString:resourcePath]; 184 | filePath = [filePlugin filesystemPathForURL:url]; 185 | if (filePath == nil) { 186 | resourceURL = [NSURL URLWithString:resourcePath]; 187 | } 188 | } else if ([resourcePath hasPrefix:FILE_PREFIX]) { 189 | filePath = [self attemptFindFilePath:resourcePath prefix:FILE_PREFIX]; 190 | } else if ([self isCdvCustomSchemeUrl:resourcePath]) { 191 | NSString *assetUrl = [self getCdvCustomSchemeAssetPrefix]; 192 | filePath = [self attemptFindFilePath:resourcePath prefix: assetUrl]; 193 | } else if ([resourcePath hasPrefix:@"/"]) { 194 | filePath = [self attemptFindFilePath:resourcePath prefix:@"/"]; 195 | } 196 | 197 | // if the resourcePath resolved to a file path, check that file exists 198 | if (filePath != nil) { 199 | // create resourceURL 200 | resourceURL = [NSURL fileURLWithPath:filePath]; 201 | // try to access file 202 | NSFileManager* fMgr = [NSFileManager defaultManager]; 203 | if (![fMgr fileExistsAtPath:filePath]) { 204 | resourceURL = nil; 205 | NSLog(@"Unknown resource '%@'", resourcePath); 206 | } 207 | } 208 | 209 | return resourceURL; 210 | } 211 | 212 | // Creates or gets the cached audio file resource object 213 | - (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord suppressValidationErrors:(BOOL)bSuppress 214 | { 215 | BOOL bError = NO; 216 | CDVMediaError errcode = MEDIA_ERR_NONE_SUPPORTED; 217 | NSString* errMsg = @""; 218 | CDVAudioFile* audioFile = nil; 219 | NSURL* resourceURL = nil; 220 | 221 | if ([self soundCache] == nil) { 222 | [self setSoundCache:[NSMutableDictionary dictionaryWithCapacity:1]]; 223 | } else { 224 | audioFile = [[self soundCache] objectForKey:mediaId]; 225 | } 226 | if (audioFile == nil) { 227 | // validate resourcePath and create 228 | if ((resourcePath == nil) || ![resourcePath isKindOfClass:[NSString class]] || [resourcePath isEqualToString:@""]) { 229 | bError = YES; 230 | errcode = MEDIA_ERR_ABORTED; 231 | errMsg = @"invalid media src argument"; 232 | } else { 233 | audioFile = [[CDVAudioFile alloc] init]; 234 | audioFile.resourcePath = resourcePath; 235 | audioFile.resourceURL = nil; // validate resourceURL when actually play or record 236 | [[self soundCache] setObject:audioFile forKey:mediaId]; 237 | } 238 | } 239 | if (bValidate && (audioFile.resourceURL == nil)) { 240 | if (bRecord) { 241 | resourceURL = [self urlForRecording:resourcePath]; 242 | } else { 243 | resourceURL = [self urlForPlaying:resourcePath]; 244 | } 245 | if ((resourceURL == nil) && !bSuppress) { 246 | bError = YES; 247 | errcode = MEDIA_ERR_ABORTED; 248 | errMsg = [NSString stringWithFormat:@"Cannot use audio file from resource '%@'", resourcePath]; 249 | } else { 250 | audioFile.resourceURL = resourceURL; 251 | } 252 | } 253 | 254 | if (bError) { 255 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 256 | [self createMediaErrorWithCode:errcode message:errMsg]]; 257 | } 258 | 259 | return audioFile; 260 | } 261 | 262 | // Creates or gets the cached audio file resource object 263 | - (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord 264 | { 265 | return [self audioFileForResource:resourcePath withId:mediaId doValidation:bValidate forRecording:bRecord suppressValidationErrors:NO]; 266 | } 267 | 268 | // returns whether or not audioSession is available - creates it if necessary 269 | - (BOOL)hasAudioSession 270 | { 271 | BOOL bSession = YES; 272 | 273 | if (!self.avSession) { 274 | NSError* error = nil; 275 | 276 | self.avSession = [AVAudioSession sharedInstance]; 277 | if (error) { 278 | // is not fatal if can't get AVAudioSession , just log the error 279 | NSLog(@"error creating audio session: %@", [[error userInfo] description]); 280 | self.avSession = nil; 281 | bSession = NO; 282 | } 283 | } 284 | return bSession; 285 | } 286 | 287 | // helper function to create a error object string 288 | - (NSDictionary*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message 289 | { 290 | NSMutableDictionary* errorDict = [NSMutableDictionary dictionaryWithCapacity:2]; 291 | 292 | [errorDict setObject:[NSNumber numberWithUnsignedInteger:code] forKey:@"code"]; 293 | [errorDict setObject:message ? message:@"" forKey:@"message"]; 294 | return errorDict; 295 | } 296 | 297 | //helper function to create specifically an abort error 298 | -(NSDictionary*)createAbortError:(NSString*)message 299 | { 300 | return [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:message]; 301 | } 302 | 303 | - (void)create:(CDVInvokedUrlCommand*)command 304 | { 305 | NSString* mediaId = [command argumentAtIndex:0]; 306 | NSString* resourcePath = [command argumentAtIndex:1]; 307 | 308 | CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:YES forRecording:NO suppressValidationErrors:YES]; 309 | 310 | if (audioFile == nil) { 311 | NSString* errorMessage = [NSString stringWithFormat:@"Failed to initialize Media file with path %@", resourcePath]; 312 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 313 | [self createAbortError:errorMessage]]; 314 | } else { 315 | NSURL* resourceUrl = audioFile.resourceURL; 316 | 317 | if (![resourceUrl isFileURL] && ![resourcePath hasPrefix:CDVFILE_PREFIX]) { 318 | // First create an AVPlayerItem 319 | AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:resourceUrl]; 320 | 321 | // Subscribe to the AVPlayerItem's DidPlayToEndTime notification. 322 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; 323 | 324 | // Subscribe to the AVPlayerItem's PlaybackStalledNotification notification. 325 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemStalledPlaying:) name:AVPlayerItemPlaybackStalledNotification object:playerItem]; 326 | 327 | // Pass the AVPlayerItem to a new player 328 | avPlayer = [[AVPlayer alloc] initWithPlayerItem:playerItem]; 329 | 330 | // Avoid excessive buffering so streaming media can play instantly on iOS 331 | // Removes preplay delay on ios 10+, makes consistent with ios9 behaviour 332 | if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10,0,0}]) { 333 | avPlayer.automaticallyWaitsToMinimizeStalling = NO; 334 | } 335 | } 336 | 337 | self.currMediaId = mediaId; 338 | 339 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 340 | [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; 341 | } 342 | } 343 | 344 | - (void)setVolume:(CDVInvokedUrlCommand*)command 345 | { 346 | NSString* callbackId = command.callbackId; 347 | 348 | #pragma unused(callbackId) 349 | NSString* mediaId = [command argumentAtIndex:0]; 350 | NSNumber* volume = [command argumentAtIndex:1 withDefault:[NSNumber numberWithFloat:1.0]]; 351 | 352 | if ([self soundCache] != nil) { 353 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 354 | if (audioFile != nil) { 355 | audioFile.volume = volume; 356 | if (audioFile.player) { 357 | audioFile.player.volume = [volume floatValue]; 358 | } 359 | else { 360 | float customVolume = [volume floatValue]; 361 | if (customVolume >= 0.0 && customVolume <= 1.0) { 362 | [avPlayer setVolume: customVolume]; 363 | } 364 | else { 365 | NSLog(@"The value must be within the range of 0.0 to 1.0"); 366 | } 367 | } 368 | [[self soundCache] setObject:audioFile forKey:mediaId]; 369 | } 370 | } 371 | 372 | // don't care for any callbacks 373 | } 374 | 375 | - (void)setRate:(CDVInvokedUrlCommand*)command 376 | { 377 | NSString* callbackId = command.callbackId; 378 | 379 | #pragma unused(callbackId) 380 | NSString* mediaId = [command argumentAtIndex:0]; 381 | NSNumber* rate = [command argumentAtIndex:1 withDefault:[NSNumber numberWithFloat:1.0]]; 382 | 383 | if ([self soundCache] != nil) { 384 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 385 | if (audioFile != nil) { 386 | audioFile.rate = rate; 387 | if (audioFile.player) { 388 | audioFile.player.enableRate = YES; 389 | audioFile.player.rate = [rate floatValue]; 390 | } 391 | if (avPlayer.currentItem && avPlayer.currentItem.asset){ 392 | float customRate = [rate floatValue]; 393 | [avPlayer setRate:customRate]; 394 | } 395 | 396 | [[self soundCache] setObject:audioFile forKey:mediaId]; 397 | } 398 | } 399 | 400 | // don't care for any callbacks 401 | } 402 | 403 | - (void)startPlayingAudio:(CDVInvokedUrlCommand*)command 404 | { 405 | [self.commandDelegate runInBackground:^{ 406 | 407 | NSString* callbackId = command.callbackId; 408 | 409 | #pragma unused(callbackId) 410 | NSString* mediaId = [command argumentAtIndex:0]; 411 | NSString* resourcePath = [command argumentAtIndex:1]; 412 | NSDictionary* options = [command argumentAtIndex:2 withDefault:nil]; 413 | 414 | BOOL bError = NO; 415 | 416 | CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:YES forRecording:NO]; 417 | if ((audioFile != nil) && (audioFile.resourceURL != nil)) { 418 | if (audioFile.player == nil) { 419 | bError = [self prepareToPlay:audioFile withId:mediaId]; 420 | } 421 | if (!bError) { 422 | //self.currMediaId = audioFile.player.mediaId; 423 | self.currMediaId = mediaId; 424 | 425 | // audioFile.player != nil or player was successfully created 426 | // get the audioSession and set the category to allow Playing when device is locked or ring/silent switch engaged 427 | if ([self hasAudioSession]) { 428 | NSError* __autoreleasing err = nil; 429 | NSNumber* playAudioWhenScreenIsLocked = [options objectForKey:@"playAudioWhenScreenIsLocked"]; 430 | BOOL bPlayAudioWhenScreenIsLocked = YES; 431 | if (playAudioWhenScreenIsLocked != nil) { 432 | bPlayAudioWhenScreenIsLocked = [playAudioWhenScreenIsLocked boolValue]; 433 | } 434 | 435 | NSString* sessionCategory = bPlayAudioWhenScreenIsLocked ? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient; 436 | [self.avSession setCategory:sessionCategory error:&err]; 437 | if (![self.avSession setActive:YES error:&err]) { 438 | // other audio with higher priority that does not allow mixing could cause this to fail 439 | NSLog(@"Unable to play audio: %@", [err localizedFailureReason]); 440 | bError = YES; 441 | } 442 | } 443 | if (!bError) { 444 | NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); 445 | double duration = 0; 446 | if (avPlayer.currentItem && avPlayer.currentItem.asset) { 447 | CMTime time = avPlayer.currentItem.asset.duration; 448 | duration = CMTimeGetSeconds(time); 449 | if (isnan(duration)) { 450 | NSLog(@"Duration is infinite, setting it to -1"); 451 | duration = -1; 452 | } 453 | 454 | if (audioFile.rate != nil){ 455 | float customRate = [audioFile.rate floatValue]; 456 | NSLog(@"Playing stream with AVPlayer & custom rate"); 457 | [avPlayer setRate:customRate]; 458 | } else { 459 | NSLog(@"Playing stream with AVPlayer & default rate"); 460 | [avPlayer play]; 461 | } 462 | 463 | } else { 464 | 465 | NSNumber* loopOption = [options objectForKey:@"numberOfLoops"]; 466 | NSInteger numberOfLoops = 0; 467 | if (loopOption != nil) { 468 | numberOfLoops = [loopOption intValue] - 1; 469 | } 470 | audioFile.player.numberOfLoops = numberOfLoops; 471 | if (audioFile.player.isPlaying) { 472 | [audioFile.player stop]; 473 | audioFile.player.currentTime = 0; 474 | } 475 | if (audioFile.volume != nil) { 476 | audioFile.player.volume = [audioFile.volume floatValue]; 477 | } 478 | 479 | audioFile.player.enableRate = YES; 480 | if (audioFile.rate != nil) { 481 | audioFile.player.rate = [audioFile.rate floatValue]; 482 | } 483 | 484 | [audioFile.player play]; 485 | duration = round(audioFile.player.duration * 1000) / 1000; 486 | } 487 | 488 | [self onStatus:MEDIA_DURATION mediaId:mediaId param:@(duration)]; 489 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_RUNNING)]; 490 | } 491 | } 492 | if (bError) { 493 | /* I don't see a problem playing previously recorded audio so removing this section - BG 494 | NSError* error; 495 | // try loading it one more time, in case the file was recorded previously 496 | audioFile.player = [[ AVAudioPlayer alloc ] initWithContentsOfURL:audioFile.resourceURL error:&error]; 497 | if (error != nil) { 498 | NSLog(@"Failed to initialize AVAudioPlayer: %@\n", error); 499 | audioFile.player = nil; 500 | } else { 501 | NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); 502 | audioFile.player.numberOfLoops = numberOfLoops; 503 | [audioFile.player play]; 504 | } */ 505 | // error creating the session or player 506 | [self onStatus:MEDIA_ERROR mediaId:mediaId 507 | param:[self createMediaErrorWithCode:MEDIA_ERR_NONE_SUPPORTED message:nil]]; 508 | } 509 | } 510 | // else audioFile was nil - error already returned from audioFile for resource 511 | return; 512 | 513 | }]; 514 | } 515 | 516 | - (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId 517 | { 518 | BOOL bError = NO; 519 | NSError* __autoreleasing playerError = nil; 520 | 521 | // create the player 522 | NSURL* resourceURL = audioFile.resourceURL; 523 | 524 | if ([resourceURL isFileURL]) { 525 | audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:resourceURL error:&playerError]; 526 | } else { 527 | /* 528 | NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:resourceURL]; 529 | NSString* userAgent = [self.commandDelegate userAgent]; 530 | if (userAgent) { 531 | [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; 532 | } 533 | NSURLResponse* __autoreleasing response = nil; 534 | NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&playerError]; 535 | if (playerError) { 536 | NSLog(@"Unable to download audio from: %@", [resourceURL absoluteString]); 537 | } else { 538 | // bug in AVAudioPlayer when playing downloaded data in NSData - we have to download the file and play from disk 539 | CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); 540 | CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); 541 | NSString* filePath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], uuidString]; 542 | CFRelease(uuidString); 543 | CFRelease(uuidRef); 544 | [data writeToFile:filePath atomically:YES]; 545 | NSURL* fileURL = [NSURL fileURLWithPath:filePath]; 546 | audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&playerError]; 547 | } 548 | */ 549 | } 550 | 551 | if (playerError != nil) { 552 | NSLog(@"Failed to initialize AVAudioPlayer: %@\n", [playerError localizedDescription]); 553 | audioFile.player = nil; 554 | if (! keepAvAudioSessionAlwaysActive && self.avSession && ! [self isPlayingOrRecording]) { 555 | [self.avSession setActive:NO error:nil]; 556 | } 557 | bError = YES; 558 | } else { 559 | audioFile.player.mediaId = mediaId; 560 | audioFile.player.delegate = self; 561 | if (avPlayer == nil) 562 | bError = ![audioFile.player prepareToPlay]; 563 | } 564 | return bError; 565 | } 566 | 567 | - (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command 568 | { 569 | NSString* mediaId = [command argumentAtIndex:0]; 570 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 571 | 572 | if ((audioFile != nil) && (audioFile.player != nil)) { 573 | NSLog(@"Stopped playing audio sample '%@'", audioFile.resourcePath); 574 | [audioFile.player stop]; 575 | audioFile.player.currentTime = 0; 576 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_STOPPED)]; 577 | } 578 | // seek to start and pause 579 | if (avPlayer.currentItem && avPlayer.currentItem.asset) { 580 | BOOL isReadyToSeek = (avPlayer.status == AVPlayerStatusReadyToPlay) && (avPlayer.currentItem.status == AVPlayerItemStatusReadyToPlay); 581 | if (isReadyToSeek) { 582 | [avPlayer seekToTime: kCMTimeZero 583 | toleranceBefore: kCMTimeZero 584 | toleranceAfter: kCMTimeZero 585 | completionHandler: ^(BOOL finished){ 586 | if (finished) [avPlayer pause]; 587 | }]; 588 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_STOPPED)]; 589 | } else { 590 | // cannot seek, wrong state 591 | CDVMediaError errcode = MEDIA_ERR_NONE_ACTIVE; 592 | NSString* errMsg = @"Cannot service stop request until the avPlayer is in 'AVPlayerStatusReadyToPlay' state."; 593 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 594 | [self createMediaErrorWithCode:errcode message:errMsg]]; 595 | } 596 | } 597 | } 598 | 599 | - (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command 600 | { 601 | NSString* mediaId = [command argumentAtIndex:0]; 602 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 603 | 604 | if ((audioFile != nil) && ((audioFile.player != nil) || (avPlayer != nil))) { 605 | NSLog(@"Paused playing audio sample '%@'", audioFile.resourcePath); 606 | if (audioFile.player != nil) { 607 | [audioFile.player pause]; 608 | } else if (avPlayer != nil) { 609 | [avPlayer pause]; 610 | } 611 | 612 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_PAUSED)]; 613 | } 614 | } 615 | 616 | - (void)seekToAudio:(CDVInvokedUrlCommand*)command 617 | { 618 | // args: 619 | // 0 = Media id 620 | // 1 = seek to location in milliseconds 621 | 622 | NSString* mediaId = [command argumentAtIndex:0]; 623 | 624 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 625 | double position = [[command argumentAtIndex:1] doubleValue]; 626 | double posInSeconds = position / 1000; 627 | 628 | if ((audioFile != nil) && (audioFile.player != nil)) { 629 | 630 | if (posInSeconds >= audioFile.player.duration) { 631 | // The seek is past the end of file. Stop media and reset to beginning instead of seeking past the end. 632 | [audioFile.player stop]; 633 | audioFile.player.currentTime = 0; 634 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_STOPPED)]; 635 | } else { 636 | audioFile.player.currentTime = posInSeconds; 637 | [self onStatus:MEDIA_POSITION mediaId:mediaId param:@(posInSeconds)]; 638 | } 639 | 640 | } else if (avPlayer != nil) { 641 | int32_t timeScale = avPlayer.currentItem.asset.duration.timescale; 642 | CMTime timeToSeek = CMTimeMakeWithSeconds(posInSeconds, timeScale); 643 | 644 | BOOL isPlaying = (avPlayer.rate > 0 && !avPlayer.error); 645 | BOOL isReadyToSeek = (avPlayer.status == AVPlayerStatusReadyToPlay) && (avPlayer.currentItem.status == AVPlayerItemStatusReadyToPlay); 646 | float currentPlaybackRate = avPlayer.rate; 647 | 648 | // CB-10535: 649 | // When dealing with remote files, we can get into a situation where we start playing before AVPlayer has had the time to buffer the file to be played. 650 | // To avoid the app crashing in such a situation, we only seek if both the player and the player item are ready to play. If not ready, we send an error back to JS land. 651 | if(isReadyToSeek) { 652 | [avPlayer seekToTime: timeToSeek 653 | toleranceBefore: kCMTimeZero 654 | toleranceAfter: kCMTimeZero 655 | completionHandler: ^(BOOL finished) { 656 | if (isPlaying) { 657 | [avPlayer play]; 658 | // [avPlayer play] sets the rate to 1, so we need to set it again after seeking 659 | [avPlayer setRate:currentPlaybackRate]; 660 | }; 661 | }]; 662 | } else { 663 | NSString* errMsg = @"AVPlayerItem cannot service a seek request with a completion handler until its status is AVPlayerItemStatusReadyToPlay."; 664 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 665 | [self createAbortError:errMsg]]; 666 | } 667 | } 668 | } 669 | 670 | 671 | - (void)release:(CDVInvokedUrlCommand*)command 672 | { 673 | NSString* mediaId = [command argumentAtIndex:0]; 674 | //NSString* mediaId = self.currMediaId; 675 | 676 | if (mediaId != nil) { 677 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 678 | 679 | if (audioFile != nil) { 680 | if (audioFile.player && [audioFile.player isPlaying]) { 681 | [audioFile.player stop]; 682 | } 683 | if (audioFile.recorder && [audioFile.recorder isRecording]) { 684 | [audioFile.recorder stop]; 685 | } 686 | if (avPlayer != nil) { 687 | [avPlayer pause]; 688 | avPlayer = nil; 689 | } 690 | if (! keepAvAudioSessionAlwaysActive && self.avSession && ! [self isPlayingOrRecording]) { 691 | [self.avSession setCategory:AVAudioSessionCategorySoloAmbient error:nil]; 692 | [self.avSession setActive:NO error:nil]; 693 | self.avSession = nil; 694 | } 695 | [[self soundCache] removeObjectForKey:mediaId]; 696 | NSLog(@"Media with id %@ released", mediaId); 697 | } 698 | } 699 | } 700 | 701 | - (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command 702 | { 703 | NSString* callbackId = command.callbackId; 704 | NSString* mediaId = [command argumentAtIndex:0]; 705 | 706 | #pragma unused(mediaId) 707 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 708 | double position = -1; 709 | 710 | if ((audioFile != nil) && (audioFile.player != nil) && [audioFile.player isPlaying]) { 711 | position = round(audioFile.player.currentTime * 1000) / 1000; 712 | } 713 | if (avPlayer) { 714 | CMTime time = [avPlayer currentTime]; 715 | position = CMTimeGetSeconds(time); 716 | } 717 | 718 | if (isnan(position)){ 719 | position = -1; 720 | } 721 | 722 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:position]; 723 | 724 | [self onStatus:MEDIA_POSITION mediaId:mediaId param:@(position)]; 725 | [self.commandDelegate sendPluginResult:result callbackId:callbackId]; 726 | } 727 | 728 | - (void)startRecordingAudio:(CDVInvokedUrlCommand*)command 729 | { 730 | NSString* callbackId = command.callbackId; 731 | 732 | #pragma unused(callbackId) 733 | 734 | NSString* mediaId = [command argumentAtIndex:0]; 735 | CDVAudioFile* audioFile = [self audioFileForResource:[command argumentAtIndex:1] withId:mediaId doValidation:YES forRecording:YES]; 736 | __block NSString* errorMsg = @""; 737 | 738 | if ((audioFile != nil) && (audioFile.resourceURL != nil)) { 739 | 740 | __weak CDVSound* weakSelf = self; 741 | 742 | void (^startRecording)(void) = ^{ 743 | NSError* __autoreleasing error = nil; 744 | 745 | if (audioFile.recorder != nil) { 746 | [audioFile.recorder stop]; 747 | audioFile.recorder = nil; 748 | } 749 | // get the audioSession and set the category to allow recording when device is locked or ring/silent switch engaged 750 | if ([weakSelf hasAudioSession]) { 751 | if (![weakSelf.avSession.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { 752 | [weakSelf.avSession setCategory:AVAudioSessionCategoryRecord error:nil]; 753 | } 754 | 755 | if (![weakSelf.avSession setActive:YES error:&error]) { 756 | // other audio with higher priority that does not allow mixing could cause this to fail 757 | errorMsg = [NSString stringWithFormat:@"Unable to record audio: %@", [error localizedFailureReason]]; 758 | [weakSelf onStatus:MEDIA_ERROR mediaId:mediaId param: 759 | [self createAbortError:errorMsg]]; 760 | return; 761 | } 762 | } 763 | 764 | // create a new recorder for each start record 765 | bool isWav=[[audioFile.resourcePath pathExtension] isEqualToString:@"wav"]; 766 | NSMutableDictionary *audioSettings = [NSMutableDictionary dictionaryWithDictionary: 767 | @{AVSampleRateKey: @(44100), 768 | AVNumberOfChannelsKey: @(1), 769 | }]; 770 | if (isWav) { 771 | audioSettings[AVFormatIDKey]=@(kAudioFormatLinearPCM); 772 | audioSettings[AVLinearPCMBitDepthKey]=@(16); 773 | audioSettings[AVLinearPCMIsBigEndianKey]=@(false); 774 | audioSettings[AVLinearPCMIsFloatKey]=@(false); 775 | } else { 776 | audioSettings[AVFormatIDKey]=@(kAudioFormatMPEG4AAC); 777 | audioSettings[AVEncoderAudioQualityKey]=@(AVAudioQualityMedium); 778 | } 779 | audioFile.recorder = [[CDVAudioRecorder alloc] initWithURL:audioFile.resourceURL settings:audioSettings error:&error]; 780 | 781 | bool recordingSuccess = NO; 782 | if (error == nil) { 783 | audioFile.recorder.delegate = weakSelf; 784 | audioFile.recorder.mediaId = mediaId; 785 | audioFile.recorder.meteringEnabled = YES; 786 | recordingSuccess = [audioFile.recorder record]; 787 | if (recordingSuccess) { 788 | NSLog(@"Started recording audio sample '%@'", audioFile.resourcePath); 789 | [weakSelf onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_RUNNING)]; 790 | } 791 | } 792 | 793 | if ((error != nil) || (recordingSuccess == NO)) { 794 | if (error != nil) { 795 | errorMsg = [NSString stringWithFormat:@"Failed to initialize AVAudioRecorder: %@\n", [error localizedFailureReason]]; 796 | } else { 797 | errorMsg = @"Failed to start recording using AVAudioRecorder"; 798 | } 799 | audioFile.recorder = nil; 800 | if (! keepAvAudioSessionAlwaysActive && weakSelf.avSession && ! [self isPlayingOrRecording]) { 801 | [weakSelf.avSession setActive:NO error:nil]; 802 | } 803 | [weakSelf onStatus:MEDIA_ERROR mediaId:mediaId param: 804 | [self createAbortError:errorMsg]]; 805 | } 806 | }; 807 | 808 | SEL rrpSel = NSSelectorFromString(@"requestRecordPermission:"); 809 | if ([self hasAudioSession] && [self.avSession respondsToSelector:rrpSel]) 810 | { 811 | #pragma clang diagnostic push 812 | #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 813 | [self.avSession performSelector:rrpSel withObject:^(BOOL granted){ 814 | if (granted) { 815 | startRecording(); 816 | } else { 817 | NSString* msg = @"Error creating audio session, microphone permission denied."; 818 | NSLog(@"%@", msg); 819 | audioFile.recorder = nil; 820 | if (! keepAvAudioSessionAlwaysActive && weakSelf.avSession && ! [self isPlayingOrRecording]) { 821 | [weakSelf.avSession setActive:NO error:nil]; 822 | } 823 | [weakSelf onStatus:MEDIA_ERROR mediaId:mediaId param: 824 | [self createAbortError:msg]]; 825 | } 826 | }]; 827 | #pragma clang diagnostic pop 828 | } else { 829 | startRecording(); 830 | } 831 | 832 | } else { 833 | // file did not validate 834 | NSString* errorMsg = [NSString stringWithFormat:@"Could not record audio at '%@'", audioFile.resourcePath]; 835 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 836 | [self createAbortError:errorMsg]]; 837 | } 838 | } 839 | 840 | - (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command 841 | { 842 | NSString* mediaId = [command argumentAtIndex:0]; 843 | 844 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 845 | 846 | if ((audioFile != nil) && (audioFile.recorder != nil)) { 847 | NSLog(@"Stopped recording audio sample '%@'", audioFile.resourcePath); 848 | [audioFile.recorder stop]; 849 | // no callback - that will happen in audioRecorderDidFinishRecording 850 | } 851 | } 852 | 853 | - (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag 854 | { 855 | CDVAudioRecorder* aRecorder = (CDVAudioRecorder*)recorder; 856 | NSString* mediaId = aRecorder.mediaId; 857 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 858 | 859 | if (audioFile != nil) { 860 | NSLog(@"Finished recording audio sample '%@'", audioFile.resourcePath); 861 | } 862 | if (flag) { 863 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_STOPPED)]; 864 | } else { 865 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 866 | [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; 867 | } 868 | if (! keepAvAudioSessionAlwaysActive && self.avSession && ! [self isPlayingOrRecording]) { 869 | [self.avSession setActive:NO error:nil]; 870 | } 871 | } 872 | 873 | - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer*)player successfully:(BOOL)flag 874 | { 875 | //commented as unused 876 | CDVAudioPlayer* aPlayer = (CDVAudioPlayer*)player; 877 | NSString* mediaId = aPlayer.mediaId; 878 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 879 | 880 | if (audioFile != nil) { 881 | NSLog(@"Finished playing audio sample '%@'", audioFile.resourcePath); 882 | } 883 | if (flag) { 884 | audioFile.player.currentTime = 0; 885 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_STOPPED)]; 886 | } else { 887 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 888 | [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; 889 | } 890 | if (! keepAvAudioSessionAlwaysActive && self.avSession && ! [self isPlayingOrRecording]) { 891 | [self.avSession setActive:NO error:nil]; 892 | } 893 | } 894 | 895 | -(void)itemDidFinishPlaying:(NSNotification *) notification { 896 | // Will be called when AVPlayer finishes playing playerItem 897 | NSString* mediaId = self.currMediaId; 898 | 899 | if (! keepAvAudioSessionAlwaysActive && self.avSession && ! [self isPlayingOrRecording]) { 900 | [self.avSession setActive:NO error:nil]; 901 | } 902 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_STOPPED)]; 903 | } 904 | 905 | -(void)itemStalledPlaying:(NSNotification *) notification { 906 | // Will be called when playback stalls due to buffer empty 907 | NSLog(@"Stalled playback"); 908 | NSString* errMsg = @"stalled_playback"; 909 | NSString* mediaId = self.currMediaId; 910 | [self onStatus:MEDIA_ERROR mediaId:mediaId param: 911 | [self createAbortError:errMsg]]; 912 | } 913 | 914 | - (void)onMemoryWarning 915 | { 916 | /* https://issues.apache.org/jira/browse/CB-11513 */ 917 | NSMutableArray* keysToRemove = [[NSMutableArray alloc] init]; 918 | 919 | for(id key in [self soundCache]) { 920 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:key]; 921 | if (audioFile != nil) { 922 | if (audioFile.player != nil && ![audioFile.player isPlaying]) { 923 | [keysToRemove addObject:key]; 924 | } 925 | if (audioFile.recorder != nil && ![audioFile.recorder isRecording]) { 926 | [keysToRemove addObject:key]; 927 | } 928 | } 929 | } 930 | 931 | [[self soundCache] removeObjectsForKeys:keysToRemove]; 932 | 933 | // [[self soundCache] removeAllObjects]; 934 | // [self setSoundCache:nil]; 935 | [self setAvSession:nil]; 936 | 937 | [super onMemoryWarning]; 938 | } 939 | 940 | 941 | - (void)dealloc 942 | { 943 | [[self soundCache] removeAllObjects]; 944 | } 945 | 946 | - (void)onReset 947 | { 948 | for (CDVAudioFile* audioFile in [[self soundCache] allValues]) { 949 | if (audioFile != nil) { 950 | if (audioFile.player != nil) { 951 | [audioFile.player stop]; 952 | audioFile.player.currentTime = 0; 953 | } 954 | if (audioFile.recorder != nil) { 955 | [audioFile.recorder stop]; 956 | } 957 | } 958 | } 959 | 960 | [[self soundCache] removeAllObjects]; 961 | } 962 | 963 | - (void)getCurrentAmplitudeAudio:(CDVInvokedUrlCommand*)command 964 | { 965 | NSString* callbackId = command.callbackId; 966 | NSString* mediaId = [command argumentAtIndex:0]; 967 | 968 | #pragma unused(mediaId) 969 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 970 | float amplitude = 0; // The linear 0.0 .. 1.0 value 971 | 972 | if ((audioFile != nil) && (audioFile.recorder != nil) && [audioFile.recorder isRecording]) { 973 | [audioFile.recorder updateMeters]; 974 | float minDecibels = -60.0f; // Or use -60dB, which I measured in a silent room. 975 | float decibels = [audioFile.recorder averagePowerForChannel:0]; 976 | if (decibels < minDecibels) { 977 | amplitude = 0.0f; 978 | } else if (decibels >= 0.0f) { 979 | amplitude = 1.0f; 980 | } else { 981 | float root = 2.0f; 982 | float minAmp = powf(10.0f, 0.05f * minDecibels); 983 | float inverseAmpRange = 1.0f / (1.0f - minAmp); 984 | float amp = powf(10.0f, 0.05f * decibels); 985 | float adjAmp = (amp - minAmp) * inverseAmpRange; 986 | amplitude = powf(adjAmp, 1.0f / root); 987 | } 988 | } 989 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:amplitude]; 990 | [self.commandDelegate sendPluginResult:result callbackId:callbackId]; 991 | } 992 | 993 | - (void)resumeRecordingAudio:(CDVInvokedUrlCommand*)command 994 | { 995 | NSString* mediaId = [command argumentAtIndex:0]; 996 | 997 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 998 | 999 | if ((audioFile != nil) && (audioFile.recorder != nil)) { 1000 | NSLog(@"Resumed recording audio sample '%@'", audioFile.resourcePath); 1001 | [audioFile.recorder record]; 1002 | // no callback - that will happen in audioRecorderDidFinishRecording 1003 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_RUNNING)]; 1004 | } 1005 | 1006 | } 1007 | 1008 | - (void)pauseRecordingAudio:(CDVInvokedUrlCommand*)command 1009 | { 1010 | NSString* mediaId = [command argumentAtIndex:0]; 1011 | 1012 | CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; 1013 | 1014 | if ((audioFile != nil) && (audioFile.recorder != nil)) { 1015 | NSLog(@"Paused recording audio sample '%@'", audioFile.resourcePath); 1016 | [audioFile.recorder pause]; 1017 | // no callback - that will happen in audioRecorderDidFinishRecording 1018 | [self onStatus:MEDIA_STATE mediaId:mediaId param:@(MEDIA_PAUSED)]; 1019 | } 1020 | } 1021 | 1022 | - (void)messageChannel:(CDVInvokedUrlCommand*)command 1023 | { 1024 | self.statusCallbackId = command.callbackId; 1025 | } 1026 | 1027 | - (void)onStatus:(CDVMediaMsg)what mediaId:(NSString*)mediaId param:(NSObject*)param 1028 | { 1029 | if (self.statusCallbackId!=nil) { //new way, android compatible 1030 | NSMutableDictionary* status=[NSMutableDictionary dictionary]; 1031 | status[@"msgType"] = @(what); 1032 | //in the error case contains a dict with "code" and "message" 1033 | //otherwise a NSNumber 1034 | status[@"value"] = param; 1035 | status[@"id"] = mediaId; 1036 | NSMutableDictionary* dict=[NSMutableDictionary dictionary]; 1037 | dict[@"action"] = @"status"; 1038 | dict[@"status"] = status; 1039 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dict]; 1040 | [result setKeepCallbackAsBool:YES]; //we keep this forever 1041 | [self.commandDelegate sendPluginResult:result callbackId:self.statusCallbackId]; 1042 | } else { //old school evalJs way 1043 | if (what==MEDIA_ERROR) { 1044 | NSData* jsonData = [NSJSONSerialization dataWithJSONObject:param options:0 error:nil]; 1045 | param=[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 1046 | } 1047 | NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", 1048 | @"cordova.require('cordova-plugin-media.Media').onStatus", 1049 | mediaId, (int)what, param]; 1050 | [self.commandDelegate evalJs:jsString]; 1051 | } 1052 | } 1053 | 1054 | -(BOOL) isPlayingOrRecording 1055 | { 1056 | for(NSString* mediaId in soundCache) { 1057 | CDVAudioFile* audioFile = [soundCache objectForKey:mediaId]; 1058 | if (audioFile.player && [audioFile.player isPlaying]) { 1059 | return true; 1060 | } 1061 | if (audioFile.recorder && [audioFile.recorder isRecording]) { 1062 | return true; 1063 | } 1064 | } 1065 | return false; 1066 | } 1067 | 1068 | @end 1069 | 1070 | @implementation CDVAudioFile 1071 | 1072 | @synthesize resourcePath; 1073 | @synthesize resourceURL; 1074 | @synthesize player, volume, rate; 1075 | @synthesize recorder; 1076 | 1077 | @end 1078 | @implementation CDVAudioPlayer 1079 | @synthesize mediaId; 1080 | 1081 | @end 1082 | 1083 | @implementation CDVAudioRecorder 1084 | @synthesize mediaId; 1085 | 1086 | @end 1087 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-media-tests", 3 | "version": "7.0.1-dev", 4 | "description": "", 5 | "cordova": { 6 | "id": "cordova-plugin-media-tests", 7 | "platforms": [] 8 | }, 9 | "keywords": [ 10 | "ecosystem:cordova" 11 | ], 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /tests/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | Cordova Media Plugin Tests 26 | Apache 2.0 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Apache Cordova Media plugin 2 | // Project: https://github.com/apache/cordova-plugin-media 3 | // Definitions by: Microsoft Open Technologies Inc 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // 6 | // Copyright (c) Microsoft Open Technologies Inc 7 | // Licensed under the MIT license 8 | 9 | declare var Media: { 10 | new ( 11 | src: string, 12 | mediaSuccess: () => void, 13 | mediaError?: (error: MediaError) => any, 14 | mediaStatus?: (status: number) => void): Media; 15 | //Media statuses 16 | MEDIA_NONE: number; 17 | MEDIA_STARTING: number; 18 | MEDIA_RUNNING: number; 19 | MEDIA_PAUSED: number; 20 | MEDIA_STOPPED: number 21 | }; 22 | 23 | /** 24 | * This plugin provides the ability to record and play back audio files on a device. 25 | * NOTE: The current implementation does not adhere to a W3C specification for media capture, 26 | * and is provided for convenience only. A future implementation will adhere to the latest 27 | * W3C specification and may deprecate the current APIs. 28 | */ 29 | interface Media { 30 | /** 31 | * Returns the current amplitude within an audio file. 32 | * @param mediaSuccess The callback that is passed the current amplitude (0.0 - 1.0). 33 | * @param mediaError The callback to execute if an error occurs. 34 | */ 35 | getCurrentAmplitude( 36 | mediaSuccess: (amplitude: number) => void, 37 | mediaError?: (error: MediaError) => void): void; 38 | /** 39 | * Returns the current position within an audio file. Also updates the Media object's position parameter. 40 | * @param mediaSuccess The callback that is passed the current position in seconds. 41 | * @param mediaError The callback to execute if an error occurs. 42 | */ 43 | getCurrentPosition( 44 | mediaSuccess: (position: number) => void, 45 | mediaError?: (error: MediaError) => void): void; 46 | /** Returns the duration of an audio file in seconds. If the duration is unknown, it returns a value of -1. */ 47 | getDuration(): number; 48 | /** 49 | * Starts or resumes playing an audio file. 50 | * @param iosPlayOptions: iOS options quirks 51 | */ 52 | play(iosPlayOptions?: IosPlayOptions): void; 53 | /** Pauses playing an audio file. */ 54 | pause(): void; 55 | /** 56 | * Releases the underlying operating system's audio resources. This is particularly important 57 | * for Android, since there are a finite amount of OpenCore instances for media playback. 58 | * Applications should call the release function for any Media resource that is no longer needed. 59 | */ 60 | release(): void; 61 | /** 62 | * Sets the current position within an audio file. 63 | * @param position Position in milliseconds. 64 | */ 65 | seekTo(position: number): void; 66 | /** 67 | * Set the volume for an audio file. 68 | * @param volume The volume to set for playback. The value must be within the range of 0.0 to 1.0. 69 | */ 70 | setVolume(volume: number): void; 71 | /** Starts recording an audio file. */ 72 | startRecord(): void; 73 | /** Stops recording an audio file. */ 74 | stopRecord(): void; 75 | /** Stops playing an audio file. */ 76 | stop(): void; 77 | /** 78 | * The position within the audio playback, in seconds. 79 | * Not automatically updated during play; call getCurrentPosition to update. 80 | */ 81 | position: number; 82 | /** The duration of the media, in seconds. */ 83 | duration: number; 84 | } 85 | /** 86 | * iOS optional parameters for media.play 87 | * See https://github.com/apache/cordova-plugin-media#ios-quirks 88 | */ 89 | interface IosPlayOptions { 90 | numberOfLoops?: number; 91 | playAudioWhenScreenIsLocked?: boolean; 92 | } 93 | -------------------------------------------------------------------------------- /www/Media.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | * 20 | */ 21 | 22 | /* global cordova */ 23 | 24 | var argscheck = require('cordova/argscheck'); 25 | var utils = require('cordova/utils'); 26 | var exec = require('cordova/exec'); 27 | 28 | var mediaObjects = {}; 29 | 30 | /** 31 | * This class provides access to the device media, interfaces to both sound and video 32 | * 33 | * @constructor 34 | * @param src The file name or url to play 35 | * @param successCallback The callback to be called when the file is done playing or recording. 36 | * successCallback() 37 | * @param errorCallback The callback to be called if there is an error. 38 | * errorCallback(int errorCode) - OPTIONAL 39 | * @param statusCallback The callback to be called when media status has changed. 40 | * statusCallback(int statusCode) - OPTIONAL 41 | * 42 | * @param durationUpdateCallback The callback to be called when the duration updates. 43 | * durationUpdateCallback(float duration) - OPTIONAL 44 | * 45 | */ 46 | var Media = function (src, successCallback, errorCallback, statusCallback, durationUpdateCallback) { 47 | argscheck.checkArgs('sFFF', 'Media', arguments); 48 | this.id = utils.createUUID(); 49 | mediaObjects[this.id] = this; 50 | this.src = src; 51 | this.successCallback = successCallback; 52 | this.errorCallback = errorCallback; 53 | this.statusCallback = statusCallback; 54 | this.durationUpdateCallback = durationUpdateCallback; 55 | this._duration = -1; 56 | this._position = -1; 57 | exec(null, this.errorCallback, 'Media', 'create', [this.id, this.src]); 58 | }; 59 | 60 | // Media messages 61 | Media.MEDIA_STATE = 1; 62 | Media.MEDIA_DURATION = 2; 63 | Media.MEDIA_POSITION = 3; 64 | Media.MEDIA_ERROR = 9; 65 | 66 | // Media states 67 | Media.MEDIA_NONE = 0; 68 | Media.MEDIA_STARTING = 1; 69 | Media.MEDIA_RUNNING = 2; 70 | Media.MEDIA_PAUSED = 3; 71 | Media.MEDIA_STOPPED = 4; 72 | Media.MEDIA_MSG = ['None', 'Starting', 'Running', 'Paused', 'Stopped']; 73 | 74 | // "static" function to return existing objs. 75 | Media.get = function (id) { 76 | return mediaObjects[id]; 77 | }; 78 | 79 | /** 80 | * Start or resume playing audio file. 81 | */ 82 | Media.prototype.play = function (options) { 83 | exec(null, null, 'Media', 'startPlayingAudio', [this.id, this.src, options]); 84 | }; 85 | 86 | /** 87 | * Stop playing audio file. 88 | */ 89 | Media.prototype.stop = function () { 90 | var me = this; 91 | exec( 92 | function () { 93 | me._position = 0; 94 | }, 95 | this.errorCallback, 96 | 'Media', 97 | 'stopPlayingAudio', 98 | [this.id] 99 | ); 100 | }; 101 | 102 | /** 103 | * Seek or jump to a new time in the track.. 104 | */ 105 | Media.prototype.seekTo = function (milliseconds) { 106 | var me = this; 107 | exec( 108 | function (p) { 109 | me._position = p; 110 | }, 111 | this.errorCallback, 112 | 'Media', 113 | 'seekToAudio', 114 | [this.id, milliseconds] 115 | ); 116 | }; 117 | 118 | /** 119 | * Pause playing audio file. 120 | */ 121 | Media.prototype.pause = function () { 122 | exec(null, this.errorCallback, 'Media', 'pausePlayingAudio', [this.id]); 123 | }; 124 | 125 | /** 126 | * Get duration of an audio file. 127 | * The duration is only set for audio that is playing, paused or stopped. 128 | * 129 | * @return duration or -1 if not known. 130 | */ 131 | Media.prototype.getDuration = function () { 132 | return this._duration; 133 | }; 134 | 135 | /** 136 | * Get position of audio. 137 | */ 138 | Media.prototype.getCurrentPosition = function (success, fail) { 139 | var me = this; 140 | exec( 141 | function (p) { 142 | me._position = p; 143 | success(p); 144 | }, 145 | fail, 146 | 'Media', 147 | 'getCurrentPositionAudio', 148 | [this.id] 149 | ); 150 | }; 151 | 152 | /** 153 | * Start recording audio file. 154 | */ 155 | Media.prototype.startRecord = function () { 156 | exec(null, this.errorCallback, 'Media', 'startRecordingAudio', [this.id, this.src]); 157 | }; 158 | 159 | /** 160 | * Stop recording audio file. 161 | */ 162 | Media.prototype.stopRecord = function () { 163 | exec(null, this.errorCallback, 'Media', 'stopRecordingAudio', [this.id]); 164 | }; 165 | 166 | /** 167 | * Pause recording audio file. 168 | */ 169 | Media.prototype.pauseRecord = function () { 170 | exec(null, this.errorCallback, 'Media', 'pauseRecordingAudio', [this.id]); 171 | }; 172 | 173 | /** 174 | * Resume recording audio file. 175 | */ 176 | Media.prototype.resumeRecord = function () { 177 | exec(null, this.errorCallback, 'Media', 'resumeRecordingAudio', [this.id]); 178 | }; 179 | 180 | /** 181 | * Release the resources. 182 | */ 183 | Media.prototype.release = function () { 184 | var me = this; 185 | exec( 186 | function () { 187 | delete mediaObjects[me.id]; 188 | }, 189 | this.errorCallback, 190 | 'Media', 191 | 'release', 192 | [this.id] 193 | ); 194 | }; 195 | 196 | /** 197 | * Adjust the volume. 198 | */ 199 | Media.prototype.setVolume = function (volume) { 200 | exec(null, null, 'Media', 'setVolume', [this.id, volume]); 201 | }; 202 | 203 | /** 204 | * Adjust the playback rate. 205 | */ 206 | Media.prototype.setRate = function (rate) { 207 | if (cordova.platformId === 'ios' || cordova.platformId === 'android') { 208 | exec(null, null, 'Media', 'setRate', [this.id, rate]); 209 | } else { 210 | console.warn('media.setRate method is currently not supported for', cordova.platformId, 'platform.'); 211 | } 212 | }; 213 | 214 | /** 215 | * Get amplitude of audio. 216 | */ 217 | Media.prototype.getCurrentAmplitude = function (success, fail) { 218 | exec( 219 | function (p) { 220 | success(p); 221 | }, 222 | fail, 223 | 'Media', 224 | 'getCurrentAmplitudeAudio', 225 | [this.id] 226 | ); 227 | }; 228 | 229 | /** 230 | * Audio has status update. 231 | * PRIVATE 232 | * 233 | * @param id The media object id (string) 234 | * @param msgType The 'type' of update this is 235 | * @param value Use of value is determined by the msgType 236 | */ 237 | Media.onStatus = function (id, msgType, value) { 238 | var media = mediaObjects[id]; 239 | 240 | if (media) { 241 | switch (msgType) { 242 | case Media.MEDIA_STATE: 243 | if (media.statusCallback) { 244 | media.statusCallback(value); 245 | } 246 | if (value === Media.MEDIA_STOPPED) { 247 | if (media.successCallback) { 248 | media.successCallback(); 249 | } 250 | } 251 | break; 252 | case Media.MEDIA_DURATION: 253 | media._duration = value; 254 | if (media.durationUpdateCallback) { 255 | media.durationUpdateCallback(value); 256 | } 257 | break; 258 | case Media.MEDIA_ERROR: 259 | if (media.errorCallback) { 260 | media.errorCallback(value); 261 | } 262 | break; 263 | case Media.MEDIA_POSITION: 264 | media._position = Number(value); 265 | break; 266 | default: 267 | if (console.error) { 268 | console.error('Unhandled Media.onStatus :: ' + msgType); 269 | } 270 | break; 271 | } 272 | } else if (console.error) { 273 | console.error('Received Media.onStatus callback for unknown media :: ' + id); 274 | } 275 | }; 276 | 277 | module.exports = Media; 278 | 279 | function onMessageFromNative (msg) { 280 | if (msg.action === 'status') { 281 | Media.onStatus(msg.status.id, msg.status.msgType, msg.status.value); 282 | } else { 283 | throw new Error('Unknown media action' + msg.action); 284 | } 285 | } 286 | 287 | if (cordova.platformId === 'android') { 288 | var channel = require('cordova/channel'); 289 | 290 | channel.createSticky('onMediaPluginReady'); 291 | channel.waitForInitialization('onMediaPluginReady'); 292 | 293 | channel.onCordovaReady.subscribe(function () { 294 | exec(onMessageFromNative, undefined, 'Media', 'messageChannel', []); 295 | channel.initializationComplete('onMediaPluginReady'); 296 | }); 297 | } 298 | -------------------------------------------------------------------------------- /www/MediaError.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | * 20 | */ 21 | 22 | /** 23 | * This class contains information about any Media errors. 24 | */ 25 | /* 26 | According to :: http://dev.w3.org/html5/spec-author-view/video.html#mediaerror 27 | We should never be creating these objects, we should just implement the interface 28 | which has 1 property for an instance, 'code' 29 | 30 | instead of doing : 31 | errorCallbackFunction( new MediaError(3,'msg') ); 32 | we should simply use a literal : 33 | errorCallbackFunction( {'code':3} ); 34 | */ 35 | 36 | var _MediaError = window.MediaError; 37 | 38 | if (!_MediaError) { 39 | window.MediaError = _MediaError = function (code, msg) { 40 | this.code = typeof code !== 'undefined' ? code : null; 41 | this.message = msg || ''; // message is NON-standard! do not use! 42 | }; 43 | } 44 | 45 | _MediaError.MEDIA_ERR_NONE_ACTIVE = _MediaError.MEDIA_ERR_NONE_ACTIVE || 0; 46 | _MediaError.MEDIA_ERR_ABORTED = _MediaError.MEDIA_ERR_ABORTED || 1; 47 | _MediaError.MEDIA_ERR_NETWORK = _MediaError.MEDIA_ERR_NETWORK || 2; 48 | _MediaError.MEDIA_ERR_DECODE = _MediaError.MEDIA_ERR_DECODE || 3; 49 | _MediaError.MEDIA_ERR_NONE_SUPPORTED = _MediaError.MEDIA_ERR_NONE_SUPPORTED || 4; 50 | // TODO: MediaError.MEDIA_ERR_NONE_SUPPORTED is legacy, the W3 spec now defines it as below. 51 | // as defined by http://dev.w3.org/html5/spec-author-view/video.html#error-codes 52 | _MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = _MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED || 4; 53 | 54 | module.exports = _MediaError; 55 | -------------------------------------------------------------------------------- /www/browser/Media.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | * 20 | */ 21 | 22 | /* global MediaError */ 23 | 24 | var argscheck = require('cordova/argscheck'); 25 | var utils = require('cordova/utils'); 26 | 27 | var mediaObjects = {}; 28 | 29 | /** 30 | * This class provides access to the device media, interfaces to both sound and video 31 | * 32 | * @constructor 33 | * @param src The file name or url to play 34 | * @param successCallback The callback to be called when the file is done playing or recording. 35 | * successCallback() 36 | * @param errorCallback The callback to be called if there is an error. 37 | * errorCallback(int errorCode) - OPTIONAL 38 | * @param statusCallback The callback to be called when media status has changed. 39 | * statusCallback(int statusCode) - OPTIONAL 40 | */ 41 | var Media = function (src, successCallback, errorCallback, statusCallback) { 42 | argscheck.checkArgs('SFFF', 'Media', arguments); 43 | this.id = utils.createUUID(); 44 | mediaObjects[this.id] = this; 45 | this.src = src; 46 | this.successCallback = successCallback; 47 | this.errorCallback = errorCallback; 48 | this.statusCallback = statusCallback; 49 | this._duration = -1; 50 | this._position = -1; 51 | 52 | try { 53 | this.node = createNode(this); 54 | } catch (err) { 55 | Media.onStatus(this.id, Media.MEDIA_ERROR, { 56 | code: MediaError.MEDIA_ERR_ABORTED 57 | }); 58 | } 59 | }; 60 | 61 | /** 62 | * Creates new Audio node and with necessary event listeners attached 63 | * @param {Media} media Media object 64 | * @return {Audio} Audio element 65 | */ 66 | function createNode (media) { 67 | var node = new Audio(); 68 | 69 | node.onplay = function () { 70 | Media.onStatus(media.id, Media.MEDIA_STATE, Media.MEDIA_STARTING); 71 | }; 72 | 73 | node.onplaying = function () { 74 | Media.onStatus(media.id, Media.MEDIA_STATE, Media.MEDIA_RUNNING); 75 | }; 76 | 77 | node.ondurationchange = function (e) { 78 | Media.onStatus(media.id, Media.MEDIA_DURATION, e.target.duration || -1); 79 | }; 80 | 81 | node.onerror = function (e) { 82 | // Due to media.spec.15 It should return MediaError for bad filename 83 | var err = e.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ? { code: MediaError.MEDIA_ERR_ABORTED } : e.target.error; 84 | 85 | Media.onStatus(media.id, Media.MEDIA_ERROR, err); 86 | }; 87 | 88 | node.onended = function () { 89 | Media.onStatus(media.id, Media.MEDIA_STATE, Media.MEDIA_STOPPED); 90 | }; 91 | 92 | if (media.src) { 93 | node.src = media.src; 94 | } 95 | 96 | return node; 97 | } 98 | 99 | // Media messages 100 | Media.MEDIA_STATE = 1; 101 | Media.MEDIA_DURATION = 2; 102 | Media.MEDIA_POSITION = 3; 103 | Media.MEDIA_ERROR = 9; 104 | 105 | // Media states 106 | Media.MEDIA_NONE = 0; 107 | Media.MEDIA_STARTING = 1; 108 | Media.MEDIA_RUNNING = 2; 109 | Media.MEDIA_PAUSED = 3; 110 | Media.MEDIA_STOPPED = 4; 111 | Media.MEDIA_MSG = ['None', 'Starting', 'Running', 'Paused', 'Stopped']; 112 | 113 | /** 114 | * Start or resume playing audio file. 115 | */ 116 | Media.prototype.play = function () { 117 | // if Media was released, then node will be null and we need to create it again 118 | if (!this.node) { 119 | try { 120 | this.node = createNode(this); 121 | } catch (err) { 122 | Media.onStatus(this.id, Media.MEDIA_ERROR, { 123 | code: MediaError.MEDIA_ERR_ABORTED 124 | }); 125 | } 126 | } 127 | 128 | this.node.play(); 129 | }; 130 | 131 | /** 132 | * Stop playing audio file. 133 | */ 134 | Media.prototype.stop = function () { 135 | try { 136 | this.pause(); 137 | this.seekTo(0); 138 | Media.onStatus(this.id, Media.MEDIA_STATE, Media.MEDIA_STOPPED); 139 | } catch (err) { 140 | Media.onStatus(this.id, Media.MEDIA_ERROR, err); 141 | } 142 | }; 143 | 144 | /** 145 | * Seek or jump to a new time in the track.. 146 | */ 147 | Media.prototype.seekTo = function (milliseconds) { 148 | try { 149 | this.node.currentTime = milliseconds / 1000; 150 | } catch (err) { 151 | Media.onStatus(this.id, Media.MEDIA_ERROR, err); 152 | } 153 | }; 154 | 155 | /** 156 | * Pause playing audio file. 157 | */ 158 | Media.prototype.pause = function () { 159 | try { 160 | this.node.pause(); 161 | Media.onStatus(this.id, Media.MEDIA_STATE, Media.MEDIA_PAUSED); 162 | } catch (err) { 163 | Media.onStatus(this.id, Media.MEDIA_ERROR, err); 164 | } 165 | }; 166 | 167 | /** 168 | * Get duration of an audio file. 169 | * The duration is only set for audio that is playing, paused or stopped. 170 | * 171 | * @return duration or -1 if not known. 172 | */ 173 | Media.prototype.getDuration = function () { 174 | return this._duration; 175 | }; 176 | 177 | /** 178 | * Get position of audio. 179 | */ 180 | Media.prototype.getCurrentPosition = function (success, fail) { 181 | try { 182 | var p = this.node.currentTime; 183 | Media.onStatus(this.id, Media.MEDIA_POSITION, p); 184 | success(p); 185 | } catch (err) { 186 | fail(err); 187 | } 188 | }; 189 | 190 | /** 191 | * Start recording audio file. 192 | */ 193 | Media.prototype.startRecord = function () { 194 | Media.onStatus(this.id, Media.MEDIA_ERROR, 'Not supported'); 195 | }; 196 | 197 | /** 198 | * Stop recording audio file. 199 | */ 200 | Media.prototype.stopRecord = function () { 201 | Media.onStatus(this.id, Media.MEDIA_ERROR, 'Not supported'); 202 | }; 203 | 204 | /** 205 | * Pause recording audio file. 206 | */ 207 | Media.prototype.pauseRecord = function () { 208 | Media.onStatus(this.id, Media.MEDIA_ERROR, 'Not supported'); 209 | }; 210 | 211 | /** 212 | * Returns the current amplitude of the current recording. 213 | */ 214 | Media.prototype.getCurrentAmplitude = function () { 215 | Media.onStatus(this.id, Media.MEDIA_ERROR, 'Not supported'); 216 | }; 217 | 218 | /** 219 | * Resume recording an audio file. 220 | */ 221 | Media.prototype.resumeRecord = function () { 222 | Media.onStatus(this.id, Media.MEDIA_ERROR, 'Not supported'); 223 | }; 224 | 225 | /** 226 | * Set rate of an autio file. 227 | */ 228 | Media.prototype.setRate = function () { 229 | Media.onStatus(this.id, Media.MEDIA_ERROR, 'Not supported'); 230 | }; 231 | 232 | /** 233 | * Release the resources. 234 | */ 235 | Media.prototype.release = function () { 236 | try { 237 | delete this.node; 238 | } catch (err) { 239 | Media.onStatus(this.id, Media.MEDIA_ERROR, err); 240 | } 241 | }; 242 | 243 | /** 244 | * Adjust the volume. 245 | */ 246 | Media.prototype.setVolume = function (volume) { 247 | this.node.volume = volume; 248 | }; 249 | 250 | /** 251 | * Audio has status update. 252 | * PRIVATE 253 | * 254 | * @param id The media object id (string) 255 | * @param msgType The 'type' of update this is 256 | * @param value Use of value is determined by the msgType 257 | */ 258 | Media.onStatus = function (id, msgType, value) { 259 | var media = mediaObjects[id]; 260 | 261 | if (media) { 262 | switch (msgType) { 263 | case Media.MEDIA_STATE: 264 | if (media.statusCallback) { 265 | media.statusCallback(value); 266 | } 267 | if (value === Media.MEDIA_STOPPED) { 268 | if (media.successCallback) { 269 | media.successCallback(); 270 | } 271 | } 272 | break; 273 | case Media.MEDIA_DURATION: 274 | media._duration = value; 275 | break; 276 | case Media.MEDIA_ERROR: 277 | if (media.errorCallback) { 278 | media.errorCallback(value); 279 | } 280 | break; 281 | case Media.MEDIA_POSITION: 282 | media._position = Number(value); 283 | break; 284 | default: 285 | if (console.error) { 286 | console.error('Unhandled Media.onStatus :: ' + msgType); 287 | } 288 | break; 289 | } 290 | } else if (console.error) { 291 | console.error('Received Media.onStatus callback for unknown media :: ' + id); 292 | } 293 | }; 294 | 295 | module.exports = Media; 296 | --------------------------------------------------------------------------------