├── .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 | [](https://github.com/apache/cordova-plugin-media/actions/workflows/android.yml) [](https://github.com/apache/cordova-plugin-media/actions/workflows/chrome.yml) [](https://github.com/apache/cordova-plugin-media/actions/workflows/ios.yml) [](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 |
--------------------------------------------------------------------------------