For freezer 0.3.0+
This commit is contained in:
commit
884bc7a269
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: ryanheise
|
53
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
53
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 1 backlog, bug
|
||||
assignees: ryanheise
|
||||
|
||||
---
|
||||
|
||||
<!-- ALL SECTIONS BELOW MUST BE COMPLETED -->
|
||||
**Which API doesn't behave as documented, and how does it misbehave?**
|
||||
Name here the specific methods or fields that are not behaving as documented, and explain clearly what is happening.
|
||||
|
||||
**Minimal reproduction project**
|
||||
Provide a link here using one of two options:
|
||||
1. Fork this repository and modify the example to reproduce the bug, then provide a link here.
|
||||
2. If the unmodified official example already reproduces the bug, just write "The example".
|
||||
|
||||
**To Reproduce (i.e. user steps, not code)**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Error messages**
|
||||
|
||||
```
|
||||
If applicable, copy & paste error message here, within the triple quotes to preserve formatting.
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. MacOS + version]
|
||||
- Browser [e.g. chrome, safari + version]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
|
||||
**Flutter SDK version**
|
||||
```
|
||||
insert output of "flutter doctor" here
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Support
|
||||
url: https://stackoverflow.com/search?q=just_audio
|
||||
about: Ask for help on Stack Overflow.
|
||||
- name: New to Flutter?
|
||||
url: https://gitter.im/flutter/flutter
|
||||
about: Chat with other Flutter developers on Gitter.
|
39
.github/ISSUE_TEMPLATE/documentation-request.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/documentation-request.md
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Documentation request
|
||||
about: Suggest an improvement to the documentation
|
||||
title: ''
|
||||
labels: 1 backlog, documentation
|
||||
assignees: ryanheise
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
PLEASE READ CAREFULLY!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
FOR YOUR DOCUMENTATION REQUEST TO BE PROCESSED, YOU WILL NEED
|
||||
TO FILL IN ALL SECTIONS BELOW. DON'T DELETE THE HEADINGS.
|
||||
|
||||
|
||||
THANK YOU :-D
|
||||
|
||||
|
||||
-->
|
||||
|
||||
**To which pages does your suggestion apply?**
|
||||
|
||||
- Direct URL 1
|
||||
- Direct URL 2
|
||||
- ...
|
||||
|
||||
**Quote the sentences(s) from the documentation to be improved (if any)**
|
||||
|
||||
> Insert here. (Skip if you are proposing an entirely new section.)
|
||||
|
||||
**Describe your suggestion**
|
||||
|
||||
...
|
37
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 1 backlog, enhancement
|
||||
assignees: ryanheise
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
PLEASE READ CAREFULLY!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
FOR YOUR FEATURE REQUEST TO BE PROCESSED, YOU WILL NEED
|
||||
TO FILL IN ALL SECTIONS BELOW. DON'T DELETE THE HEADINGS.
|
||||
|
||||
|
||||
THANK YOU :-D
|
||||
|
||||
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# Visual Studio Code related
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!**/ios/**/default.mode1v3
|
||||
!**/ios/**/default.mode2v3
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
10
.metadata
Normal file
10
.metadata
Normal file
@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 68587a0916366e9512a78df22c44163d041dd5f3
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
114
CHANGELOG.md
Normal file
114
CHANGELOG.md
Normal file
@ -0,0 +1,114 @@
|
||||
## 0.3.1
|
||||
|
||||
* Prevent hang in dispose
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Playlists
|
||||
* Looping
|
||||
* Shuffling
|
||||
* Composing
|
||||
* Clipping support added for iOS/macOS
|
||||
* New player state model consisting of:
|
||||
* playing: true/false
|
||||
* processingState: none/loading/buffering/ready/completed
|
||||
* Feature complete on iOS and macOS (except for DASH)
|
||||
* Improved example
|
||||
* Exception classes
|
||||
|
||||
## 0.2.2
|
||||
|
||||
* Fix dependencies for stable channel.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* Improve handling of headers.
|
||||
* Report setUrl errors and duration on web.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Support dynamic duration
|
||||
* Support seeking to end of live streams
|
||||
* Support request headers
|
||||
* V2 implementation
|
||||
* Report setUrl errors on iOS
|
||||
* setUrl throws exception if interrupted
|
||||
* Return null when duration is unknown
|
||||
|
||||
## 0.1.10
|
||||
|
||||
* Option to set audio session category on iOS.
|
||||
|
||||
## 0.1.9
|
||||
|
||||
* Bug fixes.
|
||||
|
||||
## 0.1.8
|
||||
|
||||
* Reduce distortion at slow speeds on iOS
|
||||
|
||||
## 0.1.7
|
||||
|
||||
* Minor bug fixes.
|
||||
|
||||
## 0.1.6
|
||||
|
||||
* Eliminate event lag over method channels.
|
||||
* Report setUrl errors on Android.
|
||||
* Report Icy Metadata on Android.
|
||||
* Bug fixes.
|
||||
|
||||
## 0.1.5
|
||||
|
||||
* Update dependencies and documentation.
|
||||
|
||||
## 0.1.4
|
||||
|
||||
* Add MacOS implementation.
|
||||
* Support cross-platform redirects on Android.
|
||||
* Bug fixes.
|
||||
|
||||
## 0.1.3
|
||||
|
||||
* Fix bug in web implementation.
|
||||
|
||||
## 0.1.2
|
||||
|
||||
* Broadcast how much audio has been buffered.
|
||||
|
||||
## 0.1.1
|
||||
|
||||
* Web implementation.
|
||||
* iOS option to minimize stalling.
|
||||
* Fix setAsset on iOS.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Separate buffering state from PlaybackState.
|
||||
* More permissive state transitions.
|
||||
* Support playing local files on iOS.
|
||||
|
||||
## 0.0.6
|
||||
|
||||
* Bug fixes.
|
||||
|
||||
## 0.0.5
|
||||
|
||||
* API change for audio clipping.
|
||||
* Performance improvements and bug fixes on Android.
|
||||
|
||||
## 0.0.4
|
||||
|
||||
* Remove reseeking hack.
|
||||
|
||||
## 0.0.3
|
||||
|
||||
* Feature to change audio speed.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
* iOS implementation for testing (may not work).
|
||||
|
||||
## 0.0.1
|
||||
|
||||
* Initial release with Android implementation.
|
229
LICENSE
Normal file
229
LICENSE
Normal file
@ -0,0 +1,229 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2020 Ryan Heise.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
==============================================================================
|
||||
|
||||
This software includes the ExoPlayer library which is licensed under the Apache
|
||||
License, Version 2.0.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
222
README.md
Normal file
222
README.md
Normal file
@ -0,0 +1,222 @@
|
||||
# just_audio
|
||||
|
||||
This Flutter plugin plays audio from URLs, files, assets, DASH/HLS streams and playlists. Furthermore, it can clip, concatenate, loop, shuffle and compose audio into complex arrangements with gapless playback. This plugin can be used with [audio_service](https://pub.dev/packages/audio_service) to play audio in the background and control playback from the lock screen, Android notifications, the iOS Control Center, and headset buttons.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Android | iOS | MacOS | Web |
|
||||
| ------- | :-------: | :-----: | :-----: | :-----: |
|
||||
| read from URL | ✅ | ✅ | ✅ | ✅ |
|
||||
| read from file | ✅ | ✅ | ✅ | |
|
||||
| read from asset | ✅ | ✅ | ✅ | |
|
||||
| request headers | ✅ | ✅ | ✅ | |
|
||||
| DASH | ✅ | | | |
|
||||
| HLS | ✅ | ✅ | ✅ | |
|
||||
| buffer status/position | ✅ | ✅ | ✅ | ✅ |
|
||||
| play/pause/seek | ✅ | ✅ | ✅ | ✅ |
|
||||
| set volume | ✅ | ✅ | ✅ | ✅ |
|
||||
| set speed | ✅ | ✅ | ✅ | ✅ |
|
||||
| clip audio | ✅ | ✅ | ✅ | ✅ |
|
||||
| playlists | ✅ | ✅ | ✅ | ✅ |
|
||||
| looping | ✅ | ✅ | ✅ | ✅ |
|
||||
| shuffle | ✅ | ✅ | ✅ | ✅ |
|
||||
| compose audio | ✅ | ✅ | ✅ | ✅ |
|
||||
| gapless playback | ✅ | ✅ | ✅ | |
|
||||
| report player errors | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
Please consider reporting any bugs you encounter [here](https://github.com/ryanheise/just_audio/issues) or submitting pull requests [here](https://github.com/ryanheise/just_audio/pulls).
|
||||
|
||||
## Example
|
||||
|
||||
![just_audio](https://user-images.githubusercontent.com/19899190/89558581-bf369080-d857-11ea-9376-3a5055284bab.png)
|
||||
|
||||
Initialisation:
|
||||
|
||||
```dart
|
||||
final player = AudioPlayer();
|
||||
var duration = await player.setUrl('https://foo.com/bar.mp3');
|
||||
```
|
||||
|
||||
Standard controls:
|
||||
|
||||
```dart
|
||||
player.play(); // Usually you don't want to wait for playback to finish.
|
||||
await player.seek(Duration(seconds: 10));
|
||||
await player.pause();
|
||||
```
|
||||
|
||||
Clipping audio:
|
||||
|
||||
```dart
|
||||
await player.setClip(start: Duration(seconds: 10), end: Duration(seconds: 20));
|
||||
await player.play(); // Waits until the clip has finished playing
|
||||
```
|
||||
Adjusting audio:
|
||||
|
||||
```dart
|
||||
await player.setSpeed(2.0); // Double speed
|
||||
await player.setVolume(0.5); // Halve volume
|
||||
```
|
||||
|
||||
Gapless playlists:
|
||||
|
||||
```dart
|
||||
await player.load(
|
||||
ConcatenatingAudioSource(
|
||||
children: [
|
||||
AudioSource.uri(Uri.parse("https://example.com/track1.mp3")),
|
||||
AudioSource.uri(Uri.parse("https://example.com/track2.mp3")),
|
||||
AudioSource.uri(Uri.parse("https://example.com/track3.mp3")),
|
||||
],
|
||||
),
|
||||
);
|
||||
player.seekToNext();
|
||||
player.seekToPrevious();
|
||||
// Jump to the beginning of track3.mp3.
|
||||
player.seek(Duration(milliseconds: 0), index: 2);
|
||||
```
|
||||
|
||||
Looping and shuffling:
|
||||
|
||||
```dart
|
||||
player.setLoopMode(LoopMode.off); // no looping (default)
|
||||
player.setLoopMode(LoopMode.all); // loop playlist
|
||||
player.setLoopMode(LoopMode.one); // loop current item
|
||||
player.setShuffleModeEnabled(true); // shuffle except for current item
|
||||
```
|
||||
|
||||
Composing audio sources:
|
||||
|
||||
```dart
|
||||
player.load(
|
||||
// Loop child 4 times
|
||||
LoopingAudioSource(
|
||||
count: 4,
|
||||
// Play children one after the other
|
||||
child: ConcatenatingAudioSource(
|
||||
children: [
|
||||
// Play a regular media file
|
||||
ProgressiveAudioSource(Uri.parse("https://example.com/foo.mp3")),
|
||||
// Play a DASH stream
|
||||
DashAudioSource(Uri.parse("https://example.com/audio.mdp")),
|
||||
// Play an HLS stream
|
||||
HlsAudioSource(Uri.parse("https://example.com/audio.m3u8")),
|
||||
// Play a segment of the child
|
||||
ClippingAudioSource(
|
||||
child: ProgressiveAudioSource(Uri.parse("https://w.xyz/p.mp3")),
|
||||
start: Duration(seconds: 25),
|
||||
end: Duration(seconds: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Releasing resources:
|
||||
|
||||
```dart
|
||||
await player.dispose();
|
||||
```
|
||||
|
||||
Catching player errors:
|
||||
|
||||
```dart
|
||||
try {
|
||||
await player.setUrl("https://s3.amazonaws.com/404-file.mp3");
|
||||
} catch (e) {
|
||||
print("Error: $e");
|
||||
}
|
||||
```
|
||||
|
||||
Listening to state changes:
|
||||
|
||||
```dart
|
||||
player.playerStateStream.listen((state) {
|
||||
if (state.playing) ... else ...
|
||||
switch (state.processingState) {
|
||||
case AudioPlaybackState.none: ...
|
||||
case AudioPlaybackState.loading: ...
|
||||
case AudioPlaybackState.buffering: ...
|
||||
case AudioPlaybackState.ready: ...
|
||||
case AudioPlaybackState.completed: ...
|
||||
}
|
||||
});
|
||||
|
||||
// See also:
|
||||
// - durationStream
|
||||
// - positionStream
|
||||
// - bufferedPositionStream
|
||||
// - currentIndexStream
|
||||
// - icyMetadataStream
|
||||
// - playingStream
|
||||
// - processingStateStream
|
||||
// - loopModeStream
|
||||
// - shuffleModeEnabledStream
|
||||
// - volumeStream
|
||||
// - speedStream
|
||||
// - playbackEventStream
|
||||
```
|
||||
|
||||
## Platform specific configuration
|
||||
|
||||
### Android
|
||||
|
||||
If you wish to connect to non-HTTPS URLS, add the following attribute to the `application` element of your `AndroidManifest.xml` file:
|
||||
|
||||
```xml
|
||||
<application ... android:usesCleartextTraffic="true">
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
If you wish to connect to non-HTTPS URLS, add the following to your `Info.plist` file:
|
||||
|
||||
```xml
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
```
|
||||
|
||||
By default, iOS will mute your app's audio when your phone is switched to
|
||||
silent mode. Depending on the requirements of your app, you can change the
|
||||
default audio session category using `AudioPlayer.setIosCategory`. For example,
|
||||
if you are writing a media app, Apple recommends that you set the category to
|
||||
`AVAudioSessionCategoryPlayback`, which you can achieve by adding the following
|
||||
code to your app's initialisation:
|
||||
|
||||
```dart
|
||||
AudioPlayer.setIosCategory(IosCategory.playback);
|
||||
```
|
||||
|
||||
Note: If your app uses a number of different audio plugins in combination, e.g.
|
||||
for audio recording, or text to speech, or background audio, it is possible
|
||||
that those plugins may internally override the setting you choose here. You may
|
||||
consider asking the developer of each other plugin you use to provide a similar
|
||||
method so that you can configure the same audio session category universally
|
||||
across all plugins you use.
|
||||
|
||||
### MacOS
|
||||
|
||||
To allow your MacOS application to access audio files on the Internet, add the following to your `DebugProfile.entitlements` and `Release.entitlements` files:
|
||||
|
||||
```xml
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
If you wish to connect to non-HTTPS URLS, add the following to your `Info.plist` file:
|
||||
|
||||
```xml
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
```
|
8
android/.gitignore
vendored
Normal file
8
android/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
48
android/build.gradle
Normal file
48
android/build.gradle
Normal file
@ -0,0 +1,48 @@
|
||||
group 'com.ryanheise.just_audio'
|
||||
version '1.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.4'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.11.4'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.11.4'
|
||||
compile files('libs/extension-flac.aar')
|
||||
}
|
4
android/gradle.properties
Normal file
4
android/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Mon Aug 10 13:15:44 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
BIN
android/libs/extension-flac.aar
Normal file
BIN
android/libs/extension-flac.aar
Normal file
Binary file not shown.
1
android/settings.gradle
Normal file
1
android/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
rootProject.name = 'just_audio'
|
3
android/src/main/AndroidManifest.xml
Normal file
3
android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.ryanheise.just_audio">
|
||||
</manifest>
|
729
android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java
Normal file
729
android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java
Normal file
@ -0,0 +1,729 @@
|
||||
package com.ryanheise.just_audio;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
||||
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
|
||||
import com.google.android.exoplayer2.metadata.icy.IcyInfo;
|
||||
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.LoopingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.EventChannel.EventSink;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
import com.ryanheise.just_audio.DeezerDataSource;
|
||||
|
||||
public class AudioPlayer implements MethodCallHandler, Player.EventListener, MetadataOutput {
|
||||
|
||||
static final String TAG = "AudioPlayer";
|
||||
|
||||
private static Random random = new Random();
|
||||
|
||||
private final Context context;
|
||||
private final MethodChannel methodChannel;
|
||||
private final EventChannel eventChannel;
|
||||
private EventSink eventSink;
|
||||
|
||||
private ProcessingState processingState;
|
||||
private long updateTime;
|
||||
private long updatePosition;
|
||||
private long bufferedPosition;
|
||||
private long duration;
|
||||
private Long start;
|
||||
private Long end;
|
||||
private Long seekPos;
|
||||
private Result prepareResult;
|
||||
private Result playResult;
|
||||
private Result seekResult;
|
||||
private boolean seekProcessed;
|
||||
private boolean playing;
|
||||
private Map<String, MediaSource> mediaSources = new HashMap<String, MediaSource>();
|
||||
private IcyInfo icyInfo;
|
||||
private IcyHeaders icyHeaders;
|
||||
private int errorCount;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private MediaSource mediaSource;
|
||||
private Integer currentIndex;
|
||||
private Map<LoopingMediaSource, MediaSource> loopingChildren = new HashMap<>();
|
||||
private Map<LoopingMediaSource, Integer> loopingCounts = new HashMap<>();
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private final Runnable bufferWatcher = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long newBufferedPosition = player.getBufferedPosition();
|
||||
if (newBufferedPosition != bufferedPosition) {
|
||||
bufferedPosition = newBufferedPosition;
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
switch (processingState) {
|
||||
case buffering:
|
||||
handler.postDelayed(this, 200);
|
||||
break;
|
||||
case ready:
|
||||
if (playing) {
|
||||
handler.postDelayed(this, 500);
|
||||
} else {
|
||||
handler.postDelayed(this, 1000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable onDispose;
|
||||
|
||||
public AudioPlayer(final Context applicationContext, final BinaryMessenger messenger,
|
||||
final String id, final Runnable onDispose) {
|
||||
this.context = applicationContext;
|
||||
this.onDispose = onDispose;
|
||||
methodChannel = new MethodChannel(messenger, "com.ryanheise.just_audio.methods." + id);
|
||||
methodChannel.setMethodCallHandler(this);
|
||||
eventChannel = new EventChannel(messenger, "com.ryanheise.just_audio.events." + id);
|
||||
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
|
||||
@Override
|
||||
public void onListen(final Object arguments, final EventSink eventSink) {
|
||||
AudioPlayer.this.eventSink = eventSink;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(final Object arguments) {
|
||||
eventSink = null;
|
||||
}
|
||||
});
|
||||
processingState = ProcessingState.none;
|
||||
}
|
||||
|
||||
private void startWatchingBuffer() {
|
||||
handler.removeCallbacks(bufferWatcher);
|
||||
handler.post(bufferWatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
final Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof IcyInfo) {
|
||||
icyInfo = (IcyInfo) entry;
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
for (int i = 0; i < trackGroups.length; i++) {
|
||||
TrackGroup trackGroup = trackGroups.get(i);
|
||||
|
||||
for (int j = 0; j < trackGroup.length; j++) {
|
||||
Metadata metadata = trackGroup.getFormat(j).metadata;
|
||||
|
||||
if (metadata != null) {
|
||||
for (int k = 0; k < metadata.length(); k++) {
|
||||
final Metadata.Entry entry = metadata.get(k);
|
||||
if (entry instanceof IcyHeaders) {
|
||||
icyHeaders = (IcyHeaders) entry;
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(int reason) {
|
||||
switch (reason) {
|
||||
case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
||||
case Player.DISCONTINUITY_REASON_SEEK:
|
||||
onItemMayHaveChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_DYNAMIC) {
|
||||
onItemMayHaveChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void onItemMayHaveChanged() {
|
||||
Integer newIndex = player.getCurrentWindowIndex();
|
||||
if (newIndex != currentIndex) {
|
||||
currentIndex = newIndex;
|
||||
}
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
if (prepareResult != null) {
|
||||
duration = getDuration();
|
||||
transition(ProcessingState.ready);
|
||||
prepareResult.success(duration);
|
||||
prepareResult = null;
|
||||
} else {
|
||||
transition(ProcessingState.ready);
|
||||
}
|
||||
if (seekProcessed) {
|
||||
completeSeek();
|
||||
}
|
||||
break;
|
||||
case Player.STATE_BUFFERING:
|
||||
if (processingState != ProcessingState.buffering) {
|
||||
transition(ProcessingState.buffering);
|
||||
startWatchingBuffer();
|
||||
}
|
||||
break;
|
||||
case Player.STATE_ENDED:
|
||||
if (processingState != ProcessingState.completed) {
|
||||
transition(ProcessingState.completed);
|
||||
}
|
||||
if (playResult != null) {
|
||||
playResult.success(null);
|
||||
playResult = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
switch (error.type) {
|
||||
case ExoPlaybackException.TYPE_SOURCE:
|
||||
Log.e(TAG, "TYPE_SOURCE: " + error.getSourceException().getMessage());
|
||||
break;
|
||||
|
||||
case ExoPlaybackException.TYPE_RENDERER:
|
||||
Log.e(TAG, "TYPE_RENDERER: " + error.getRendererException().getMessage());
|
||||
break;
|
||||
|
||||
case ExoPlaybackException.TYPE_UNEXPECTED:
|
||||
Log.e(TAG, "TYPE_UNEXPECTED: " + error.getUnexpectedException().getMessage());
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "default: " + error.getUnexpectedException().getMessage());
|
||||
}
|
||||
sendError(String.valueOf(error.type), error.getMessage());
|
||||
errorCount++;
|
||||
if (player.hasNext() && currentIndex != null && errorCount <= 5) {
|
||||
int nextIndex = currentIndex + 1;
|
||||
player.prepare(mediaSource);
|
||||
player.seekTo(nextIndex, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekProcessed() {
|
||||
if (seekResult != null) {
|
||||
seekProcessed = true;
|
||||
if (player.getPlaybackState() == Player.STATE_READY) {
|
||||
completeSeek();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void completeSeek() {
|
||||
seekProcessed = false;
|
||||
seekPos = null;
|
||||
seekResult.success(null);
|
||||
seekResult = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(final MethodCall call, final Result result) {
|
||||
ensurePlayerInitialized();
|
||||
|
||||
final List<?> args = (List<?>) call.arguments;
|
||||
try {
|
||||
switch (call.method) {
|
||||
case "load":
|
||||
load(getAudioSource(args.get(0)), result);
|
||||
break;
|
||||
case "play":
|
||||
play(result);
|
||||
break;
|
||||
case "pause":
|
||||
pause();
|
||||
result.success(null);
|
||||
break;
|
||||
case "setVolume":
|
||||
setVolume((float) ((double) ((Double) args.get(0))));
|
||||
result.success(null);
|
||||
break;
|
||||
case "setSpeed":
|
||||
setSpeed((float) ((double) ((Double) args.get(0))));
|
||||
result.success(null);
|
||||
break;
|
||||
case "setLoopMode":
|
||||
setLoopMode((Integer) args.get(0));
|
||||
result.success(null);
|
||||
break;
|
||||
case "setShuffleModeEnabled":
|
||||
setShuffleModeEnabled((Boolean) args.get(0));
|
||||
result.success(null);
|
||||
break;
|
||||
case "setAutomaticallyWaitsToMinimizeStalling":
|
||||
result.success(null);
|
||||
break;
|
||||
case "seek":
|
||||
Long position = getLong(args.get(0));
|
||||
Integer index = (Integer)args.get(1);
|
||||
seek(position == null ? C.TIME_UNSET : position, result, index);
|
||||
break;
|
||||
case "dispose":
|
||||
dispose();
|
||||
result.success(null);
|
||||
break;
|
||||
case "concatenating.add":
|
||||
concatenating(args.get(0))
|
||||
.addMediaSource(getAudioSource(args.get(1)), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.insert":
|
||||
concatenating(args.get(0))
|
||||
.addMediaSource((Integer)args.get(1), getAudioSource(args.get(2)), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.addAll":
|
||||
concatenating(args.get(0))
|
||||
.addMediaSources(getAudioSources(args.get(1)), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.insertAll":
|
||||
concatenating(args.get(0))
|
||||
.addMediaSources((Integer)args.get(1), getAudioSources(args.get(2)), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.removeAt":
|
||||
concatenating(args.get(0))
|
||||
.removeMediaSource((Integer)args.get(1), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.removeRange":
|
||||
concatenating(args.get(0))
|
||||
.removeMediaSourceRange((Integer)args.get(1), (Integer)args.get(2), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.move":
|
||||
concatenating(args.get(0))
|
||||
.moveMediaSource((Integer)args.get(1), (Integer)args.get(2), handler, () -> result.success(null));
|
||||
break;
|
||||
case "concatenating.clear":
|
||||
concatenating(args.get(0)).clear(handler, () -> result.success(null));
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
result.error("Illegal state: " + e.getMessage(), null, null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
result.error("Error: " + e, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the shuffle order for mediaSource, with currentIndex at
|
||||
// the first position. Traverse the tree incrementing index at each
|
||||
// node.
|
||||
private int setShuffleOrder(MediaSource mediaSource, int index) {
|
||||
if (mediaSource instanceof ConcatenatingMediaSource) {
|
||||
final ConcatenatingMediaSource source = (ConcatenatingMediaSource)mediaSource;
|
||||
// Find which child is current
|
||||
Integer currentChildIndex = null;
|
||||
for (int i = 0; i < source.getSize(); i++) {
|
||||
final int indexBefore = index;
|
||||
final MediaSource child = source.getMediaSource(i);
|
||||
index = setShuffleOrder(child, index);
|
||||
// If currentIndex falls within this child, make this child come first.
|
||||
if (currentIndex >= indexBefore && currentIndex < index) {
|
||||
currentChildIndex = i;
|
||||
}
|
||||
}
|
||||
// Shuffle so that the current child is first in the shuffle order
|
||||
source.setShuffleOrder(createShuffleOrder(source.getSize(), currentChildIndex));
|
||||
} else if (mediaSource instanceof LoopingMediaSource) {
|
||||
final LoopingMediaSource source = (LoopingMediaSource)mediaSource;
|
||||
// The ExoPlayer API doesn't provide accessors for these so we have
|
||||
// to index them ourselves.
|
||||
MediaSource child = loopingChildren.get(source);
|
||||
int count = loopingCounts.get(source);
|
||||
for (int i = 0; i < count; i++) {
|
||||
index = setShuffleOrder(child, index);
|
||||
}
|
||||
} else {
|
||||
// An actual media item takes up one spot in the playlist.
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int[] shuffle(int length, Integer firstIndex) {
|
||||
final int[] shuffleOrder = new int[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
final int j = random.nextInt(i + 1);
|
||||
shuffleOrder[i] = shuffleOrder[j];
|
||||
shuffleOrder[j] = i;
|
||||
}
|
||||
if (firstIndex != null) {
|
||||
for (int i = 1; i < length; i++) {
|
||||
if (shuffleOrder[i] == firstIndex) {
|
||||
final int v = shuffleOrder[0];
|
||||
shuffleOrder[0] = shuffleOrder[i];
|
||||
shuffleOrder[i] = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shuffleOrder;
|
||||
}
|
||||
|
||||
// Create a shuffle order optionally fixing the first index.
|
||||
private ShuffleOrder createShuffleOrder(int length, Integer firstIndex) {
|
||||
int[] shuffleIndices = shuffle(length, firstIndex);
|
||||
return new DefaultShuffleOrder(shuffleIndices, random.nextLong());
|
||||
}
|
||||
|
||||
private ConcatenatingMediaSource concatenating(final Object index) {
|
||||
return (ConcatenatingMediaSource)mediaSources.get((String)index);
|
||||
}
|
||||
|
||||
private MediaSource getAudioSource(final Object json) {
|
||||
Map<?, ?> map = (Map<?, ?>)json;
|
||||
String id = (String)map.get("id");
|
||||
MediaSource mediaSource = mediaSources.get(id);
|
||||
if (mediaSource == null) {
|
||||
mediaSource = decodeAudioSource(map);
|
||||
mediaSources.put(id, mediaSource);
|
||||
}
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
private MediaSource decodeAudioSource(final Object json) {
|
||||
Map<?, ?> map = (Map<?, ?>)json;
|
||||
String id = (String)map.get("id");
|
||||
switch ((String)map.get("type")) {
|
||||
case "progressive":
|
||||
Uri uri = Uri.parse((String)map.get("uri"));
|
||||
//Deezer
|
||||
if (uri.getHost() != null && uri.getHost().contains("dzcdn.net")) {
|
||||
//Track id is stored in URL fragment (after #)
|
||||
String fragment = uri.getFragment();
|
||||
uri = Uri.parse(((String)map.get("uri")).replace("#" + fragment, ""));
|
||||
return new ProgressiveMediaSource.Factory(
|
||||
() -> {
|
||||
HttpDataSource deezerDataSource = new DeezerDataSource(fragment);
|
||||
return deezerDataSource;
|
||||
}
|
||||
).setTag(id).createMediaSource(uri);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return new ProgressiveMediaSource.Factory(buildDataSourceFactory())
|
||||
.setTag(id)
|
||||
.createMediaSource(uri);
|
||||
case "dash":
|
||||
return new DashMediaSource.Factory(buildDataSourceFactory())
|
||||
.setTag(id)
|
||||
.createMediaSource(Uri.parse((String)map.get("uri")));
|
||||
case "hls":
|
||||
return new HlsMediaSource.Factory(buildDataSourceFactory())
|
||||
.setTag(id)
|
||||
.createMediaSource(Uri.parse((String)map.get("uri")));
|
||||
case "concatenating":
|
||||
List<Object> audioSources = (List<Object>)map.get("audioSources");
|
||||
return new ConcatenatingMediaSource(
|
||||
false, // isAtomic
|
||||
(Boolean)map.get("useLazyPreparation"),
|
||||
new DefaultShuffleOrder(audioSources.size()),
|
||||
audioSources
|
||||
.stream()
|
||||
.map(s -> getAudioSource(s))
|
||||
.toArray(MediaSource[]::new));
|
||||
case "clipping":
|
||||
Long start = getLong(map.get("start"));
|
||||
Long end = getLong(map.get("end"));
|
||||
return new ClippingMediaSource(getAudioSource(map.get("audioSource")),
|
||||
(start != null ? start : 0) * 1000L,
|
||||
(end != null ? end : C.TIME_END_OF_SOURCE) * 1000L);
|
||||
case "looping":
|
||||
Integer count = (Integer)map.get("count");
|
||||
MediaSource looperChild = getAudioSource(map.get("audioSource"));
|
||||
LoopingMediaSource looper = new LoopingMediaSource(looperChild, count);
|
||||
// TODO: store both in a single map
|
||||
loopingChildren.put(looper, looperChild);
|
||||
loopingCounts.put(looper, count);
|
||||
return looper;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown AudioSource type: " + map.get("type"));
|
||||
}
|
||||
}
|
||||
|
||||
private List<MediaSource> getAudioSources(final Object json) {
|
||||
return ((List<Object>)json)
|
||||
.stream()
|
||||
.map(s -> getAudioSource(s))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private DataSource.Factory buildDataSourceFactory() {
|
||||
String userAgent = Util.getUserAgent(context, "just_audio");
|
||||
DataSource.Factory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
|
||||
userAgent,
|
||||
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
true
|
||||
);
|
||||
return new DefaultDataSourceFactory(context, httpDataSourceFactory);
|
||||
}
|
||||
|
||||
private void load(final MediaSource mediaSource, final Result result) {
|
||||
switch (processingState) {
|
||||
case none:
|
||||
break;
|
||||
case loading:
|
||||
abortExistingConnection();
|
||||
player.stop();
|
||||
break;
|
||||
default:
|
||||
player.stop();
|
||||
break;
|
||||
}
|
||||
errorCount = 0;
|
||||
prepareResult = result;
|
||||
transition(ProcessingState.loading);
|
||||
if (player.getShuffleModeEnabled()) {
|
||||
setShuffleOrder(mediaSource, 0);
|
||||
}
|
||||
this.mediaSource = mediaSource;
|
||||
player.prepare(mediaSource);
|
||||
}
|
||||
|
||||
private void ensurePlayerInitialized() {
|
||||
if (player == null) {
|
||||
player = new SimpleExoPlayer.Builder(context).build();
|
||||
player.addMetadataOutput(this);
|
||||
player.addListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastPlaybackEvent() {
|
||||
final Map<String, Object> event = new HashMap<String, Object>();
|
||||
event.put("processingState", processingState.ordinal());
|
||||
event.put("updatePosition", updatePosition = getCurrentPosition());
|
||||
event.put("updateTime", updateTime = System.currentTimeMillis());
|
||||
event.put("bufferedPosition", Math.max(updatePosition, bufferedPosition));
|
||||
event.put("icyMetadata", collectIcyMetadata());
|
||||
event.put("duration", duration = getDuration());
|
||||
event.put("currentIndex", currentIndex);
|
||||
event.put("qualityString", null);
|
||||
|
||||
if (eventSink != null) {
|
||||
eventSink.success(event);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> collectIcyMetadata() {
|
||||
final Map<String, Object> icyData = new HashMap<>();
|
||||
if (icyInfo != null) {
|
||||
final Map<String, String> info = new HashMap<>();
|
||||
info.put("title", icyInfo.title);
|
||||
info.put("url", icyInfo.url);
|
||||
icyData.put("info", info);
|
||||
}
|
||||
if (icyHeaders != null) {
|
||||
final Map<String, Object> headers = new HashMap<>();
|
||||
headers.put("bitrate", icyHeaders.bitrate);
|
||||
headers.put("genre", icyHeaders.genre);
|
||||
headers.put("name", icyHeaders.name);
|
||||
headers.put("metadataInterval", icyHeaders.metadataInterval);
|
||||
headers.put("url", icyHeaders.url);
|
||||
headers.put("isPublic", icyHeaders.isPublic);
|
||||
icyData.put("headers", headers);
|
||||
}
|
||||
return icyData;
|
||||
}
|
||||
|
||||
private long getCurrentPosition() {
|
||||
if (processingState == ProcessingState.none || processingState == ProcessingState.loading) {
|
||||
return 0;
|
||||
} else if (seekPos != null && seekPos != C.TIME_UNSET) {
|
||||
return seekPos;
|
||||
} else {
|
||||
return player.getCurrentPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private long getDuration() {
|
||||
if (processingState == ProcessingState.none || processingState == ProcessingState.loading) {
|
||||
return C.TIME_UNSET;
|
||||
} else {
|
||||
return player.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(String errorCode, String errorMsg) {
|
||||
if (prepareResult != null) {
|
||||
prepareResult.error(errorCode, errorMsg, null);
|
||||
prepareResult = null;
|
||||
}
|
||||
|
||||
if (eventSink != null) {
|
||||
eventSink.error(errorCode, errorMsg, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void transition(final ProcessingState newState) {
|
||||
processingState = newState;
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
|
||||
private String getLowerCaseExtension(Uri uri) {
|
||||
// Until ExoPlayer provides automatic detection of media source types, we
|
||||
// rely on the file extension. When this is absent, as a temporary
|
||||
// workaround we allow the app to supply a fake extension in the URL
|
||||
// fragment. e.g. https://somewhere.com/somestream?x=etc#.m3u8
|
||||
String fragment = uri.getFragment();
|
||||
String filename = fragment != null && fragment.contains(".") ? fragment : uri.getPath();
|
||||
return filename.replaceAll("^.*\\.", "").toLowerCase();
|
||||
}
|
||||
|
||||
public void play(Result result) {
|
||||
if (player.getPlayWhenReady()) return;
|
||||
if (playResult != null) {
|
||||
playResult.success(null);
|
||||
}
|
||||
playResult = result;
|
||||
startWatchingBuffer();
|
||||
player.setPlayWhenReady(true);
|
||||
if (processingState == ProcessingState.completed && playResult != null) {
|
||||
playResult.success(null);
|
||||
playResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
if (!player.getPlayWhenReady()) return;
|
||||
player.setPlayWhenReady(false);
|
||||
if (playResult != null) {
|
||||
playResult.success(null);
|
||||
playResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setVolume(final float volume) {
|
||||
player.setVolume(volume);
|
||||
}
|
||||
|
||||
public void setSpeed(final float speed) {
|
||||
player.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
|
||||
public void setLoopMode(final int mode) {
|
||||
player.setRepeatMode(mode);
|
||||
}
|
||||
|
||||
public void setShuffleModeEnabled(final boolean enabled) {
|
||||
if (enabled) {
|
||||
setShuffleOrder(mediaSource, 0);
|
||||
}
|
||||
player.setShuffleModeEnabled(enabled);
|
||||
}
|
||||
|
||||
public void seek(final long position, final Result result, final Integer index) {
|
||||
if (processingState == ProcessingState.none || processingState == ProcessingState.loading) {
|
||||
return;
|
||||
}
|
||||
abortSeek();
|
||||
seekPos = position;
|
||||
seekResult = result;
|
||||
seekProcessed = false;
|
||||
int windowIndex = index != null ? index : player.getCurrentWindowIndex();
|
||||
player.seekTo(windowIndex, position);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
mediaSources.clear();
|
||||
mediaSource = null;
|
||||
loopingChildren.clear();
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
transition(ProcessingState.none);
|
||||
}
|
||||
if (eventSink != null) {
|
||||
eventSink.endOfStream();
|
||||
}
|
||||
onDispose.run();
|
||||
}
|
||||
|
||||
private void abortSeek() {
|
||||
if (seekResult != null) {
|
||||
seekResult.success(null);
|
||||
seekResult = null;
|
||||
seekPos = null;
|
||||
seekProcessed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void abortExistingConnection() {
|
||||
sendError("abort", "Connection aborted");
|
||||
}
|
||||
|
||||
public static Long getLong(Object o) {
|
||||
return (o == null || o instanceof Long) ? (Long)o : new Long(((Integer)o).intValue());
|
||||
}
|
||||
|
||||
enum ProcessingState {
|
||||
none,
|
||||
loading,
|
||||
buffering,
|
||||
ready,
|
||||
completed
|
||||
}
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
package com.ryanheise.just_audio;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class DeezerDataSource implements HttpDataSource {
|
||||
HttpURLConnection connection;
|
||||
InputStream inputStream;
|
||||
int counter = 0;
|
||||
byte[] key;
|
||||
DataSpec dataSpec;
|
||||
|
||||
//Quality fallback stuff
|
||||
String trackId;
|
||||
int quality = 0;
|
||||
String md5origin;
|
||||
String mediaVersion;
|
||||
|
||||
public DeezerDataSource(String trackId) {
|
||||
this.trackId = trackId;
|
||||
this.key = getKey(trackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws HttpDataSource.HttpDataSourceException {
|
||||
this.dataSpec = dataSpec;
|
||||
try {
|
||||
//Check if real url or placeholder for quality fallback
|
||||
URL url = new URL(dataSpec.uri.toString());
|
||||
String[] qp = url.getQuery().split("&");
|
||||
//Real deezcdn url doesnt have query params
|
||||
if (qp.length >= 3) {
|
||||
//Parse query parameters
|
||||
for (int i = 0; i < qp.length; i++) {
|
||||
String p = qp[i].replace("?", "");
|
||||
if (p.startsWith("md5")) {
|
||||
this.md5origin = p.replace("md5=", "");
|
||||
}
|
||||
if (p.startsWith("mv")) {
|
||||
this.mediaVersion = p.replace("mv=", "");
|
||||
}
|
||||
if (p.startsWith("q")) {
|
||||
if (this.quality == 0) {
|
||||
this.quality = Integer.parseInt(p.replace("q=", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
//Get real url
|
||||
url = new URL(this.getTrackUrl(trackId, md5origin, mediaVersion, quality));
|
||||
}
|
||||
|
||||
|
||||
this.connection = (HttpURLConnection) url.openConnection();
|
||||
this.connection.setChunkedStreamingMode(2048);
|
||||
if (dataSpec.position > 0) {
|
||||
this.counter = (int) (dataSpec.position/2048);
|
||||
this.connection.setRequestProperty("Range",
|
||||
"bytes=" + Long.toString(this.counter*2048) + "-");
|
||||
}
|
||||
|
||||
InputStream is = this.connection.getInputStream();
|
||||
this.inputStream = new BufferedInputStream(new FilterInputStream(is) {
|
||||
@Override
|
||||
public int read(byte buffer[], int offset, int len) throws IOException {
|
||||
byte[] b = new byte[2048];
|
||||
int t = 0;
|
||||
int read = 0;
|
||||
while (read != -1 && t != 2048) {
|
||||
t += read = in.read(b, t, 2048-t);
|
||||
}
|
||||
|
||||
if (counter % 3 == 0) {
|
||||
byte[] dec = decryptChunk(key, b);
|
||||
System.arraycopy(dec, 0, buffer, offset, 2048);
|
||||
} else {
|
||||
System.arraycopy(b, 0, buffer, offset, 2048);
|
||||
}
|
||||
counter++;
|
||||
|
||||
return t;
|
||||
|
||||
}
|
||||
},2048);
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
//Quality fallback
|
||||
if (this.quality == 1) {
|
||||
Log.e("E", e.toString());
|
||||
throw new HttpDataSourceException("Error loading URL", dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
if (this.quality == 3) this.quality = 1;
|
||||
if (this.quality == 9) this.quality = 3;
|
||||
// r e c u r s i o n
|
||||
return this.open(dataSpec);
|
||||
}
|
||||
String sizeString = this.connection.getHeaderField("Content-Length");
|
||||
Long size = Long.parseLong(sizeString);
|
||||
|
||||
//Get quality string
|
||||
String qualityString = "MP3 ";
|
||||
if (this.quality == 9) qualityString = "FLAC ";
|
||||
qualityString += Math.round(size / 8000) + "kbps";
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException {
|
||||
int read = 0;
|
||||
try {
|
||||
read = this.inputStream.read(buffer, offset, length);
|
||||
} catch (Exception e) {
|
||||
Log.e("E", e.toString());
|
||||
//throw new HttpDataSourceException("Error reading from stream", this.dataSpec, HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
if (this.inputStream != null) this.inputStream.close();
|
||||
if (this.connection != null) this.connection.disconnect();
|
||||
} catch (Exception e) {
|
||||
Log.e("E", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestProperty(String name, String value) {
|
||||
Log.d("D", "setRequestProperty");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRequestProperty(String name) {
|
||||
Log.d("D", "clearRequestProperty");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllRequestProperties() {
|
||||
Log.d("D", "clearAllRequestProperties");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() {
|
||||
Log.d("D", "getResponseCode");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getResponseHeaders() {
|
||||
return this.connection.getHeaderFields();
|
||||
}
|
||||
|
||||
public final void addTransferListener(TransferListener transferListener) {
|
||||
Log.d("D", "addTransferListener");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
return Uri.parse(this.connection.getURL().toString());
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
byte[] getKey(String id) {
|
||||
String secret = "g4el58wc0zvf9na1";
|
||||
try {
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
md5.update(id.getBytes());
|
||||
byte[] md5id = md5.digest();
|
||||
String idmd5 = bytesToHex(md5id).toLowerCase();
|
||||
String key = "";
|
||||
for(int i=0; i<16; i++) {
|
||||
int s0 = idmd5.charAt(i);
|
||||
int s1 = idmd5.charAt(i+16);
|
||||
int s2 = secret.charAt(i);
|
||||
key += (char)(s0^s1^s2);
|
||||
}
|
||||
return key.getBytes();
|
||||
} catch (Exception e) {
|
||||
Log.e("E", e.toString());
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
byte[] decryptChunk(byte[] key, byte[] data) {
|
||||
try {
|
||||
byte[] IV = {00, 01, 02, 03, 04, 05, 06, 07};
|
||||
SecretKeySpec Skey = new SecretKeySpec(key, "Blowfish");
|
||||
Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, Skey, new javax.crypto.spec.IvParameterSpec(IV));
|
||||
return cipher.doFinal(data);
|
||||
}catch (Exception e) {
|
||||
Log.e("D", e.toString());
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
public String getTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
|
||||
try {
|
||||
int magic = 164;
|
||||
|
||||
ByteArrayOutputStream step1 = new ByteArrayOutputStream();
|
||||
step1.write(md5origin.getBytes());
|
||||
step1.write(magic);
|
||||
step1.write(Integer.toString(quality).getBytes());
|
||||
step1.write(magic);
|
||||
step1.write(trackId.getBytes());
|
||||
step1.write(magic);
|
||||
step1.write(mediaVersion.getBytes());
|
||||
//Get MD5
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
md5.update(step1.toByteArray());
|
||||
byte[] digest = md5.digest();
|
||||
String md5hex = bytesToHex(digest).toLowerCase();
|
||||
|
||||
ByteArrayOutputStream step2 = new ByteArrayOutputStream();
|
||||
step2.write(md5hex.getBytes());
|
||||
step2.write(magic);
|
||||
step2.write(step1.toByteArray());
|
||||
step2.write(magic);
|
||||
|
||||
//Pad step2 with dots, to get correct length
|
||||
while(step2.size()%16 > 0) step2.write(46);
|
||||
|
||||
//Prepare AES encryption
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
SecretKeySpec key = new SecretKeySpec("jo6aey6haid2Teih".getBytes(), "AES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
//Encrypt
|
||||
StringBuilder step3 = new StringBuilder();
|
||||
for (int i=0; i<step2.size()/16; i++) {
|
||||
byte[] b = Arrays.copyOfRange(step2.toByteArray(), i*16, (i+1)*16);
|
||||
step3.append(bytesToHex(cipher.doFinal(b)).toLowerCase());
|
||||
}
|
||||
//Join to URL
|
||||
return "https://e-cdns-proxy-" + md5origin.charAt(0) + ".dzcdn.net/mobile/1/" + step3.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.ryanheise.just_audio;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.PluginRegistry.Registrar;
|
||||
|
||||
/**
|
||||
* JustAudioPlugin
|
||||
*/
|
||||
public class JustAudioPlugin implements FlutterPlugin {
|
||||
|
||||
private MethodChannel channel;
|
||||
private MainMethodCallHandler methodCallHandler;
|
||||
|
||||
public JustAudioPlugin() {
|
||||
}
|
||||
|
||||
/**
|
||||
* v1 plugin registration.
|
||||
*/
|
||||
public static void registerWith(Registrar registrar) {
|
||||
final JustAudioPlugin plugin = new JustAudioPlugin();
|
||||
plugin.startListening(registrar.context(), registrar.messenger());
|
||||
registrar.addViewDestroyListener(
|
||||
view -> {
|
||||
plugin.stopListening();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
|
||||
startListening(binding.getApplicationContext(), binding.getBinaryMessenger());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
stopListening();
|
||||
}
|
||||
|
||||
private void startListening(Context applicationContext, BinaryMessenger messenger) {
|
||||
methodCallHandler = new MainMethodCallHandler(applicationContext, messenger);
|
||||
|
||||
channel = new MethodChannel(messenger, "com.ryanheise.just_audio.methods");
|
||||
channel.setMethodCallHandler(methodCallHandler);
|
||||
}
|
||||
|
||||
private void stopListening() {
|
||||
methodCallHandler.dispose();
|
||||
methodCallHandler = null;
|
||||
|
||||
channel.setMethodCallHandler(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.ryanheise.just_audio;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
public class MainMethodCallHandler implements MethodCallHandler {
|
||||
|
||||
private final Context applicationContext;
|
||||
private final BinaryMessenger messenger;
|
||||
|
||||
private final Map<String, AudioPlayer> players = new HashMap<>();
|
||||
|
||||
public MainMethodCallHandler(Context applicationContext,
|
||||
BinaryMessenger messenger) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, @NonNull Result result) {
|
||||
switch (call.method) {
|
||||
case "init":
|
||||
final List<String> ids = call.arguments();
|
||||
String id = ids.get(0);
|
||||
players.put(id, new AudioPlayer(applicationContext, messenger, id,
|
||||
() -> players.remove(id)
|
||||
));
|
||||
result.success(null);
|
||||
break;
|
||||
case "setIosCategory":
|
||||
result.success(null);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (AudioPlayer player : new ArrayList<AudioPlayer>(players.values())) {
|
||||
player.dispose();
|
||||
}
|
||||
}
|
||||
}
|
1138
darwin/Classes/AudioPlayer.m
Normal file
1138
darwin/Classes/AudioPlayer.m
Normal file
File diff suppressed because it is too large
Load Diff
37
darwin/Classes/AudioSource.m
Normal file
37
darwin/Classes/AudioSource.m
Normal file
@ -0,0 +1,37 @@
|
||||
#import "AudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation AudioSource {
|
||||
NSString *_sourceId;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_sourceId = sid;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)sourceId {
|
||||
return _sourceId;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
if ([_sourceId isEqualToString:sourceId]) {
|
||||
[matches addObject:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
79
darwin/Classes/ClippingAudioSource.m
Normal file
79
darwin/Classes/ClippingAudioSource.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "AudioSource.h"
|
||||
#import "ClippingAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import "UriAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation ClippingAudioSource {
|
||||
UriAudioSource *_audioSource;
|
||||
CMTime _start;
|
||||
CMTime _end;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSource = audioSource;
|
||||
_start = start == [NSNull null] ? kCMTimeZero : CMTimeMake([start intValue], 1000);
|
||||
_end = end == [NSNull null] ? kCMTimeInvalid : CMTimeMake([end intValue], 1000);
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UriAudioSource *)audioSource {
|
||||
return _audioSource;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
[_audioSource findById:sourceId matches:matches];
|
||||
}
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
[super attach:player];
|
||||
_audioSource.playerItem.forwardPlaybackEndTime = _end;
|
||||
// XXX: Not needed since currentItem observer handles it?
|
||||
[self seek:kCMTimeZero];
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _audioSource.playerItem;
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
if (!completionHandler || (self.playerItem.status == AVPlayerItemStatusReadyToPlay)) {
|
||||
CMTime absPosition = CMTimeAdd(_start, position);
|
||||
[_audioSource.playerItem seekToTime:absPosition toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return CMTimeSubtract(CMTIME_IS_INVALID(_end) ? self.playerItem.duration : _end, _start);
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return CMTimeSubtract(self.playerItem.currentTime, _start);
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
CMTime pos = CMTimeSubtract(_audioSource.bufferedPosition, _start);
|
||||
CMTime dur = [self duration];
|
||||
return CMTimeCompare(pos, dur) >= 0 ? dur : pos;
|
||||
}
|
||||
|
||||
@end
|
109
darwin/Classes/ConcatenatingAudioSource.m
Normal file
109
darwin/Classes/ConcatenatingAudioSource.m
Normal file
@ -0,0 +1,109 @@
|
||||
#import "AudioSource.h"
|
||||
#import "ConcatenatingAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <stdlib.h>
|
||||
|
||||
@implementation ConcatenatingAudioSource {
|
||||
NSMutableArray<AudioSource *> *_audioSources;
|
||||
NSMutableArray<NSNumber *> *_shuffleOrder;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray<AudioSource *> *)audioSources {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSources = audioSources;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)count {
|
||||
return _audioSources.count;
|
||||
}
|
||||
|
||||
- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index {
|
||||
[_audioSources insertObject:audioSource atIndex:index];
|
||||
}
|
||||
|
||||
- (void)removeSourcesFromIndex:(int)start toIndex:(int)end {
|
||||
if (end == -1) end = _audioSources.count;
|
||||
for (int i = start; i < end; i++) {
|
||||
[_audioSources removeObjectAtIndex:start];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex {
|
||||
AudioSource *source = _audioSources[currentIndex];
|
||||
[_audioSources removeObjectAtIndex:currentIndex];
|
||||
[_audioSources insertObject:source atIndex:newIndex];
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_audioSources[i] findById:sourceId matches:matches];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
NSMutableArray *order = [NSMutableArray new];
|
||||
int offset = [order count];
|
||||
NSMutableArray *childOrders = [NSMutableArray new]; // array of array of ints
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
AudioSource *audioSource = _audioSources[i];
|
||||
NSArray *childShuffleOrder = [audioSource getShuffleOrder];
|
||||
NSMutableArray *offsetChildShuffleOrder = [NSMutableArray new];
|
||||
for (int j = 0; j < [childShuffleOrder count]; j++) {
|
||||
[offsetChildShuffleOrder addObject:@([childShuffleOrder[j] integerValue] + offset)];
|
||||
}
|
||||
[childOrders addObject:offsetChildShuffleOrder];
|
||||
offset += [childShuffleOrder count];
|
||||
}
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[order addObjectsFromArray:childOrders[[_shuffleOrder[i] integerValue]]];
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
int currentChildIndex = -1;
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
int indexBefore = treeIndex;
|
||||
AudioSource *child = _audioSources[i];
|
||||
treeIndex = [child shuffle:treeIndex currentIndex:currentIndex];
|
||||
if (currentIndex >= indexBefore && currentIndex < treeIndex) {
|
||||
currentChildIndex = i;
|
||||
} else {}
|
||||
}
|
||||
// Shuffle so that the current child is first in the shuffle order
|
||||
_shuffleOrder = [NSMutableArray arrayWithCapacity:[_audioSources count]];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_shuffleOrder addObject:@(0)];
|
||||
}
|
||||
NSLog(@"shuffle: audioSources.count=%d and shuffleOrder.count=%d", [_audioSources count], [_shuffleOrder count]);
|
||||
// First generate a random shuffle
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
int j = arc4random_uniform(i + 1);
|
||||
_shuffleOrder[i] = _shuffleOrder[j];
|
||||
_shuffleOrder[j] = @(i);
|
||||
}
|
||||
// Then bring currentIndex to the front
|
||||
if (currentChildIndex != -1) {
|
||||
for (int i = 1; i < [_audioSources count]; i++) {
|
||||
if ([_shuffleOrder[i] integerValue] == currentChildIndex) {
|
||||
NSNumber *v = _shuffleOrder[0];
|
||||
_shuffleOrder[0] = _shuffleOrder[i];
|
||||
_shuffleOrder[i] = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
@end
|
68
darwin/Classes/IndexedAudioSource.m
Normal file
68
darwin/Classes/IndexedAudioSource.m
Normal file
@ -0,0 +1,68 @@
|
||||
#import "IndexedAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation IndexedAudioSource {
|
||||
BOOL _isAttached;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_isAttached = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isAttached {
|
||||
return _isAttached;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
[sequence addObject:self];
|
||||
return treeIndex + 1;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
return treeIndex + 1;
|
||||
}
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
_isAttached = YES;
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position {
|
||||
[self seek:position completionHandler:nil];
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
@end
|
16
darwin/Classes/IndexedPlayerItem.m
Normal file
16
darwin/Classes/IndexedPlayerItem.m
Normal file
@ -0,0 +1,16 @@
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import "IndexedAudioSource.h"
|
||||
|
||||
@implementation IndexedPlayerItem {
|
||||
IndexedAudioSource *_audioSource;
|
||||
}
|
||||
|
||||
-(void)setAudioSource:(IndexedAudioSource *)audioSource {
|
||||
_audioSource = audioSource;
|
||||
}
|
||||
|
||||
-(IndexedAudioSource *)audioSource {
|
||||
return _audioSource;
|
||||
}
|
||||
|
||||
@end
|
55
darwin/Classes/JustAudioPlugin.m
Normal file
55
darwin/Classes/JustAudioPlugin.m
Normal file
@ -0,0 +1,55 @@
|
||||
#import "JustAudioPlugin.h"
|
||||
#import "AudioPlayer.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
@implementation JustAudioPlugin {
|
||||
NSObject<FlutterPluginRegistrar>* _registrar;
|
||||
BOOL _configuredSession;
|
||||
}
|
||||
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
FlutterMethodChannel* channel = [FlutterMethodChannel
|
||||
methodChannelWithName:@"com.ryanheise.just_audio.methods"
|
||||
binaryMessenger:[registrar messenger]];
|
||||
JustAudioPlugin* instance = [[JustAudioPlugin alloc] initWithRegistrar:registrar];
|
||||
[registrar addMethodCallDelegate:instance channel:channel];
|
||||
}
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_registrar = registrar;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||
if ([@"init" isEqualToString:call.method]) {
|
||||
NSArray* args = (NSArray*)call.arguments;
|
||||
NSString* playerId = args[0];
|
||||
/*AudioPlayer* player =*/ [[AudioPlayer alloc] initWithRegistrar:_registrar playerId:playerId configuredSession:_configuredSession];
|
||||
result(nil);
|
||||
} else if ([@"setIosCategory" isEqualToString:call.method]) {
|
||||
#if TARGET_OS_IPHONE
|
||||
NSNumber* categoryIndex = (NSNumber*)call.arguments;
|
||||
AVAudioSessionCategory category = nil;
|
||||
switch (categoryIndex.integerValue) {
|
||||
case 0: category = AVAudioSessionCategoryAmbient; break;
|
||||
case 1: category = AVAudioSessionCategorySoloAmbient; break;
|
||||
case 2: category = AVAudioSessionCategoryPlayback; break;
|
||||
case 3: category = AVAudioSessionCategoryRecord; break;
|
||||
case 4: category = AVAudioSessionCategoryPlayAndRecord; break;
|
||||
case 5: category = AVAudioSessionCategoryMultiRoute; break;
|
||||
}
|
||||
if (category) {
|
||||
_configuredSession = YES;
|
||||
}
|
||||
[[AVAudioSession sharedInstance] setCategory:category error:nil];
|
||||
#endif
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
53
darwin/Classes/LoopingAudioSource.m
Normal file
53
darwin/Classes/LoopingAudioSource.m
Normal file
@ -0,0 +1,53 @@
|
||||
#import "AudioSource.h"
|
||||
#import "LoopingAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation LoopingAudioSource {
|
||||
// An array of duplicates
|
||||
NSArray<AudioSource *> *_audioSources; // <AudioSource *>
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray<AudioSource *> *)audioSources {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSources = audioSources;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_audioSources[i] findById:sourceId matches:matches];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
NSMutableArray *order = [NSMutableArray new];
|
||||
int offset = (int)[order count];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
AudioSource *audioSource = _audioSources[i];
|
||||
NSArray *childShuffleOrder = [audioSource getShuffleOrder];
|
||||
for (int j = 0; j < [childShuffleOrder count]; j++) {
|
||||
[order addObject:@([childShuffleOrder[j] integerValue] + offset)];
|
||||
}
|
||||
offset += [childShuffleOrder count];
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
// TODO: This should probably shuffle the same way on all duplicates.
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] shuffle:treeIndex currentIndex:currentIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
@end
|
79
darwin/Classes/UriAudioSource.m
Normal file
79
darwin/Classes/UriAudioSource.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "UriAudioSource.h"
|
||||
#import "IndexedAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation UriAudioSource {
|
||||
NSString *_uri;
|
||||
IndexedPlayerItem *_playerItem;
|
||||
/* CMTime _duration; */
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_uri = uri;
|
||||
if ([_uri hasPrefix:@"file://"]) {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[_uri substringFromIndex:7]]];
|
||||
} else {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL URLWithString:_uri]];
|
||||
}
|
||||
if (@available(macOS 10.13, iOS 11.0, *)) {
|
||||
// This does the best at reducing distortion on voice with speeds below 1.0
|
||||
_playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain;
|
||||
}
|
||||
/* NSKeyValueObservingOptions options = */
|
||||
/* NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; */
|
||||
/* [_playerItem addObserver:self */
|
||||
/* forKeyPath:@"duration" */
|
||||
/* options:options */
|
||||
/* context:nil]; */
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _playerItem;
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
if (!completionHandler || (_playerItem.status == AVPlayerItemStatusReadyToPlay)) {
|
||||
[_playerItem seekToTime:position toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return _playerItem.duration;
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return _playerItem.currentTime;
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
NSValue *last = _playerItem.loadedTimeRanges.lastObject;
|
||||
if (last) {
|
||||
CMTimeRange timeRange = [last CMTimeRangeValue];
|
||||
return CMTimeAdd(timeRange.start, timeRange.duration);
|
||||
} else {
|
||||
return _playerItem.currentTime;
|
||||
}
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
@end
|
37
ios/.gitignore
vendored
Normal file
37
ios/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
.idea/
|
||||
.vagrant/
|
||||
.sconsign.dblite
|
||||
.svn/
|
||||
|
||||
.DS_Store
|
||||
*.swp
|
||||
profile
|
||||
|
||||
DerivedData/
|
||||
build/
|
||||
GeneratedPluginRegistrant.h
|
||||
GeneratedPluginRegistrant.m
|
||||
|
||||
.generated/
|
||||
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
|
||||
!default.pbxuser
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.perspectivev3
|
||||
|
||||
xcuserdata
|
||||
|
||||
*.moved-aside
|
||||
|
||||
*.pyc
|
||||
*sync/
|
||||
Icon?
|
||||
.tags*
|
||||
|
||||
/Flutter/Generated.xcconfig
|
||||
/Flutter/flutter_export_environment.sh
|
0
ios/Assets/.gitkeep
Normal file
0
ios/Assets/.gitkeep
Normal file
21
ios/Classes/AudioPlayer.h
Normal file
21
ios/Classes/AudioPlayer.h
Normal file
@ -0,0 +1,21 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface AudioPlayer : NSObject<FlutterStreamHandler>
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar playerId:(NSString*)idParam configuredSession:(BOOL)configuredSession;
|
||||
|
||||
@end
|
||||
|
||||
enum ProcessingState {
|
||||
none,
|
||||
loading,
|
||||
buffering,
|
||||
ready,
|
||||
completed
|
||||
};
|
||||
|
||||
enum LoopMode {
|
||||
loopOff,
|
||||
loopOne,
|
||||
loopAll
|
||||
};
|
1138
ios/Classes/AudioPlayer.m
Normal file
1138
ios/Classes/AudioPlayer.m
Normal file
File diff suppressed because it is too large
Load Diff
13
ios/Classes/AudioSource.h
Normal file
13
ios/Classes/AudioSource.h
Normal file
@ -0,0 +1,13 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface AudioSource : NSObject
|
||||
|
||||
@property (readonly, nonatomic) NSString* sourceId;
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid;
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex;
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches;
|
||||
- (NSArray *)getShuffleOrder;
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex;
|
||||
|
||||
@end
|
37
ios/Classes/AudioSource.m
Normal file
37
ios/Classes/AudioSource.m
Normal file
@ -0,0 +1,37 @@
|
||||
#import "AudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation AudioSource {
|
||||
NSString *_sourceId;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_sourceId = sid;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)sourceId {
|
||||
return _sourceId;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
if ([_sourceId isEqualToString:sourceId]) {
|
||||
[matches addObject:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
11
ios/Classes/ClippingAudioSource.h
Normal file
11
ios/Classes/ClippingAudioSource.h
Normal file
@ -0,0 +1,11 @@
|
||||
#import "AudioSource.h"
|
||||
#import "UriAudioSource.h"
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface ClippingAudioSource : IndexedAudioSource
|
||||
|
||||
@property (readonly, nonatomic) UriAudioSource* audioSource;
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end;
|
||||
|
||||
@end
|
79
ios/Classes/ClippingAudioSource.m
Normal file
79
ios/Classes/ClippingAudioSource.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "AudioSource.h"
|
||||
#import "ClippingAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import "UriAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation ClippingAudioSource {
|
||||
UriAudioSource *_audioSource;
|
||||
CMTime _start;
|
||||
CMTime _end;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSource = audioSource;
|
||||
_start = start == [NSNull null] ? kCMTimeZero : CMTimeMake([start intValue], 1000);
|
||||
_end = end == [NSNull null] ? kCMTimeInvalid : CMTimeMake([end intValue], 1000);
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UriAudioSource *)audioSource {
|
||||
return _audioSource;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
[_audioSource findById:sourceId matches:matches];
|
||||
}
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
[super attach:player];
|
||||
_audioSource.playerItem.forwardPlaybackEndTime = _end;
|
||||
// XXX: Not needed since currentItem observer handles it?
|
||||
[self seek:kCMTimeZero];
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _audioSource.playerItem;
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
if (!completionHandler || (self.playerItem.status == AVPlayerItemStatusReadyToPlay)) {
|
||||
CMTime absPosition = CMTimeAdd(_start, position);
|
||||
[_audioSource.playerItem seekToTime:absPosition toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return CMTimeSubtract(CMTIME_IS_INVALID(_end) ? self.playerItem.duration : _end, _start);
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return CMTimeSubtract(self.playerItem.currentTime, _start);
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
CMTime pos = CMTimeSubtract(_audioSource.bufferedPosition, _start);
|
||||
CMTime dur = [self duration];
|
||||
return CMTimeCompare(pos, dur) >= 0 ? dur : pos;
|
||||
}
|
||||
|
||||
@end
|
13
ios/Classes/ConcatenatingAudioSource.h
Normal file
13
ios/Classes/ConcatenatingAudioSource.h
Normal file
@ -0,0 +1,13 @@
|
||||
#import "AudioSource.h"
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface ConcatenatingAudioSource : AudioSource
|
||||
|
||||
@property (readonly, nonatomic) int count;
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray<AudioSource *> *)audioSources;
|
||||
- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index;
|
||||
- (void)removeSourcesFromIndex:(int)start toIndex:(int)end;
|
||||
- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex;
|
||||
|
||||
@end
|
109
ios/Classes/ConcatenatingAudioSource.m
Normal file
109
ios/Classes/ConcatenatingAudioSource.m
Normal file
@ -0,0 +1,109 @@
|
||||
#import "AudioSource.h"
|
||||
#import "ConcatenatingAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <stdlib.h>
|
||||
|
||||
@implementation ConcatenatingAudioSource {
|
||||
NSMutableArray<AudioSource *> *_audioSources;
|
||||
NSMutableArray<NSNumber *> *_shuffleOrder;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray<AudioSource *> *)audioSources {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSources = audioSources;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)count {
|
||||
return _audioSources.count;
|
||||
}
|
||||
|
||||
- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index {
|
||||
[_audioSources insertObject:audioSource atIndex:index];
|
||||
}
|
||||
|
||||
- (void)removeSourcesFromIndex:(int)start toIndex:(int)end {
|
||||
if (end == -1) end = _audioSources.count;
|
||||
for (int i = start; i < end; i++) {
|
||||
[_audioSources removeObjectAtIndex:start];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex {
|
||||
AudioSource *source = _audioSources[currentIndex];
|
||||
[_audioSources removeObjectAtIndex:currentIndex];
|
||||
[_audioSources insertObject:source atIndex:newIndex];
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_audioSources[i] findById:sourceId matches:matches];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
NSMutableArray *order = [NSMutableArray new];
|
||||
int offset = [order count];
|
||||
NSMutableArray *childOrders = [NSMutableArray new]; // array of array of ints
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
AudioSource *audioSource = _audioSources[i];
|
||||
NSArray *childShuffleOrder = [audioSource getShuffleOrder];
|
||||
NSMutableArray *offsetChildShuffleOrder = [NSMutableArray new];
|
||||
for (int j = 0; j < [childShuffleOrder count]; j++) {
|
||||
[offsetChildShuffleOrder addObject:@([childShuffleOrder[j] integerValue] + offset)];
|
||||
}
|
||||
[childOrders addObject:offsetChildShuffleOrder];
|
||||
offset += [childShuffleOrder count];
|
||||
}
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[order addObjectsFromArray:childOrders[[_shuffleOrder[i] integerValue]]];
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
int currentChildIndex = -1;
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
int indexBefore = treeIndex;
|
||||
AudioSource *child = _audioSources[i];
|
||||
treeIndex = [child shuffle:treeIndex currentIndex:currentIndex];
|
||||
if (currentIndex >= indexBefore && currentIndex < treeIndex) {
|
||||
currentChildIndex = i;
|
||||
} else {}
|
||||
}
|
||||
// Shuffle so that the current child is first in the shuffle order
|
||||
_shuffleOrder = [NSMutableArray arrayWithCapacity:[_audioSources count]];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_shuffleOrder addObject:@(0)];
|
||||
}
|
||||
NSLog(@"shuffle: audioSources.count=%d and shuffleOrder.count=%d", [_audioSources count], [_shuffleOrder count]);
|
||||
// First generate a random shuffle
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
int j = arc4random_uniform(i + 1);
|
||||
_shuffleOrder[i] = _shuffleOrder[j];
|
||||
_shuffleOrder[j] = @(i);
|
||||
}
|
||||
// Then bring currentIndex to the front
|
||||
if (currentChildIndex != -1) {
|
||||
for (int i = 1; i < [_audioSources count]; i++) {
|
||||
if ([_shuffleOrder[i] integerValue] == currentChildIndex) {
|
||||
NSNumber *v = _shuffleOrder[0];
|
||||
_shuffleOrder[0] = _shuffleOrder[i];
|
||||
_shuffleOrder[i] = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
@end
|
21
ios/Classes/IndexedAudioSource.h
Normal file
21
ios/Classes/IndexedAudioSource.h
Normal file
@ -0,0 +1,21 @@
|
||||
#import "AudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <Flutter/Flutter.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface IndexedAudioSource : AudioSource
|
||||
|
||||
@property (readonly, nonatomic) IndexedPlayerItem *playerItem;
|
||||
@property (readwrite, nonatomic) CMTime duration;
|
||||
@property (readonly, nonatomic) CMTime position;
|
||||
@property (readonly, nonatomic) CMTime bufferedPosition;
|
||||
@property (readonly, nonatomic) BOOL isAttached;
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player;
|
||||
- (void)play:(AVQueuePlayer *)player;
|
||||
- (void)pause:(AVQueuePlayer *)player;
|
||||
- (void)stop:(AVQueuePlayer *)player;
|
||||
- (void)seek:(CMTime)position;
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler;
|
||||
|
||||
@end
|
68
ios/Classes/IndexedAudioSource.m
Normal file
68
ios/Classes/IndexedAudioSource.m
Normal file
@ -0,0 +1,68 @@
|
||||
#import "IndexedAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation IndexedAudioSource {
|
||||
BOOL _isAttached;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_isAttached = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isAttached {
|
||||
return _isAttached;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
[sequence addObject:self];
|
||||
return treeIndex + 1;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
return treeIndex + 1;
|
||||
}
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
_isAttached = YES;
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position {
|
||||
[self seek:position completionHandler:nil];
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
@end
|
9
ios/Classes/IndexedPlayerItem.h
Normal file
9
ios/Classes/IndexedPlayerItem.h
Normal file
@ -0,0 +1,9 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@class IndexedAudioSource;
|
||||
|
||||
@interface IndexedPlayerItem : AVPlayerItem
|
||||
|
||||
@property (readwrite, nonatomic) IndexedAudioSource *audioSource;
|
||||
|
||||
@end
|
16
ios/Classes/IndexedPlayerItem.m
Normal file
16
ios/Classes/IndexedPlayerItem.m
Normal file
@ -0,0 +1,16 @@
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import "IndexedAudioSource.h"
|
||||
|
||||
@implementation IndexedPlayerItem {
|
||||
IndexedAudioSource *_audioSource;
|
||||
}
|
||||
|
||||
-(void)setAudioSource:(IndexedAudioSource *)audioSource {
|
||||
_audioSource = audioSource;
|
||||
}
|
||||
|
||||
-(IndexedAudioSource *)audioSource {
|
||||
return _audioSource;
|
||||
}
|
||||
|
||||
@end
|
4
ios/Classes/JustAudioPlugin.h
Normal file
4
ios/Classes/JustAudioPlugin.h
Normal file
@ -0,0 +1,4 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface JustAudioPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
55
ios/Classes/JustAudioPlugin.m
Normal file
55
ios/Classes/JustAudioPlugin.m
Normal file
@ -0,0 +1,55 @@
|
||||
#import "JustAudioPlugin.h"
|
||||
#import "AudioPlayer.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
@implementation JustAudioPlugin {
|
||||
NSObject<FlutterPluginRegistrar>* _registrar;
|
||||
BOOL _configuredSession;
|
||||
}
|
||||
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
FlutterMethodChannel* channel = [FlutterMethodChannel
|
||||
methodChannelWithName:@"com.ryanheise.just_audio.methods"
|
||||
binaryMessenger:[registrar messenger]];
|
||||
JustAudioPlugin* instance = [[JustAudioPlugin alloc] initWithRegistrar:registrar];
|
||||
[registrar addMethodCallDelegate:instance channel:channel];
|
||||
}
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_registrar = registrar;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||
if ([@"init" isEqualToString:call.method]) {
|
||||
NSArray* args = (NSArray*)call.arguments;
|
||||
NSString* playerId = args[0];
|
||||
/*AudioPlayer* player =*/ [[AudioPlayer alloc] initWithRegistrar:_registrar playerId:playerId configuredSession:_configuredSession];
|
||||
result(nil);
|
||||
} else if ([@"setIosCategory" isEqualToString:call.method]) {
|
||||
#if TARGET_OS_IPHONE
|
||||
NSNumber* categoryIndex = (NSNumber*)call.arguments;
|
||||
AVAudioSessionCategory category = nil;
|
||||
switch (categoryIndex.integerValue) {
|
||||
case 0: category = AVAudioSessionCategoryAmbient; break;
|
||||
case 1: category = AVAudioSessionCategorySoloAmbient; break;
|
||||
case 2: category = AVAudioSessionCategoryPlayback; break;
|
||||
case 3: category = AVAudioSessionCategoryRecord; break;
|
||||
case 4: category = AVAudioSessionCategoryPlayAndRecord; break;
|
||||
case 5: category = AVAudioSessionCategoryMultiRoute; break;
|
||||
}
|
||||
if (category) {
|
||||
_configuredSession = YES;
|
||||
}
|
||||
[[AVAudioSession sharedInstance] setCategory:category error:nil];
|
||||
#endif
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
8
ios/Classes/LoopingAudioSource.h
Normal file
8
ios/Classes/LoopingAudioSource.h
Normal file
@ -0,0 +1,8 @@
|
||||
#import "AudioSource.h"
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface LoopingAudioSource : AudioSource
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray<AudioSource *> *)audioSources;
|
||||
|
||||
@end
|
53
ios/Classes/LoopingAudioSource.m
Normal file
53
ios/Classes/LoopingAudioSource.m
Normal file
@ -0,0 +1,53 @@
|
||||
#import "AudioSource.h"
|
||||
#import "LoopingAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation LoopingAudioSource {
|
||||
// An array of duplicates
|
||||
NSArray<AudioSource *> *_audioSources; // <AudioSource *>
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray<AudioSource *> *)audioSources {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSources = audioSources;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_audioSources[i] findById:sourceId matches:matches];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
NSMutableArray *order = [NSMutableArray new];
|
||||
int offset = (int)[order count];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
AudioSource *audioSource = _audioSources[i];
|
||||
NSArray *childShuffleOrder = [audioSource getShuffleOrder];
|
||||
for (int j = 0; j < [childShuffleOrder count]; j++) {
|
||||
[order addObject:@([childShuffleOrder[j] integerValue] + offset)];
|
||||
}
|
||||
offset += [childShuffleOrder count];
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
// TODO: This should probably shuffle the same way on all duplicates.
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] shuffle:treeIndex currentIndex:currentIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
@end
|
8
ios/Classes/UriAudioSource.h
Normal file
8
ios/Classes/UriAudioSource.h
Normal file
@ -0,0 +1,8 @@
|
||||
#import "IndexedAudioSource.h"
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface UriAudioSource : IndexedAudioSource
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri;
|
||||
|
||||
@end
|
79
ios/Classes/UriAudioSource.m
Normal file
79
ios/Classes/UriAudioSource.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "UriAudioSource.h"
|
||||
#import "IndexedAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation UriAudioSource {
|
||||
NSString *_uri;
|
||||
IndexedPlayerItem *_playerItem;
|
||||
/* CMTime _duration; */
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_uri = uri;
|
||||
if ([_uri hasPrefix:@"file://"]) {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[_uri substringFromIndex:7]]];
|
||||
} else {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL URLWithString:_uri]];
|
||||
}
|
||||
if (@available(macOS 10.13, iOS 11.0, *)) {
|
||||
// This does the best at reducing distortion on voice with speeds below 1.0
|
||||
_playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain;
|
||||
}
|
||||
/* NSKeyValueObservingOptions options = */
|
||||
/* NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; */
|
||||
/* [_playerItem addObserver:self */
|
||||
/* forKeyPath:@"duration" */
|
||||
/* options:options */
|
||||
/* context:nil]; */
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _playerItem;
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
if (!completionHandler || (_playerItem.status == AVPlayerItemStatusReadyToPlay)) {
|
||||
[_playerItem seekToTime:position toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return _playerItem.duration;
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return _playerItem.currentTime;
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
NSValue *last = _playerItem.loadedTimeRanges.lastObject;
|
||||
if (last) {
|
||||
CMTimeRange timeRange = [last CMTimeRangeValue];
|
||||
return CMTimeAdd(timeRange.start, timeRange.duration);
|
||||
} else {
|
||||
return _playerItem.currentTime;
|
||||
}
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
@end
|
21
ios/just_audio.podspec
Normal file
21
ios/just_audio.podspec
Normal file
@ -0,0 +1,21 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'just_audio'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'A new flutter plugin project.'
|
||||
s.description = <<-DESC
|
||||
A new flutter plugin project.
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '8.0'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
|
||||
end
|
||||
|
1300
lib/just_audio.dart
Normal file
1300
lib/just_audio.dart
Normal file
File diff suppressed because it is too large
Load Diff
957
lib/just_audio_web.dart
Normal file
957
lib/just_audio_web.dart
Normal file
@ -0,0 +1,957 @@
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
final Random _random = Random();
|
||||
|
||||
class JustAudioPlugin {
|
||||
static void registerWith(Registrar registrar) {
|
||||
final MethodChannel channel = MethodChannel(
|
||||
'com.ryanheise.just_audio.methods',
|
||||
const StandardMethodCodec(),
|
||||
registrar.messenger);
|
||||
final JustAudioPlugin instance = JustAudioPlugin(registrar);
|
||||
channel.setMethodCallHandler(instance.handleMethodCall);
|
||||
}
|
||||
|
||||
final Registrar registrar;
|
||||
|
||||
JustAudioPlugin(this.registrar);
|
||||
|
||||
Future<dynamic> handleMethodCall(MethodCall call) async {
|
||||
switch (call.method) {
|
||||
case 'init':
|
||||
final String id = call.arguments[0];
|
||||
new Html5AudioPlayer(id: id, registrar: registrar);
|
||||
return null;
|
||||
case 'setIosCategory':
|
||||
return null;
|
||||
default:
|
||||
throw PlatformException(code: 'Unimplemented');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class JustAudioPlayer {
|
||||
final String id;
|
||||
final Registrar registrar;
|
||||
final MethodChannel methodChannel;
|
||||
final PluginEventChannel eventChannel;
|
||||
final StreamController eventController = StreamController();
|
||||
ProcessingState _processingState = ProcessingState.none;
|
||||
bool _playing = false;
|
||||
int _index;
|
||||
|
||||
JustAudioPlayer({@required this.id, @required this.registrar})
|
||||
: methodChannel = MethodChannel('com.ryanheise.just_audio.methods.$id',
|
||||
const StandardMethodCodec(), registrar.messenger),
|
||||
eventChannel = PluginEventChannel('com.ryanheise.just_audio.events.$id',
|
||||
const StandardMethodCodec(), registrar.messenger) {
|
||||
methodChannel.setMethodCallHandler(_methodHandler);
|
||||
eventChannel.controller = eventController;
|
||||
}
|
||||
|
||||
Future<dynamic> _methodHandler(MethodCall call) async {
|
||||
try {
|
||||
final args = call.arguments;
|
||||
switch (call.method) {
|
||||
case 'load':
|
||||
return await load(args[0]);
|
||||
case 'play':
|
||||
return await play();
|
||||
case 'pause':
|
||||
return await pause();
|
||||
case 'setVolume':
|
||||
return await setVolume(args[0]);
|
||||
case 'setSpeed':
|
||||
return await setSpeed(args[0]);
|
||||
case 'setLoopMode':
|
||||
return await setLoopMode(args[0]);
|
||||
case 'setShuffleModeEnabled':
|
||||
return await setShuffleModeEnabled(args[0]);
|
||||
case 'setAutomaticallyWaitsToMinimizeStalling':
|
||||
return null;
|
||||
case 'seek':
|
||||
return await seek(args[0], args[1]);
|
||||
case 'dispose':
|
||||
return dispose();
|
||||
case 'concatenating.add':
|
||||
return await concatenatingAdd(args[0], args[1]);
|
||||
case "concatenating.insert":
|
||||
return await concatenatingInsert(args[0], args[1], args[2]);
|
||||
case "concatenating.addAll":
|
||||
return await concatenatingAddAll(args[0], args[1]);
|
||||
case "concatenating.insertAll":
|
||||
return await concatenatingInsertAll(args[0], args[1], args[2]);
|
||||
case "concatenating.removeAt":
|
||||
return await concatenatingRemoveAt(args[0], args[1]);
|
||||
case "concatenating.removeRange":
|
||||
return await concatenatingRemoveRange(args[0], args[1], args[2]);
|
||||
case "concatenating.move":
|
||||
return await concatenatingMove(args[0], args[1], args[2]);
|
||||
case "concatenating.clear":
|
||||
return await concatenatingClear(args[0]);
|
||||
default:
|
||||
throw PlatformException(code: 'Unimplemented');
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
print("$stacktrace");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> load(Map source);
|
||||
|
||||
Future<void> play();
|
||||
|
||||
Future<void> pause();
|
||||
|
||||
Future<void> setVolume(double volume);
|
||||
|
||||
Future<void> setSpeed(double speed);
|
||||
|
||||
Future<void> setLoopMode(int mode);
|
||||
|
||||
Future<void> setShuffleModeEnabled(bool enabled);
|
||||
|
||||
Future<void> seek(int position, int index);
|
||||
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
eventController.close();
|
||||
}
|
||||
|
||||
Duration getCurrentPosition();
|
||||
|
||||
Duration getBufferedPosition();
|
||||
|
||||
Duration getDuration();
|
||||
|
||||
concatenatingAdd(String playerId, Map source);
|
||||
|
||||
concatenatingInsert(String playerId, int index, Map source);
|
||||
|
||||
concatenatingAddAll(String playerId, List sources);
|
||||
|
||||
concatenatingInsertAll(String playerId, int index, List sources);
|
||||
|
||||
concatenatingRemoveAt(String playerId, int index);
|
||||
|
||||
concatenatingRemoveRange(String playerId, int start, int end);
|
||||
|
||||
concatenatingMove(String playerId, int currentIndex, int newIndex);
|
||||
|
||||
concatenatingClear(String playerId);
|
||||
|
||||
broadcastPlaybackEvent() {
|
||||
var updateTime = DateTime.now().millisecondsSinceEpoch;
|
||||
eventController.add({
|
||||
'processingState': _processingState.index,
|
||||
'updatePosition': getCurrentPosition()?.inMilliseconds,
|
||||
'updateTime': updateTime,
|
||||
'bufferedPosition': getBufferedPosition()?.inMilliseconds,
|
||||
// TODO: Icy Metadata
|
||||
'icyMetadata': null,
|
||||
'duration': getDuration()?.inMilliseconds,
|
||||
'currentIndex': _index,
|
||||
});
|
||||
}
|
||||
|
||||
transition(ProcessingState processingState) {
|
||||
_processingState = processingState;
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
}
|
||||
|
||||
class Html5AudioPlayer extends JustAudioPlayer {
|
||||
AudioElement _audioElement = AudioElement();
|
||||
Completer _durationCompleter;
|
||||
AudioSourcePlayer _audioSourcePlayer;
|
||||
LoopMode _loopMode = LoopMode.off;
|
||||
bool _shuffleModeEnabled = false;
|
||||
final Map<String, AudioSourcePlayer> _audioSourcePlayers = {};
|
||||
|
||||
Html5AudioPlayer({@required String id, @required Registrar registrar})
|
||||
: super(id: id, registrar: registrar) {
|
||||
_audioElement.addEventListener('durationchange', (event) {
|
||||
_durationCompleter?.complete();
|
||||
broadcastPlaybackEvent();
|
||||
});
|
||||
_audioElement.addEventListener('error', (event) {
|
||||
_durationCompleter?.completeError(_audioElement.error);
|
||||
});
|
||||
_audioElement.addEventListener('ended', (event) async {
|
||||
_currentAudioSourcePlayer.complete();
|
||||
});
|
||||
_audioElement.addEventListener('timeupdate', (event) {
|
||||
_currentAudioSourcePlayer.timeUpdated(_audioElement.currentTime);
|
||||
});
|
||||
_audioElement.addEventListener('loadstart', (event) {
|
||||
transition(ProcessingState.buffering);
|
||||
});
|
||||
_audioElement.addEventListener('waiting', (event) {
|
||||
transition(ProcessingState.buffering);
|
||||
});
|
||||
_audioElement.addEventListener('stalled', (event) {
|
||||
transition(ProcessingState.buffering);
|
||||
});
|
||||
_audioElement.addEventListener('canplaythrough', (event) {
|
||||
transition(ProcessingState.ready);
|
||||
});
|
||||
_audioElement.addEventListener('progress', (event) {
|
||||
broadcastPlaybackEvent();
|
||||
});
|
||||
}
|
||||
|
||||
List<int> get order {
|
||||
final sequence = _audioSourcePlayer.sequence;
|
||||
List<int> order = List<int>(sequence.length);
|
||||
if (_shuffleModeEnabled) {
|
||||
order = _audioSourcePlayer.shuffleOrder;
|
||||
} else {
|
||||
for (var i = 0; i < order.length; i++) {
|
||||
order[i] = i;
|
||||
}
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
List<int> getInv(List<int> order) {
|
||||
List<int> orderInv = List<int>(order.length);
|
||||
for (var i = 0; i < order.length; i++) {
|
||||
orderInv[order[i]] = i;
|
||||
}
|
||||
return orderInv;
|
||||
}
|
||||
|
||||
onEnded() async {
|
||||
if (_loopMode == LoopMode.one) {
|
||||
await seek(0, null);
|
||||
play();
|
||||
} else {
|
||||
final order = this.order;
|
||||
final orderInv = getInv(order);
|
||||
if (orderInv[_index] + 1 < order.length) {
|
||||
// move to next item
|
||||
_index = order[orderInv[_index] + 1];
|
||||
await _currentAudioSourcePlayer.load();
|
||||
// Should always be true...
|
||||
if (_playing) {
|
||||
play();
|
||||
}
|
||||
} else {
|
||||
// reached end of playlist
|
||||
if (_loopMode == LoopMode.all) {
|
||||
// Loop back to the beginning
|
||||
if (order.length == 1) {
|
||||
await seek(0, null);
|
||||
play();
|
||||
} else {
|
||||
_index = order[0];
|
||||
await _currentAudioSourcePlayer.load();
|
||||
// Should always be true...
|
||||
if (_playing) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
transition(ProcessingState.completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Improve efficiency.
|
||||
IndexedAudioSourcePlayer get _currentAudioSourcePlayer =>
|
||||
_audioSourcePlayer != null && _index < _audioSourcePlayer.sequence.length
|
||||
? _audioSourcePlayer.sequence[_index]
|
||||
: null;
|
||||
|
||||
@override
|
||||
Future<int> load(Map source) async {
|
||||
_currentAudioSourcePlayer?.pause();
|
||||
_audioSourcePlayer = getAudioSource(source);
|
||||
_index = 0;
|
||||
if (_shuffleModeEnabled) {
|
||||
_audioSourcePlayer?.shuffle(0, _index);
|
||||
}
|
||||
return (await _currentAudioSourcePlayer.load()).inMilliseconds;
|
||||
}
|
||||
|
||||
Future<Duration> loadUri(final Uri uri) async {
|
||||
transition(ProcessingState.loading);
|
||||
final src = uri.toString();
|
||||
if (src != _audioElement.src) {
|
||||
_durationCompleter = Completer<num>();
|
||||
_audioElement.src = src;
|
||||
_audioElement.preload = 'auto';
|
||||
_audioElement.load();
|
||||
try {
|
||||
await _durationCompleter.future;
|
||||
} on MediaError catch (e) {
|
||||
throw PlatformException(
|
||||
code: "${e.code}", message: "Failed to load URL");
|
||||
} finally {
|
||||
_durationCompleter = null;
|
||||
}
|
||||
}
|
||||
transition(ProcessingState.ready);
|
||||
final seconds = _audioElement.duration;
|
||||
return seconds.isFinite
|
||||
? Duration(milliseconds: (seconds * 1000).toInt())
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
_playing = true;
|
||||
await _currentAudioSourcePlayer.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
_playing = false;
|
||||
_currentAudioSourcePlayer.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVolume(double volume) async {
|
||||
_audioElement.volume = volume;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setSpeed(double speed) async {
|
||||
_audioElement.playbackRate = speed;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setLoopMode(int mode) async {
|
||||
_loopMode = LoopMode.values[mode];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setShuffleModeEnabled(bool enabled) async {
|
||||
_shuffleModeEnabled = enabled;
|
||||
if (enabled) {
|
||||
_audioSourcePlayer?.shuffle(0, _index);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(int position, int newIndex) async {
|
||||
int index = newIndex ?? _index;
|
||||
if (index != _index) {
|
||||
_currentAudioSourcePlayer.pause();
|
||||
_index = index;
|
||||
await _currentAudioSourcePlayer.load();
|
||||
await _currentAudioSourcePlayer.seek(position);
|
||||
if (_playing) {
|
||||
_currentAudioSourcePlayer.play();
|
||||
}
|
||||
} else {
|
||||
await _currentAudioSourcePlayer.seek(position);
|
||||
}
|
||||
}
|
||||
|
||||
ConcatenatingAudioSourcePlayer _concatenating(String playerId) =>
|
||||
_audioSourcePlayers[playerId] as ConcatenatingAudioSourcePlayer;
|
||||
|
||||
concatenatingAdd(String playerId, Map source) {
|
||||
final playlist = _concatenating(playerId);
|
||||
playlist.add(getAudioSource(source));
|
||||
}
|
||||
|
||||
concatenatingInsert(String playerId, int index, Map source) {
|
||||
_concatenating(playerId).insert(index, getAudioSource(source));
|
||||
if (index <= _index) {
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
concatenatingAddAll(String playerId, List sources) {
|
||||
_concatenating(playerId).addAll(getAudioSources(sources));
|
||||
}
|
||||
|
||||
concatenatingInsertAll(String playerId, int index, List sources) {
|
||||
_concatenating(playerId).insertAll(index, getAudioSources(sources));
|
||||
if (index <= _index) {
|
||||
_index += sources.length;
|
||||
}
|
||||
}
|
||||
|
||||
concatenatingRemoveAt(String playerId, int index) async {
|
||||
// Pause if removing current item
|
||||
if (_index == index && _playing) {
|
||||
_currentAudioSourcePlayer.pause();
|
||||
}
|
||||
_concatenating(playerId).removeAt(index);
|
||||
if (_index == index) {
|
||||
// Skip backward if there's nothing after this
|
||||
if (index == _audioSourcePlayer.sequence.length) {
|
||||
_index--;
|
||||
}
|
||||
// Resume playback at the new item (if it exists)
|
||||
if (_playing && _currentAudioSourcePlayer != null) {
|
||||
await _currentAudioSourcePlayer.load();
|
||||
_currentAudioSourcePlayer.play();
|
||||
}
|
||||
} else if (index < _index) {
|
||||
// Reflect that the current item has shifted its position
|
||||
_index--;
|
||||
}
|
||||
}
|
||||
|
||||
concatenatingRemoveRange(String playerId, int start, int end) async {
|
||||
if (_index >= start && _index < end && _playing) {
|
||||
// Pause if removing current item
|
||||
_currentAudioSourcePlayer.pause();
|
||||
}
|
||||
_concatenating(playerId).removeRange(start, end);
|
||||
if (_index >= start && _index < end) {
|
||||
// Skip backward if there's nothing after this
|
||||
if (start >= _audioSourcePlayer.sequence.length) {
|
||||
_index = start - 1;
|
||||
} else {
|
||||
_index = start;
|
||||
}
|
||||
// Resume playback at the new item (if it exists)
|
||||
if (_playing && _currentAudioSourcePlayer != null) {
|
||||
await _currentAudioSourcePlayer.load();
|
||||
_currentAudioSourcePlayer.play();
|
||||
}
|
||||
} else if (end <= _index) {
|
||||
// Reflect that the current item has shifted its position
|
||||
_index -= (end - start);
|
||||
}
|
||||
}
|
||||
|
||||
concatenatingMove(String playerId, int currentIndex, int newIndex) {
|
||||
_concatenating(playerId).move(currentIndex, newIndex);
|
||||
if (currentIndex == _index) {
|
||||
_index = newIndex;
|
||||
} else if (currentIndex < _index && newIndex >= _index) {
|
||||
_index--;
|
||||
} else if (currentIndex > _index && newIndex <= _index) {
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
concatenatingClear(String playerId) {
|
||||
_currentAudioSourcePlayer.pause();
|
||||
_concatenating(playerId).clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Duration getCurrentPosition() => _currentAudioSourcePlayer?.position;
|
||||
|
||||
@override
|
||||
Duration getBufferedPosition() => _currentAudioSourcePlayer?.bufferedPosition;
|
||||
|
||||
@override
|
||||
Duration getDuration() => _currentAudioSourcePlayer?.duration;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_currentAudioSourcePlayer?.pause();
|
||||
_audioElement.removeAttribute('src');
|
||||
_audioElement.load();
|
||||
transition(ProcessingState.none);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<AudioSourcePlayer> getAudioSources(List json) =>
|
||||
json.map((s) => getAudioSource(s)).toList();
|
||||
|
||||
AudioSourcePlayer getAudioSource(Map json) {
|
||||
final String id = json['id'];
|
||||
var audioSourcePlayer = _audioSourcePlayers[id];
|
||||
if (audioSourcePlayer == null) {
|
||||
audioSourcePlayer = decodeAudioSource(json);
|
||||
_audioSourcePlayers[id] = audioSourcePlayer;
|
||||
}
|
||||
return audioSourcePlayer;
|
||||
}
|
||||
|
||||
AudioSourcePlayer decodeAudioSource(Map json) {
|
||||
try {
|
||||
switch (json['type']) {
|
||||
case 'progressive':
|
||||
return ProgressiveAudioSourcePlayer(
|
||||
this, json['id'], Uri.parse(json['uri']), json['headers']);
|
||||
case "dash":
|
||||
return DashAudioSourcePlayer(
|
||||
this, json['id'], Uri.parse(json['uri']), json['headers']);
|
||||
case "hls":
|
||||
return HlsAudioSourcePlayer(
|
||||
this, json['id'], Uri.parse(json['uri']), json['headers']);
|
||||
case "concatenating":
|
||||
return ConcatenatingAudioSourcePlayer(
|
||||
this,
|
||||
json['id'],
|
||||
getAudioSources(json['audioSources']),
|
||||
json['useLazyPreparation']);
|
||||
case "clipping":
|
||||
return ClippingAudioSourcePlayer(
|
||||
this,
|
||||
json['id'],
|
||||
getAudioSource(json['audioSource']),
|
||||
Duration(milliseconds: json['start']),
|
||||
Duration(milliseconds: json['end']));
|
||||
case "looping":
|
||||
return LoopingAudioSourcePlayer(this, json['id'],
|
||||
getAudioSource(json['audioSource']), json['count']);
|
||||
default:
|
||||
throw Exception("Unknown AudioSource type: " + json['type']);
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
print("$stacktrace");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AudioSourcePlayer {
|
||||
Html5AudioPlayer html5AudioPlayer;
|
||||
final String id;
|
||||
|
||||
AudioSourcePlayer(this.html5AudioPlayer, this.id);
|
||||
|
||||
List<IndexedAudioSourcePlayer> get sequence;
|
||||
|
||||
List<int> get shuffleOrder;
|
||||
|
||||
int shuffle(int treeIndex, int currentIndex);
|
||||
}
|
||||
|
||||
abstract class IndexedAudioSourcePlayer extends AudioSourcePlayer {
|
||||
IndexedAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id)
|
||||
: super(html5AudioPlayer, id);
|
||||
|
||||
Future<Duration> load();
|
||||
|
||||
Future<void> play();
|
||||
|
||||
Future<void> pause();
|
||||
|
||||
Future<void> seek(int position);
|
||||
|
||||
Future<void> complete();
|
||||
|
||||
Future<void> timeUpdated(double seconds) async {}
|
||||
|
||||
Duration get duration;
|
||||
|
||||
Duration get position;
|
||||
|
||||
Duration get bufferedPosition;
|
||||
|
||||
AudioElement get _audioElement => html5AudioPlayer._audioElement;
|
||||
|
||||
@override
|
||||
int shuffle(int treeIndex, int currentIndex) => treeIndex + 1;
|
||||
|
||||
@override
|
||||
String toString() => "${this.runtimeType}";
|
||||
}
|
||||
|
||||
abstract class UriAudioSourcePlayer extends IndexedAudioSourcePlayer {
|
||||
final Uri uri;
|
||||
final Map headers;
|
||||
double _resumePos;
|
||||
Duration _duration;
|
||||
Completer _completer;
|
||||
|
||||
UriAudioSourcePlayer(
|
||||
Html5AudioPlayer html5AudioPlayer, String id, this.uri, this.headers)
|
||||
: super(html5AudioPlayer, id);
|
||||
|
||||
@override
|
||||
List<IndexedAudioSourcePlayer> get sequence => [this];
|
||||
|
||||
@override
|
||||
List<int> get shuffleOrder => [0];
|
||||
|
||||
@override
|
||||
Future<Duration> load() async {
|
||||
_resumePos = 0.0;
|
||||
return _duration = await html5AudioPlayer.loadUri(uri);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
_audioElement.currentTime = _resumePos;
|
||||
_audioElement.play();
|
||||
_completer = Completer();
|
||||
await _completer.future;
|
||||
_completer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
_resumePos = _audioElement.currentTime;
|
||||
_audioElement.pause();
|
||||
_interruptPlay();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(int position) async {
|
||||
_audioElement.currentTime = _resumePos = position / 1000.0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> complete() async {
|
||||
_interruptPlay();
|
||||
html5AudioPlayer.onEnded();
|
||||
}
|
||||
|
||||
_interruptPlay() {
|
||||
if (_completer?.isCompleted == false) {
|
||||
_completer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get duration {
|
||||
return _duration;
|
||||
//final seconds = _audioElement.duration;
|
||||
//return seconds.isFinite
|
||||
// ? Duration(milliseconds: (seconds * 1000).toInt())
|
||||
// : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get position {
|
||||
double seconds = _audioElement.currentTime;
|
||||
return Duration(milliseconds: (seconds * 1000).toInt());
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get bufferedPosition {
|
||||
if (_audioElement.buffered.length > 0) {
|
||||
return Duration(
|
||||
milliseconds:
|
||||
(_audioElement.buffered.end(_audioElement.buffered.length - 1) *
|
||||
1000)
|
||||
.toInt());
|
||||
} else {
|
||||
return Duration.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ProgressiveAudioSourcePlayer extends UriAudioSourcePlayer {
|
||||
ProgressiveAudioSourcePlayer(
|
||||
Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers)
|
||||
: super(html5AudioPlayer, id, uri, headers);
|
||||
}
|
||||
|
||||
class DashAudioSourcePlayer extends UriAudioSourcePlayer {
|
||||
DashAudioSourcePlayer(
|
||||
Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers)
|
||||
: super(html5AudioPlayer, id, uri, headers);
|
||||
}
|
||||
|
||||
class HlsAudioSourcePlayer extends UriAudioSourcePlayer {
|
||||
HlsAudioSourcePlayer(
|
||||
Html5AudioPlayer html5AudioPlayer, String id, Uri uri, Map headers)
|
||||
: super(html5AudioPlayer, id, uri, headers);
|
||||
}
|
||||
|
||||
class ConcatenatingAudioSourcePlayer extends AudioSourcePlayer {
|
||||
static List<int> generateShuffleOrder(int length, [int firstIndex]) {
|
||||
final shuffleOrder = List<int>(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
final j = _random.nextInt(i + 1);
|
||||
shuffleOrder[i] = shuffleOrder[j];
|
||||
shuffleOrder[j] = i;
|
||||
}
|
||||
if (firstIndex != null) {
|
||||
for (var i = 1; i < length; i++) {
|
||||
if (shuffleOrder[i] == firstIndex) {
|
||||
final v = shuffleOrder[0];
|
||||
shuffleOrder[0] = shuffleOrder[i];
|
||||
shuffleOrder[i] = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shuffleOrder;
|
||||
}
|
||||
|
||||
final List<AudioSourcePlayer> audioSourcePlayers;
|
||||
final bool useLazyPreparation;
|
||||
List<int> _shuffleOrder;
|
||||
|
||||
ConcatenatingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id,
|
||||
this.audioSourcePlayers, this.useLazyPreparation)
|
||||
: _shuffleOrder = generateShuffleOrder(audioSourcePlayers.length),
|
||||
super(html5AudioPlayer, id);
|
||||
|
||||
@override
|
||||
List<IndexedAudioSourcePlayer> get sequence =>
|
||||
audioSourcePlayers.expand((p) => p.sequence).toList();
|
||||
|
||||
@override
|
||||
List<int> get shuffleOrder {
|
||||
final order = <int>[];
|
||||
var offset = order.length;
|
||||
final childOrders = <List<int>>[];
|
||||
for (var audioSourcePlayer in audioSourcePlayers) {
|
||||
final childShuffleOrder = audioSourcePlayer.shuffleOrder;
|
||||
childOrders.add(childShuffleOrder.map((i) => i + offset).toList());
|
||||
offset += childShuffleOrder.length;
|
||||
}
|
||||
for (var i = 0; i < childOrders.length; i++) {
|
||||
order.addAll(childOrders[_shuffleOrder[i]]);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
@override
|
||||
int shuffle(int treeIndex, int currentIndex) {
|
||||
int currentChildIndex;
|
||||
for (var i = 0; i < audioSourcePlayers.length; i++) {
|
||||
final indexBefore = treeIndex;
|
||||
final child = audioSourcePlayers[i];
|
||||
treeIndex = child.shuffle(treeIndex, currentIndex);
|
||||
if (currentIndex >= indexBefore && currentIndex < treeIndex) {
|
||||
currentChildIndex = i;
|
||||
} else {}
|
||||
}
|
||||
// Shuffle so that the current child is first in the shuffle order
|
||||
_shuffleOrder =
|
||||
generateShuffleOrder(audioSourcePlayers.length, currentChildIndex);
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
add(AudioSourcePlayer player) {
|
||||
audioSourcePlayers.add(player);
|
||||
_shuffleOrder.add(audioSourcePlayers.length - 1);
|
||||
}
|
||||
|
||||
insert(int index, AudioSourcePlayer player) {
|
||||
audioSourcePlayers.insert(index, player);
|
||||
for (var i = 0; i < audioSourcePlayers.length; i++) {
|
||||
if (_shuffleOrder[i] >= index) {
|
||||
_shuffleOrder[i]++;
|
||||
}
|
||||
}
|
||||
_shuffleOrder.add(index);
|
||||
}
|
||||
|
||||
addAll(List<AudioSourcePlayer> players) {
|
||||
audioSourcePlayers.addAll(players);
|
||||
_shuffleOrder.addAll(
|
||||
List.generate(players.length, (i) => audioSourcePlayers.length + i)
|
||||
.toList()
|
||||
..shuffle());
|
||||
}
|
||||
|
||||
insertAll(int index, List<AudioSourcePlayer> players) {
|
||||
audioSourcePlayers.insertAll(index, players);
|
||||
for (var i = 0; i < audioSourcePlayers.length; i++) {
|
||||
if (_shuffleOrder[i] >= index) {
|
||||
_shuffleOrder[i] += players.length;
|
||||
}
|
||||
}
|
||||
_shuffleOrder.addAll(
|
||||
List.generate(players.length, (i) => index + i).toList()..shuffle());
|
||||
}
|
||||
|
||||
removeAt(int index) {
|
||||
audioSourcePlayers.removeAt(index);
|
||||
// 0 1 2 3
|
||||
// 3 2 0 1
|
||||
for (var i = 0; i < audioSourcePlayers.length; i++) {
|
||||
if (_shuffleOrder[i] > index) {
|
||||
_shuffleOrder[i]--;
|
||||
}
|
||||
}
|
||||
_shuffleOrder.removeWhere((i) => i == index);
|
||||
}
|
||||
|
||||
removeRange(int start, int end) {
|
||||
audioSourcePlayers.removeRange(start, end);
|
||||
for (var i = 0; i < audioSourcePlayers.length; i++) {
|
||||
if (_shuffleOrder[i] >= end) {
|
||||
_shuffleOrder[i] -= (end - start);
|
||||
}
|
||||
}
|
||||
_shuffleOrder.removeWhere((i) => i >= start && i < end);
|
||||
}
|
||||
|
||||
move(int currentIndex, int newIndex) {
|
||||
audioSourcePlayers.insert(
|
||||
newIndex, audioSourcePlayers.removeAt(currentIndex));
|
||||
}
|
||||
|
||||
clear() {
|
||||
audioSourcePlayers.clear();
|
||||
_shuffleOrder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class ClippingAudioSourcePlayer extends IndexedAudioSourcePlayer {
|
||||
final UriAudioSourcePlayer audioSourcePlayer;
|
||||
final Duration start;
|
||||
final Duration end;
|
||||
Completer<ClipInterruptReason> _completer;
|
||||
double _resumePos;
|
||||
Duration _duration;
|
||||
|
||||
ClippingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id,
|
||||
this.audioSourcePlayer, this.start, this.end)
|
||||
: super(html5AudioPlayer, id);
|
||||
|
||||
@override
|
||||
List<IndexedAudioSourcePlayer> get sequence => [this];
|
||||
|
||||
@override
|
||||
List<int> get shuffleOrder => [0];
|
||||
|
||||
@override
|
||||
Future<Duration> load() async {
|
||||
_resumePos = (start ?? Duration.zero).inMilliseconds / 1000.0;
|
||||
Duration fullDuration =
|
||||
await html5AudioPlayer.loadUri(audioSourcePlayer.uri);
|
||||
_audioElement.currentTime = _resumePos;
|
||||
_duration = Duration(
|
||||
milliseconds: min((end ?? fullDuration).inMilliseconds,
|
||||
fullDuration.inMilliseconds) -
|
||||
(start ?? Duration.zero).inMilliseconds);
|
||||
return _duration;
|
||||
}
|
||||
|
||||
double get remaining => end.inMilliseconds / 1000 - _audioElement.currentTime;
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
_interruptPlay(ClipInterruptReason.simultaneous);
|
||||
_audioElement.currentTime = _resumePos;
|
||||
_audioElement.play();
|
||||
_completer = Completer<ClipInterruptReason>();
|
||||
ClipInterruptReason reason;
|
||||
while ((reason = await _completer.future) == ClipInterruptReason.seek) {
|
||||
_completer = Completer<ClipInterruptReason>();
|
||||
}
|
||||
if (reason == ClipInterruptReason.end) {
|
||||
html5AudioPlayer.onEnded();
|
||||
}
|
||||
_completer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
_interruptPlay(ClipInterruptReason.pause);
|
||||
_resumePos = _audioElement.currentTime;
|
||||
_audioElement.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(int position) async {
|
||||
_interruptPlay(ClipInterruptReason.seek);
|
||||
_audioElement.currentTime =
|
||||
_resumePos = start.inMilliseconds / 1000.0 + position / 1000.0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> complete() async {
|
||||
_interruptPlay(ClipInterruptReason.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> timeUpdated(double seconds) async {
|
||||
if (end != null) {
|
||||
if (seconds >= end.inMilliseconds / 1000) {
|
||||
_interruptPlay(ClipInterruptReason.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get duration {
|
||||
return _duration;
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get position {
|
||||
double seconds = _audioElement.currentTime;
|
||||
var position = Duration(milliseconds: (seconds * 1000).toInt());
|
||||
if (start != null) {
|
||||
position -= start;
|
||||
}
|
||||
if (position < Duration.zero) {
|
||||
position = Duration.zero;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get bufferedPosition {
|
||||
if (_audioElement.buffered.length > 0) {
|
||||
var seconds =
|
||||
_audioElement.buffered.end(_audioElement.buffered.length - 1);
|
||||
var position = Duration(milliseconds: (seconds * 1000).toInt());
|
||||
if (start != null) {
|
||||
position -= start;
|
||||
}
|
||||
if (position < Duration.zero) {
|
||||
position = Duration.zero;
|
||||
}
|
||||
if (duration != null && position > duration) {
|
||||
position = duration;
|
||||
}
|
||||
return position;
|
||||
} else {
|
||||
return Duration.zero;
|
||||
}
|
||||
}
|
||||
|
||||
_interruptPlay(ClipInterruptReason reason) {
|
||||
if (_completer?.isCompleted == false) {
|
||||
_completer.complete(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ClipInterruptReason { end, pause, seek, simultaneous }
|
||||
|
||||
class LoopingAudioSourcePlayer extends AudioSourcePlayer {
|
||||
final AudioSourcePlayer audioSourcePlayer;
|
||||
final int count;
|
||||
|
||||
LoopingAudioSourcePlayer(Html5AudioPlayer html5AudioPlayer, String id,
|
||||
this.audioSourcePlayer, this.count)
|
||||
: super(html5AudioPlayer, id);
|
||||
|
||||
@override
|
||||
List<IndexedAudioSourcePlayer> get sequence =>
|
||||
List.generate(count, (i) => audioSourcePlayer)
|
||||
.expand((p) => p.sequence)
|
||||
.toList();
|
||||
|
||||
@override
|
||||
List<int> get shuffleOrder {
|
||||
final order = <int>[];
|
||||
var offset = order.length;
|
||||
for (var i = 0; i < count; i++) {
|
||||
final childShuffleOrder = audioSourcePlayer.shuffleOrder;
|
||||
order.addAll(childShuffleOrder.map((i) => i + offset).toList());
|
||||
offset += childShuffleOrder.length;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
@override
|
||||
int shuffle(int treeIndex, int currentIndex) {
|
||||
for (var i = 0; i < count; i++) {
|
||||
treeIndex = audioSourcePlayer.shuffle(treeIndex, currentIndex);
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
}
|
37
macos/.gitignore
vendored
Normal file
37
macos/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
.idea/
|
||||
.vagrant/
|
||||
.sconsign.dblite
|
||||
.svn/
|
||||
|
||||
.DS_Store
|
||||
*.swp
|
||||
profile
|
||||
|
||||
DerivedData/
|
||||
build/
|
||||
GeneratedPluginRegistrant.h
|
||||
GeneratedPluginRegistrant.m
|
||||
|
||||
.generated/
|
||||
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
|
||||
!default.pbxuser
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.perspectivev3
|
||||
|
||||
xcuserdata
|
||||
|
||||
*.moved-aside
|
||||
|
||||
*.pyc
|
||||
*sync/
|
||||
Icon?
|
||||
.tags*
|
||||
|
||||
/Flutter/Generated.xcconfig
|
||||
/Flutter/flutter_export_environment.sh
|
0
macos/Assets/.gitkeep
Normal file
0
macos/Assets/.gitkeep
Normal file
21
macos/Classes/AudioPlayer.h
Normal file
21
macos/Classes/AudioPlayer.h
Normal file
@ -0,0 +1,21 @@
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface AudioPlayer : NSObject<FlutterStreamHandler>
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar playerId:(NSString*)idParam configuredSession:(BOOL)configuredSession;
|
||||
|
||||
@end
|
||||
|
||||
enum ProcessingState {
|
||||
none,
|
||||
loading,
|
||||
buffering,
|
||||
ready,
|
||||
completed
|
||||
};
|
||||
|
||||
enum LoopMode {
|
||||
loopOff,
|
||||
loopOne,
|
||||
loopAll
|
||||
};
|
1138
macos/Classes/AudioPlayer.m
Normal file
1138
macos/Classes/AudioPlayer.m
Normal file
File diff suppressed because it is too large
Load Diff
13
macos/Classes/AudioSource.h
Normal file
13
macos/Classes/AudioSource.h
Normal file
@ -0,0 +1,13 @@
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface AudioSource : NSObject
|
||||
|
||||
@property (readonly, nonatomic) NSString* sourceId;
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid;
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex;
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches;
|
||||
- (NSArray *)getShuffleOrder;
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex;
|
||||
|
||||
@end
|
37
macos/Classes/AudioSource.m
Normal file
37
macos/Classes/AudioSource.m
Normal file
@ -0,0 +1,37 @@
|
||||
#import "AudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation AudioSource {
|
||||
NSString *_sourceId;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_sourceId = sid;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)sourceId {
|
||||
return _sourceId;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
if ([_sourceId isEqualToString:sourceId]) {
|
||||
[matches addObject:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
11
macos/Classes/ClippingAudioSource.h
Normal file
11
macos/Classes/ClippingAudioSource.h
Normal file
@ -0,0 +1,11 @@
|
||||
#import "AudioSource.h"
|
||||
#import "UriAudioSource.h"
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface ClippingAudioSource : IndexedAudioSource
|
||||
|
||||
@property (readonly, nonatomic) UriAudioSource* audioSource;
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end;
|
||||
|
||||
@end
|
79
macos/Classes/ClippingAudioSource.m
Normal file
79
macos/Classes/ClippingAudioSource.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "AudioSource.h"
|
||||
#import "ClippingAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import "UriAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation ClippingAudioSource {
|
||||
UriAudioSource *_audioSource;
|
||||
CMTime _start;
|
||||
CMTime _end;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSource:(UriAudioSource *)audioSource start:(NSNumber *)start end:(NSNumber *)end {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSource = audioSource;
|
||||
_start = start == [NSNull null] ? kCMTimeZero : CMTimeMake([start intValue], 1000);
|
||||
_end = end == [NSNull null] ? kCMTimeInvalid : CMTimeMake([end intValue], 1000);
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UriAudioSource *)audioSource {
|
||||
return _audioSource;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
[_audioSource findById:sourceId matches:matches];
|
||||
}
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
[super attach:player];
|
||||
_audioSource.playerItem.forwardPlaybackEndTime = _end;
|
||||
// XXX: Not needed since currentItem observer handles it?
|
||||
[self seek:kCMTimeZero];
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _audioSource.playerItem;
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
if (!completionHandler || (self.playerItem.status == AVPlayerItemStatusReadyToPlay)) {
|
||||
CMTime absPosition = CMTimeAdd(_start, position);
|
||||
[_audioSource.playerItem seekToTime:absPosition toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return CMTimeSubtract(CMTIME_IS_INVALID(_end) ? self.playerItem.duration : _end, _start);
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return CMTimeSubtract(self.playerItem.currentTime, _start);
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
CMTime pos = CMTimeSubtract(_audioSource.bufferedPosition, _start);
|
||||
CMTime dur = [self duration];
|
||||
return CMTimeCompare(pos, dur) >= 0 ? dur : pos;
|
||||
}
|
||||
|
||||
@end
|
13
macos/Classes/ConcatenatingAudioSource.h
Normal file
13
macos/Classes/ConcatenatingAudioSource.h
Normal file
@ -0,0 +1,13 @@
|
||||
#import "AudioSource.h"
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface ConcatenatingAudioSource : AudioSource
|
||||
|
||||
@property (readonly, nonatomic) int count;
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray<AudioSource *> *)audioSources;
|
||||
- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index;
|
||||
- (void)removeSourcesFromIndex:(int)start toIndex:(int)end;
|
||||
- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex;
|
||||
|
||||
@end
|
109
macos/Classes/ConcatenatingAudioSource.m
Normal file
109
macos/Classes/ConcatenatingAudioSource.m
Normal file
@ -0,0 +1,109 @@
|
||||
#import "AudioSource.h"
|
||||
#import "ConcatenatingAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <stdlib.h>
|
||||
|
||||
@implementation ConcatenatingAudioSource {
|
||||
NSMutableArray<AudioSource *> *_audioSources;
|
||||
NSMutableArray<NSNumber *> *_shuffleOrder;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray<AudioSource *> *)audioSources {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSources = audioSources;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)count {
|
||||
return _audioSources.count;
|
||||
}
|
||||
|
||||
- (void)insertSource:(AudioSource *)audioSource atIndex:(int)index {
|
||||
[_audioSources insertObject:audioSource atIndex:index];
|
||||
}
|
||||
|
||||
- (void)removeSourcesFromIndex:(int)start toIndex:(int)end {
|
||||
if (end == -1) end = _audioSources.count;
|
||||
for (int i = start; i < end; i++) {
|
||||
[_audioSources removeObjectAtIndex:start];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex {
|
||||
AudioSource *source = _audioSources[currentIndex];
|
||||
[_audioSources removeObjectAtIndex:currentIndex];
|
||||
[_audioSources insertObject:source atIndex:newIndex];
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_audioSources[i] findById:sourceId matches:matches];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
NSMutableArray *order = [NSMutableArray new];
|
||||
int offset = [order count];
|
||||
NSMutableArray *childOrders = [NSMutableArray new]; // array of array of ints
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
AudioSource *audioSource = _audioSources[i];
|
||||
NSArray *childShuffleOrder = [audioSource getShuffleOrder];
|
||||
NSMutableArray *offsetChildShuffleOrder = [NSMutableArray new];
|
||||
for (int j = 0; j < [childShuffleOrder count]; j++) {
|
||||
[offsetChildShuffleOrder addObject:@([childShuffleOrder[j] integerValue] + offset)];
|
||||
}
|
||||
[childOrders addObject:offsetChildShuffleOrder];
|
||||
offset += [childShuffleOrder count];
|
||||
}
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[order addObjectsFromArray:childOrders[[_shuffleOrder[i] integerValue]]];
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
int currentChildIndex = -1;
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
int indexBefore = treeIndex;
|
||||
AudioSource *child = _audioSources[i];
|
||||
treeIndex = [child shuffle:treeIndex currentIndex:currentIndex];
|
||||
if (currentIndex >= indexBefore && currentIndex < treeIndex) {
|
||||
currentChildIndex = i;
|
||||
} else {}
|
||||
}
|
||||
// Shuffle so that the current child is first in the shuffle order
|
||||
_shuffleOrder = [NSMutableArray arrayWithCapacity:[_audioSources count]];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_shuffleOrder addObject:@(0)];
|
||||
}
|
||||
NSLog(@"shuffle: audioSources.count=%d and shuffleOrder.count=%d", [_audioSources count], [_shuffleOrder count]);
|
||||
// First generate a random shuffle
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
int j = arc4random_uniform(i + 1);
|
||||
_shuffleOrder[i] = _shuffleOrder[j];
|
||||
_shuffleOrder[j] = @(i);
|
||||
}
|
||||
// Then bring currentIndex to the front
|
||||
if (currentChildIndex != -1) {
|
||||
for (int i = 1; i < [_audioSources count]; i++) {
|
||||
if ([_shuffleOrder[i] integerValue] == currentChildIndex) {
|
||||
NSNumber *v = _shuffleOrder[0];
|
||||
_shuffleOrder[0] = _shuffleOrder[i];
|
||||
_shuffleOrder[i] = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
@end
|
21
macos/Classes/IndexedAudioSource.h
Normal file
21
macos/Classes/IndexedAudioSource.h
Normal file
@ -0,0 +1,21 @@
|
||||
#import "AudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface IndexedAudioSource : AudioSource
|
||||
|
||||
@property (readonly, nonatomic) IndexedPlayerItem *playerItem;
|
||||
@property (readwrite, nonatomic) CMTime duration;
|
||||
@property (readonly, nonatomic) CMTime position;
|
||||
@property (readonly, nonatomic) CMTime bufferedPosition;
|
||||
@property (readonly, nonatomic) BOOL isAttached;
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player;
|
||||
- (void)play:(AVQueuePlayer *)player;
|
||||
- (void)pause:(AVQueuePlayer *)player;
|
||||
- (void)stop:(AVQueuePlayer *)player;
|
||||
- (void)seek:(CMTime)position;
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler;
|
||||
|
||||
@end
|
68
macos/Classes/IndexedAudioSource.m
Normal file
68
macos/Classes/IndexedAudioSource.m
Normal file
@ -0,0 +1,68 @@
|
||||
#import "IndexedAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation IndexedAudioSource {
|
||||
BOOL _isAttached;
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_isAttached = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isAttached {
|
||||
return _isAttached;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
[sequence addObject:self];
|
||||
return treeIndex + 1;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
return treeIndex + 1;
|
||||
}
|
||||
|
||||
- (void)attach:(AVQueuePlayer *)player {
|
||||
_isAttached = YES;
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position {
|
||||
[self seek:position completionHandler:nil];
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
@end
|
9
macos/Classes/IndexedPlayerItem.h
Normal file
9
macos/Classes/IndexedPlayerItem.h
Normal file
@ -0,0 +1,9 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@class IndexedAudioSource;
|
||||
|
||||
@interface IndexedPlayerItem : AVPlayerItem
|
||||
|
||||
@property (readwrite, nonatomic) IndexedAudioSource *audioSource;
|
||||
|
||||
@end
|
16
macos/Classes/IndexedPlayerItem.m
Normal file
16
macos/Classes/IndexedPlayerItem.m
Normal file
@ -0,0 +1,16 @@
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import "IndexedAudioSource.h"
|
||||
|
||||
@implementation IndexedPlayerItem {
|
||||
IndexedAudioSource *_audioSource;
|
||||
}
|
||||
|
||||
-(void)setAudioSource:(IndexedAudioSource *)audioSource {
|
||||
_audioSource = audioSource;
|
||||
}
|
||||
|
||||
-(IndexedAudioSource *)audioSource {
|
||||
return _audioSource;
|
||||
}
|
||||
|
||||
@end
|
4
macos/Classes/JustAudioPlugin.h
Normal file
4
macos/Classes/JustAudioPlugin.h
Normal file
@ -0,0 +1,4 @@
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface JustAudioPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
55
macos/Classes/JustAudioPlugin.m
Normal file
55
macos/Classes/JustAudioPlugin.m
Normal file
@ -0,0 +1,55 @@
|
||||
#import "JustAudioPlugin.h"
|
||||
#import "AudioPlayer.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
@implementation JustAudioPlugin {
|
||||
NSObject<FlutterPluginRegistrar>* _registrar;
|
||||
BOOL _configuredSession;
|
||||
}
|
||||
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
FlutterMethodChannel* channel = [FlutterMethodChannel
|
||||
methodChannelWithName:@"com.ryanheise.just_audio.methods"
|
||||
binaryMessenger:[registrar messenger]];
|
||||
JustAudioPlugin* instance = [[JustAudioPlugin alloc] initWithRegistrar:registrar];
|
||||
[registrar addMethodCallDelegate:instance channel:channel];
|
||||
}
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_registrar = registrar;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||
if ([@"init" isEqualToString:call.method]) {
|
||||
NSArray* args = (NSArray*)call.arguments;
|
||||
NSString* playerId = args[0];
|
||||
/*AudioPlayer* player =*/ [[AudioPlayer alloc] initWithRegistrar:_registrar playerId:playerId configuredSession:_configuredSession];
|
||||
result(nil);
|
||||
} else if ([@"setIosCategory" isEqualToString:call.method]) {
|
||||
#if TARGET_OS_IPHONE
|
||||
NSNumber* categoryIndex = (NSNumber*)call.arguments;
|
||||
AVAudioSessionCategory category = nil;
|
||||
switch (categoryIndex.integerValue) {
|
||||
case 0: category = AVAudioSessionCategoryAmbient; break;
|
||||
case 1: category = AVAudioSessionCategorySoloAmbient; break;
|
||||
case 2: category = AVAudioSessionCategoryPlayback; break;
|
||||
case 3: category = AVAudioSessionCategoryRecord; break;
|
||||
case 4: category = AVAudioSessionCategoryPlayAndRecord; break;
|
||||
case 5: category = AVAudioSessionCategoryMultiRoute; break;
|
||||
}
|
||||
if (category) {
|
||||
_configuredSession = YES;
|
||||
}
|
||||
[[AVAudioSession sharedInstance] setCategory:category error:nil];
|
||||
#endif
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
8
macos/Classes/LoopingAudioSource.h
Normal file
8
macos/Classes/LoopingAudioSource.h
Normal file
@ -0,0 +1,8 @@
|
||||
#import "AudioSource.h"
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface LoopingAudioSource : AudioSource
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray<AudioSource *> *)audioSources;
|
||||
|
||||
@end
|
53
macos/Classes/LoopingAudioSource.m
Normal file
53
macos/Classes/LoopingAudioSource.m
Normal file
@ -0,0 +1,53 @@
|
||||
#import "AudioSource.h"
|
||||
#import "LoopingAudioSource.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation LoopingAudioSource {
|
||||
// An array of duplicates
|
||||
NSArray<AudioSource *> *_audioSources; // <AudioSource *>
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid audioSources:(NSArray<AudioSource *> *)audioSources {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_audioSources = audioSources;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex {
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
- (void)findById:(NSString *)sourceId matches:(NSMutableArray<AudioSource *> *)matches {
|
||||
[super findById:sourceId matches:matches];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
[_audioSources[i] findById:sourceId matches:matches];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
NSMutableArray *order = [NSMutableArray new];
|
||||
int offset = (int)[order count];
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
AudioSource *audioSource = _audioSources[i];
|
||||
NSArray *childShuffleOrder = [audioSource getShuffleOrder];
|
||||
for (int j = 0; j < [childShuffleOrder count]; j++) {
|
||||
[order addObject:@([childShuffleOrder[j] integerValue] + offset)];
|
||||
}
|
||||
offset += [childShuffleOrder count];
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
- (int)shuffle:(int)treeIndex currentIndex:(int)currentIndex {
|
||||
// TODO: This should probably shuffle the same way on all duplicates.
|
||||
for (int i = 0; i < [_audioSources count]; i++) {
|
||||
treeIndex = [_audioSources[i] shuffle:treeIndex currentIndex:currentIndex];
|
||||
}
|
||||
return treeIndex;
|
||||
}
|
||||
|
||||
@end
|
8
macos/Classes/UriAudioSource.h
Normal file
8
macos/Classes/UriAudioSource.h
Normal file
@ -0,0 +1,8 @@
|
||||
#import "IndexedAudioSource.h"
|
||||
#import <FlutterMacOS/FlutterMacOS.h>
|
||||
|
||||
@interface UriAudioSource : IndexedAudioSource
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri;
|
||||
|
||||
@end
|
79
macos/Classes/UriAudioSource.m
Normal file
79
macos/Classes/UriAudioSource.m
Normal file
@ -0,0 +1,79 @@
|
||||
#import "UriAudioSource.h"
|
||||
#import "IndexedAudioSource.h"
|
||||
#import "IndexedPlayerItem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation UriAudioSource {
|
||||
NSString *_uri;
|
||||
IndexedPlayerItem *_playerItem;
|
||||
/* CMTime _duration; */
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri {
|
||||
self = [super initWithId:sid];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_uri = uri;
|
||||
if ([_uri hasPrefix:@"file://"]) {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[_uri substringFromIndex:7]]];
|
||||
} else {
|
||||
_playerItem = [[IndexedPlayerItem alloc] initWithURL:[NSURL URLWithString:_uri]];
|
||||
}
|
||||
if (@available(macOS 10.13, iOS 11.0, *)) {
|
||||
// This does the best at reducing distortion on voice with speeds below 1.0
|
||||
_playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain;
|
||||
}
|
||||
/* NSKeyValueObservingOptions options = */
|
||||
/* NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; */
|
||||
/* [_playerItem addObserver:self */
|
||||
/* forKeyPath:@"duration" */
|
||||
/* options:options */
|
||||
/* context:nil]; */
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IndexedPlayerItem *)playerItem {
|
||||
return _playerItem;
|
||||
}
|
||||
|
||||
- (NSArray *)getShuffleOrder {
|
||||
return @[@(0)];
|
||||
}
|
||||
|
||||
- (void)play:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)pause:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)stop:(AVQueuePlayer *)player {
|
||||
}
|
||||
|
||||
- (void)seek:(CMTime)position completionHandler:(void (^)(BOOL))completionHandler {
|
||||
if (!completionHandler || (_playerItem.status == AVPlayerItemStatusReadyToPlay)) {
|
||||
[_playerItem seekToTime:position toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CMTime)duration {
|
||||
return _playerItem.duration;
|
||||
}
|
||||
|
||||
- (void)setDuration:(CMTime)duration {
|
||||
}
|
||||
|
||||
- (CMTime)position {
|
||||
return _playerItem.currentTime;
|
||||
}
|
||||
|
||||
- (CMTime)bufferedPosition {
|
||||
NSValue *last = _playerItem.loadedTimeRanges.lastObject;
|
||||
if (last) {
|
||||
CMTimeRange timeRange = [last CMTimeRangeValue];
|
||||
return CMTimeAdd(timeRange.start, timeRange.duration);
|
||||
} else {
|
||||
return _playerItem.currentTime;
|
||||
}
|
||||
return kCMTimeInvalid;
|
||||
}
|
||||
|
||||
@end
|
21
macos/just_audio.podspec
Normal file
21
macos/just_audio.podspec
Normal file
@ -0,0 +1,21 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'just_audio'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'A new flutter plugin project.'
|
||||
s.description = <<-DESC
|
||||
A new flutter plugin project.
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.dependency 'FlutterMacOS'
|
||||
s.platform = :osx, '10.11'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
end
|
||||
|
250
pubspec.lock
Normal file
250
pubspec.lock
Normal file
@ -0,0 +1,250 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.13"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.8"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.10"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+1"
|
||||
path_provider_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.4+3"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.13"
|
||||
rxdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rxdart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.24.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.5"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.17"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
sdks:
|
||||
dart: ">=2.9.0-14.0.dev <3.0.0"
|
||||
flutter: ">=1.12.13+hotfix.5 <2.0.0"
|
37
pubspec.yaml
Normal file
37
pubspec.yaml
Normal file
@ -0,0 +1,37 @@
|
||||
name: just_audio
|
||||
description: Flutter plugin to play audio from streams, files, assets, DASH/HLS streams and playlists. Works with audio_service to play audio in the background.
|
||||
version: 0.3.1
|
||||
homepage: https://github.com/ryanheise/just_audio
|
||||
|
||||
environment:
|
||||
sdk: '>=2.6.0 <3.0.0'
|
||||
flutter: ">=1.12.8 <2.0.0"
|
||||
|
||||
dependencies:
|
||||
rxdart: ^0.24.1
|
||||
path: ^1.6.4
|
||||
path_provider: ^1.6.10
|
||||
async: ^2.4.1
|
||||
uuid: ^2.2.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: com.ryanheise.just_audio
|
||||
pluginClass: JustAudioPlugin
|
||||
ios:
|
||||
pluginClass: JustAudioPlugin
|
||||
macos:
|
||||
pluginClass: JustAudioPlugin
|
||||
web:
|
||||
pluginClass: JustAudioPlugin
|
||||
fileName: just_audio_web.dart
|
21
test/just_audio_test.dart
Normal file
21
test/just_audio_test.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
void main() {
|
||||
const MethodChannel channel = MethodChannel('just_audio');
|
||||
|
||||
setUp(() {
|
||||
channel.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
return '42';
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
channel.setMockMethodCallHandler(null);
|
||||
});
|
||||
|
||||
// test('getPlatformVersion', () async {
|
||||
// expect(await AudioPlayer.platformVersion, '42');
|
||||
// });
|
||||
}
|
Loading…
Reference in New Issue
Block a user