Updated packages, rewrote player = gapless playback, faster loading
This commit is contained in:
parent
6f250df004
commit
d4299f736f
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,8 +2,6 @@
|
|||||||
freezerkey.jsk
|
freezerkey.jsk
|
||||||
android/key.properties
|
android/key.properties
|
||||||
|
|
||||||
just_audio/
|
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
|
@ -27,7 +27,7 @@ https://t.me/freezerandroid
|
|||||||
Tobs: Beta tester
|
Tobs: Beta tester
|
||||||
Bas Curtiz: Icon, Logo, Banner, Design suggestions
|
Bas Curtiz: Icon, Logo, Banner, Design suggestions
|
||||||
Deemix: https://notabug.org/RemixDev/deemix
|
Deemix: https://notabug.org/RemixDev/deemix
|
||||||
just_audio: https://github.com/ryanheise/just_audio
|
just_audio && audio_service: https://github.com/ryanheise/just_audio
|
||||||
|
|
||||||
|
|
||||||
## Support me
|
## Support me
|
||||||
@ -35,7 +35,8 @@ BTC: `14hcr4PGbgqeXd3SoXY9QyJFNpyurgrL9y`
|
|||||||
ETH: `0xb4D1893195404E1F4b45e5BDA77F202Ac4012288`
|
ETH: `0xb4D1893195404E1F4b45e5BDA77F202Ac4012288`
|
||||||
|
|
||||||
## just_audio
|
## just_audio
|
||||||
This app depends on modified just_audio plugin with Deezer support. Repo: https://notabug.org/exttex/just_audio
|
This app depends on modified just_audio plugin with Deezer support.
|
||||||
|
The fork repo is deprecated, current version available in this repo.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
```
|
```
|
||||||
|
1
just_audio/.github/FUNDING.yml
vendored
Normal file
1
just_audio/.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: ryanheise
|
53
just_audio/.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
53
just_audio/.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
just_audio/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
just_audio/.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
just_audio/.github/ISSUE_TEMPLATE/documentation-request.md
vendored
Normal file
39
just_audio/.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
just_audio/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
37
just_audio/.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
just_audio/.gitignore
vendored
Normal file
70
just_audio/.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
just_audio/.metadata
Normal file
10
just_audio/.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
just_audio/CHANGELOG.md
Normal file
114
just_audio/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
just_audio/LICENSE
Normal file
229
just_audio/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
just_audio/README.md
Normal file
222
just_audio/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
just_audio/android/.gitignore
vendored
Normal file
8
just_audio/android/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
48
just_audio/android/build.gradle
Normal file
48
just_audio/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
just_audio/android/gradle.properties
Normal file
4
just_audio/android/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
6
just_audio/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
just_audio/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
just_audio/android/libs/extension-flac.aar
Normal file
BIN
just_audio/android/libs/extension-flac.aar
Normal file
Binary file not shown.
1
just_audio/android/settings.gradle
Normal file
1
just_audio/android/settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'just_audio'
|
3
just_audio/android/src/main/AndroidManifest.xml
Normal file
3
just_audio/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>
|
@ -0,0 +1,723 @@
|
|||||||
|
package com.ryanheise.just_audio;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
|
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.Log;
|
||||||
|
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().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);
|
||||||
|
|
||||||
|
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,264 @@
|
|||||||
|
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 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 size = this.connection.getHeaderField("Content-Length");
|
||||||
|
return Long.parseLong(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
just_audio/darwin/Classes/AudioPlayer.m
Normal file
1138
just_audio/darwin/Classes/AudioPlayer.m
Normal file
File diff suppressed because it is too large
Load Diff
37
just_audio/darwin/Classes/AudioSource.m
Normal file
37
just_audio/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
just_audio/darwin/Classes/ClippingAudioSource.m
Normal file
79
just_audio/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
just_audio/darwin/Classes/ConcatenatingAudioSource.m
Normal file
109
just_audio/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
just_audio/darwin/Classes/IndexedAudioSource.m
Normal file
68
just_audio/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
just_audio/darwin/Classes/IndexedPlayerItem.m
Normal file
16
just_audio/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
just_audio/darwin/Classes/JustAudioPlugin.m
Normal file
55
just_audio/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
just_audio/darwin/Classes/LoopingAudioSource.m
Normal file
53
just_audio/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
just_audio/darwin/Classes/UriAudioSource.m
Normal file
79
just_audio/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
just_audio/ios/.gitignore
vendored
Normal file
37
just_audio/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
just_audio/ios/Assets/.gitkeep
Normal file
0
just_audio/ios/Assets/.gitkeep
Normal file
21
just_audio/ios/Classes/AudioPlayer.h
Normal file
21
just_audio/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
just_audio/ios/Classes/AudioPlayer.m
Normal file
1138
just_audio/ios/Classes/AudioPlayer.m
Normal file
File diff suppressed because it is too large
Load Diff
13
just_audio/ios/Classes/AudioSource.h
Normal file
13
just_audio/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
just_audio/ios/Classes/AudioSource.m
Normal file
37
just_audio/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
just_audio/ios/Classes/ClippingAudioSource.h
Normal file
11
just_audio/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
just_audio/ios/Classes/ClippingAudioSource.m
Normal file
79
just_audio/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
just_audio/ios/Classes/ConcatenatingAudioSource.h
Normal file
13
just_audio/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
just_audio/ios/Classes/ConcatenatingAudioSource.m
Normal file
109
just_audio/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
just_audio/ios/Classes/IndexedAudioSource.h
Normal file
21
just_audio/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
just_audio/ios/Classes/IndexedAudioSource.m
Normal file
68
just_audio/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
just_audio/ios/Classes/IndexedPlayerItem.h
Normal file
9
just_audio/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
just_audio/ios/Classes/IndexedPlayerItem.m
Normal file
16
just_audio/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
just_audio/ios/Classes/JustAudioPlugin.h
Normal file
4
just_audio/ios/Classes/JustAudioPlugin.h
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#import <Flutter/Flutter.h>
|
||||||
|
|
||||||
|
@interface JustAudioPlugin : NSObject<FlutterPlugin>
|
||||||
|
@end
|
55
just_audio/ios/Classes/JustAudioPlugin.m
Normal file
55
just_audio/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
just_audio/ios/Classes/LoopingAudioSource.h
Normal file
8
just_audio/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
just_audio/ios/Classes/LoopingAudioSource.m
Normal file
53
just_audio/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
just_audio/ios/Classes/UriAudioSource.h
Normal file
8
just_audio/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
just_audio/ios/Classes/UriAudioSource.m
Normal file
79
just_audio/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
just_audio/ios/just_audio.podspec
Normal file
21
just_audio/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
|
||||||
|
|
1293
just_audio/lib/just_audio.dart
Normal file
1293
just_audio/lib/just_audio.dart
Normal file
File diff suppressed because it is too large
Load Diff
957
just_audio/lib/just_audio_web.dart
Normal file
957
just_audio/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
just_audio/macos/.gitignore
vendored
Normal file
37
just_audio/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
just_audio/macos/Assets/.gitkeep
Normal file
0
just_audio/macos/Assets/.gitkeep
Normal file
21
just_audio/macos/Classes/AudioPlayer.h
Normal file
21
just_audio/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
just_audio/macos/Classes/AudioPlayer.m
Normal file
1138
just_audio/macos/Classes/AudioPlayer.m
Normal file
File diff suppressed because it is too large
Load Diff
13
just_audio/macos/Classes/AudioSource.h
Normal file
13
just_audio/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
just_audio/macos/Classes/AudioSource.m
Normal file
37
just_audio/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
just_audio/macos/Classes/ClippingAudioSource.h
Normal file
11
just_audio/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
just_audio/macos/Classes/ClippingAudioSource.m
Normal file
79
just_audio/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
just_audio/macos/Classes/ConcatenatingAudioSource.h
Normal file
13
just_audio/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
just_audio/macos/Classes/ConcatenatingAudioSource.m
Normal file
109
just_audio/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
just_audio/macos/Classes/IndexedAudioSource.h
Normal file
21
just_audio/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
just_audio/macos/Classes/IndexedAudioSource.m
Normal file
68
just_audio/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
just_audio/macos/Classes/IndexedPlayerItem.h
Normal file
9
just_audio/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
just_audio/macos/Classes/IndexedPlayerItem.m
Normal file
16
just_audio/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
just_audio/macos/Classes/JustAudioPlugin.h
Normal file
4
just_audio/macos/Classes/JustAudioPlugin.h
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#import <FlutterMacOS/FlutterMacOS.h>
|
||||||
|
|
||||||
|
@interface JustAudioPlugin : NSObject<FlutterPlugin>
|
||||||
|
@end
|
55
just_audio/macos/Classes/JustAudioPlugin.m
Normal file
55
just_audio/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
just_audio/macos/Classes/LoopingAudioSource.h
Normal file
8
just_audio/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
just_audio/macos/Classes/LoopingAudioSource.m
Normal file
53
just_audio/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
just_audio/macos/Classes/UriAudioSource.h
Normal file
8
just_audio/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
just_audio/macos/Classes/UriAudioSource.m
Normal file
79
just_audio/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
just_audio/macos/just_audio.podspec
Normal file
21
just_audio/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
just_audio/pubspec.lock
Normal file
250
just_audio/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
just_audio/pubspec.yaml
Normal file
37
just_audio/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
just_audio/test/just_audio_test.dart
Normal file
21
just_audio/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');
|
||||||
|
// });
|
||||||
|
}
|
@ -361,5 +361,17 @@ class DeezerAPI {
|
|||||||
return data['results'].toString();
|
return data['results'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Get part of discography
|
||||||
|
Future<List<Album>> discographyPage(String artistId, {int start = 0, int nb = 50}) async {
|
||||||
|
Map data = await callApi('album.getDiscography', params: {
|
||||||
|
'art_id': int.parse(artistId),
|
||||||
|
'discography_mode': 'all',
|
||||||
|
'nb': nb,
|
||||||
|
'start': start,
|
||||||
|
'nb_songs': 30
|
||||||
|
});
|
||||||
|
|
||||||
|
return data['results']['data'].map<Album>((a) => Album.fromPrivateJson(a)).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ class Track {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
extras: {
|
extras: {
|
||||||
"playbackDetails": jsonEncode(this.playbackDetails),
|
"playbackDetails": jsonEncode(this.playbackDetails),
|
||||||
|
"thumb": this.albumArt.thumb,
|
||||||
"lyrics": jsonEncode(this.lyrics.toJson()),
|
"lyrics": jsonEncode(this.lyrics.toJson()),
|
||||||
"albumId": this.album.id,
|
"albumId": this.album.id,
|
||||||
"artists": jsonEncode(this.artists.map<Map>((art) => art.toJson()).toList())
|
"artists": jsonEncode(this.artists.map<Map>((art) => art.toJson()).toList())
|
||||||
@ -102,7 +103,10 @@ class Track {
|
|||||||
artists: artists,
|
artists: artists,
|
||||||
album: album,
|
album: album,
|
||||||
id: mi.id,
|
id: mi.id,
|
||||||
albumArt: ImageDetails(fullUrl: mi.artUri),
|
albumArt: ImageDetails(
|
||||||
|
fullUrl: mi.artUri,
|
||||||
|
thumbUrl: mi.extras['thumb']
|
||||||
|
),
|
||||||
duration: mi.duration,
|
duration: mi.duration,
|
||||||
playbackDetails: null, // So it gets updated from api
|
playbackDetails: null, // So it gets updated from api
|
||||||
lyrics: Lyrics.fromJson(jsonDecode(((mi.extras??{})['lyrics'])??"{}"))
|
lyrics: Lyrics.fromJson(jsonDecode(((mi.extras??{})['lyrics'])??"{}"))
|
||||||
@ -116,7 +120,7 @@ class Track {
|
|||||||
title = "${json['SNG_TITLE']} ${json['VERSION']}";
|
title = "${json['SNG_TITLE']} ${json['VERSION']}";
|
||||||
}
|
}
|
||||||
return Track(
|
return Track(
|
||||||
id: json['SNG_ID'],
|
id: json['SNG_ID'].toString(),
|
||||||
title: title,
|
title: title,
|
||||||
duration: Duration(seconds: int.parse(json['DURATION'])),
|
duration: Duration(seconds: int.parse(json['DURATION'])),
|
||||||
albumArt: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
albumArt: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
||||||
@ -180,7 +184,7 @@ class Album {
|
|||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
factory Album.fromPrivateJson(Map<dynamic, dynamic> json, {Map<dynamic, dynamic> songsJson = const {}, bool library = false}) => Album(
|
factory Album.fromPrivateJson(Map<dynamic, dynamic> json, {Map<dynamic, dynamic> songsJson = const {}, bool library = false}) => Album(
|
||||||
id: json['ALB_ID'],
|
id: json['ALB_ID'].toString(),
|
||||||
title: json['ALB_TITLE'],
|
title: json['ALB_TITLE'],
|
||||||
art: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
art: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
||||||
artists: (json['ARTISTS']??[json]).map<Artist>((dynamic art) => Artist.fromPrivateJson(art)).toList(),
|
artists: (json['ARTISTS']??[json]).map<Artist>((dynamic art) => Artist.fromPrivateJson(art)).toList(),
|
||||||
@ -240,7 +244,7 @@ class Artist {
|
|||||||
Map<dynamic, dynamic> topJson = const {},
|
Map<dynamic, dynamic> topJson = const {},
|
||||||
bool library = false
|
bool library = false
|
||||||
}) => Artist(
|
}) => Artist(
|
||||||
id: json['ART_ID'],
|
id: json['ART_ID'].toString(),
|
||||||
name: json['ART_NAME'],
|
name: json['ART_NAME'],
|
||||||
fans: json['NB_FAN'],
|
fans: json['NB_FAN'],
|
||||||
picture: ImageDetails.fromPrivateString(json['ART_PICTURE'], type: 'artist'),
|
picture: ImageDetails.fromPrivateString(json['ART_PICTURE'], type: 'artist'),
|
||||||
@ -299,7 +303,7 @@ class Playlist {
|
|||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
factory Playlist.fromPrivateJson(Map<dynamic, dynamic> json, {Map<dynamic, dynamic> songsJson = const {}, bool library = false}) => Playlist(
|
factory Playlist.fromPrivateJson(Map<dynamic, dynamic> json, {Map<dynamic, dynamic> songsJson = const {}, bool library = false}) => Playlist(
|
||||||
id: json['PLAYLIST_ID'],
|
id: json['PLAYLIST_ID'].toString(),
|
||||||
title: json['TITLE'],
|
title: json['TITLE'],
|
||||||
trackCount: json['NB_SONG']??songsJson['total'],
|
trackCount: json['NB_SONG']??songsJson['total'],
|
||||||
image: ImageDetails.fromPrivateString(json['PLAYLIST_PICTURE'], type: 'playlist'),
|
image: ImageDetails.fromPrivateString(json['PLAYLIST_PICTURE'], type: 'playlist'),
|
||||||
|
@ -328,7 +328,8 @@ class DownloadManager {
|
|||||||
List<Map> duplicate = await db.rawQuery('SELECT * FROM downloads WHERE trackId == ?', [track.id]);
|
List<Map> duplicate = await db.rawQuery('SELECT * FROM downloads WHERE trackId == ?', [track.id]);
|
||||||
if (duplicate.length != 0) return;
|
if (duplicate.length != 0) return;
|
||||||
//Save art
|
//Save art
|
||||||
await imagesDatabase.getImage(track.albumArt.full, permanent: true);
|
//await imagesDatabase.getImage(track.albumArt.full);
|
||||||
|
imagesDatabase.saveImage(track.albumArt.full);
|
||||||
//Save to db
|
//Save to db
|
||||||
b.insert('tracks', track.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
|
b.insert('tracks', track.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
b.insert('albums', track.album.toSQL(), conflictAlgorithm: ConflictAlgorithm.ignore);
|
b.insert('albums', track.album.toSQL(), conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/ui/cached_image.dart';
|
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:connectivity/connectivity.dart';
|
import 'package:connectivity/connectivity.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@ -23,7 +20,8 @@ class PlayerHelper {
|
|||||||
StreamSubscription _customEventSubscription;
|
StreamSubscription _customEventSubscription;
|
||||||
StreamSubscription _playbackStateStreamSubscription;
|
StreamSubscription _playbackStateStreamSubscription;
|
||||||
QueueSource queueSource;
|
QueueSource queueSource;
|
||||||
RepeatType repeatType = RepeatType.NONE;
|
LoopMode repeatType = LoopMode.off;
|
||||||
|
bool shuffle = false;
|
||||||
|
|
||||||
//Find queue index by id
|
//Find queue index by id
|
||||||
int get queueIndex => AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1');
|
int get queueIndex => AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1');
|
||||||
@ -45,7 +43,6 @@ class PlayerHelper {
|
|||||||
if (event['action'] == 'queueEnd') {
|
if (event['action'] == 'queueEnd') {
|
||||||
//If last song is played, load more queue
|
//If last song is played, load more queue
|
||||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||||
print(queueSource.toJson());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -74,20 +71,24 @@ class PlayerHelper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future toggleShuffle() async {
|
||||||
|
this.shuffle = !this.shuffle;
|
||||||
|
await AudioService.customAction('shuffle', this.shuffle);
|
||||||
|
}
|
||||||
|
|
||||||
//Repeat toggle
|
//Repeat toggle
|
||||||
Future changeRepeat() async {
|
Future changeRepeat() async {
|
||||||
//Change to next repeat type
|
//Change to next repeat type
|
||||||
switch (repeatType) {
|
switch (repeatType) {
|
||||||
case RepeatType.NONE:
|
case LoopMode.one:
|
||||||
repeatType = RepeatType.LIST; break;
|
repeatType = LoopMode.off; break;
|
||||||
case RepeatType.LIST:
|
case LoopMode.all:
|
||||||
repeatType = RepeatType.TRACK; break;
|
repeatType = LoopMode.one; break;
|
||||||
default:
|
default:
|
||||||
repeatType = RepeatType.NONE; break;
|
repeatType = LoopMode.all; break;
|
||||||
}
|
}
|
||||||
//Set repeat type
|
//Set repeat type
|
||||||
await AudioService.customAction("repeatType", RepeatType.values.indexOf(repeatType));
|
await AudioService.customAction("repeatType", LoopMode.values.indexOf(repeatType));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Executed before exit
|
//Executed before exit
|
||||||
@ -101,7 +102,7 @@ class PlayerHelper {
|
|||||||
await startService();
|
await startService();
|
||||||
await settings.updateAudioServiceQuality();
|
await settings.updateAudioServiceQuality();
|
||||||
await AudioService.updateQueue(queue);
|
await AudioService.updateQueue(queue);
|
||||||
await AudioService.playFromMediaId(trackId);
|
await AudioService.skipToQueueItem(trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Play track from album
|
//Play track from album
|
||||||
@ -178,277 +179,229 @@ void backgroundTaskEntrypoint() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AudioPlayerTask extends BackgroundAudioTask {
|
class AudioPlayerTask extends BackgroundAudioTask {
|
||||||
|
AudioPlayer _player = AudioPlayer();
|
||||||
|
|
||||||
AudioPlayer _audioPlayer = AudioPlayer();
|
//Queue
|
||||||
|
|
||||||
List<MediaItem> _queue = <MediaItem>[];
|
List<MediaItem> _queue = <MediaItem>[];
|
||||||
int _queueIndex = -1;
|
int _queueIndex = 0;
|
||||||
|
ConcatenatingAudioSource _audioSource;
|
||||||
|
|
||||||
bool _playing;
|
|
||||||
bool _interrupted;
|
|
||||||
AudioProcessingState _skipState;
|
AudioProcessingState _skipState;
|
||||||
Duration _lastPosition;
|
bool _interrupted;
|
||||||
|
Seeker _seeker;
|
||||||
|
|
||||||
ImagesDatabase imagesDB;
|
//Stream subscriptions
|
||||||
|
StreamSubscription _eventSub;
|
||||||
|
|
||||||
|
//Loaded from file/frontend
|
||||||
int mobileQuality;
|
int mobileQuality;
|
||||||
int wifiQuality;
|
int wifiQuality;
|
||||||
|
|
||||||
StreamSubscription _eventSub;
|
|
||||||
StreamSubscription _playerStateSub;
|
|
||||||
|
|
||||||
QueueSource queueSource;
|
QueueSource queueSource;
|
||||||
int repeatType = 0;
|
Duration _lastPosition;
|
||||||
|
|
||||||
MediaItem get mediaItem => _queue[_queueIndex];
|
MediaItem get mediaItem => _queue[_queueIndex];
|
||||||
|
|
||||||
//Controls
|
|
||||||
final playControl = MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_play_arrow',
|
|
||||||
label: 'Play',
|
|
||||||
action: MediaAction.play
|
|
||||||
);
|
|
||||||
final pauseControl = MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_pause',
|
|
||||||
label: 'Pause',
|
|
||||||
action: MediaAction.pause
|
|
||||||
);
|
|
||||||
final stopControl = MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_stop',
|
|
||||||
label: 'Stop',
|
|
||||||
action: MediaAction.stop
|
|
||||||
);
|
|
||||||
final nextControl = MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_skip_next',
|
|
||||||
label: 'Next',
|
|
||||||
action: MediaAction.skipToNext
|
|
||||||
);
|
|
||||||
final previousControl = MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_skip_previous',
|
|
||||||
label: 'Previous',
|
|
||||||
action: MediaAction.skipToPrevious
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onStart(Map<String, dynamic> params) async {
|
Future onStart(Map<String, dynamic> params) {
|
||||||
_playerStateSub = _audioPlayer.playbackStateStream
|
|
||||||
.where((state) => state == AudioPlaybackState.completed)
|
//Update track index
|
||||||
.listen((_event) {
|
_player.currentIndexStream.listen((index) {
|
||||||
if (_queue.length > _queueIndex + 1) {
|
if (index != null) {
|
||||||
onSkipToNext();
|
_queueIndex = index;
|
||||||
return;
|
AudioServiceBackground.setMediaItem(mediaItem);
|
||||||
} else {
|
|
||||||
//Repeat whole list (if enabled)
|
|
||||||
if (repeatType == 1) {
|
|
||||||
_skip(-_queueIndex);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
//Ask for more tracks in queue
|
});
|
||||||
|
//Update state on all clients on change
|
||||||
|
_eventSub = _player.playbackEventStream.listen((event) {
|
||||||
|
_broadcastState();
|
||||||
|
});
|
||||||
|
_player.processingStateStream.listen((state) {
|
||||||
|
switch(state) {
|
||||||
|
case ProcessingState.completed:
|
||||||
|
//Player ended, get more songs
|
||||||
AudioServiceBackground.sendCustomEvent({
|
AudioServiceBackground.sendCustomEvent({
|
||||||
'action': 'queueEnd',
|
'action': 'queueEnd',
|
||||||
'queueSource': (queueSource??QueueSource()).toJson()
|
'queueSource': (queueSource??QueueSource()).toJson()
|
||||||
});
|
});
|
||||||
if (_playing) _playing = false;
|
|
||||||
_setState(AudioProcessingState.none);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Read audio player events
|
|
||||||
_eventSub = _audioPlayer.playbackEventStream.listen((event) {
|
|
||||||
AudioProcessingState bufferingState = event.buffering ? AudioProcessingState.buffering : null;
|
|
||||||
switch (event.state) {
|
|
||||||
case AudioPlaybackState.paused:
|
|
||||||
case AudioPlaybackState.playing:
|
|
||||||
_setState(bufferingState ?? AudioProcessingState.ready, pos: event.position);
|
|
||||||
break;
|
break;
|
||||||
case AudioPlaybackState.connecting:
|
case ProcessingState.ready:
|
||||||
_setState(_skipState ?? AudioProcessingState.connecting, pos: event.position);
|
//Ready to play
|
||||||
|
_skipState = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Initialize later
|
//Load queue
|
||||||
//await imagesDB.init();
|
|
||||||
|
|
||||||
AudioServiceBackground.setQueue(_queue);
|
AudioServiceBackground.setQueue(_queue);
|
||||||
AudioServiceBackground.sendCustomEvent({'action': 'onLoad'});
|
AudioServiceBackground.sendCustomEvent({'action': 'onLoad'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onSkipToNext() async {
|
Future onSkipToQueueItem(String mediaId) async {
|
||||||
//If repeating allowed
|
|
||||||
if (repeatType == 2) {
|
|
||||||
await _skip(0);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
_skip(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future onSkipToPrevious() => _skip(-1);
|
|
||||||
|
|
||||||
Future _skip(int offset) async {
|
|
||||||
int newPos = _queueIndex + offset;
|
|
||||||
//Out of bounds
|
|
||||||
if (newPos >= _queue.length || newPos < 0) return;
|
|
||||||
//First song
|
|
||||||
if (_playing == null) {
|
|
||||||
_playing = true;
|
|
||||||
} else if (_playing) {
|
|
||||||
await _audioPlayer.stop();
|
|
||||||
}
|
|
||||||
//Update position, album art source, queue source text
|
|
||||||
_queueIndex = newPos;
|
|
||||||
//Get uri
|
|
||||||
String uri = await _getTrackUri(mediaItem);
|
|
||||||
//Modify extras
|
|
||||||
Map<String, dynamic> extras = mediaItem.extras;
|
|
||||||
extras.addAll({"qualityString": await _getQualityString(uri, mediaItem.duration)});
|
|
||||||
_queue[_queueIndex] = mediaItem.copyWith(
|
|
||||||
artUri: await _getArtUri(mediaItem.artUri),
|
|
||||||
extras: extras
|
|
||||||
);
|
|
||||||
//Play
|
|
||||||
AudioServiceBackground.setMediaItem(mediaItem);
|
|
||||||
_skipState = offset > 0 ? AudioProcessingState.skippingToNext:AudioProcessingState.skippingToPrevious;
|
|
||||||
//Load
|
|
||||||
await _audioPlayer.setUrl(uri);
|
|
||||||
_skipState = null;
|
|
||||||
await _saveQueue();
|
|
||||||
(_playing??false) ? onPlay() : _setState(AudioProcessingState.ready);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onPlay() async {
|
|
||||||
//Start playing preloaded queue
|
|
||||||
if (AudioServiceBackground.state.processingState == AudioProcessingState.none && _queue.length > 0) {
|
|
||||||
if (_queueIndex < 0 || _queueIndex == null) {
|
|
||||||
await this._skip(1);
|
|
||||||
} else {
|
|
||||||
await this._skip(0);
|
|
||||||
}
|
|
||||||
//Restore position from saved queue
|
|
||||||
if (_lastPosition != null) {
|
|
||||||
onSeekTo(_lastPosition);
|
|
||||||
_lastPosition = null;
|
_lastPosition = null;
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_skipState == null) {
|
|
||||||
_playing = true;
|
|
||||||
_audioPlayer.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
//Calculate new index
|
||||||
void onPause() {
|
final newIndex = _queue.indexWhere((i) => i.id == mediaId);
|
||||||
if (_skipState == null && _playing) {
|
if (newIndex == -1) return;
|
||||||
_playing = false;
|
|
||||||
_audioPlayer.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
//Update buffering state
|
||||||
void onSeekTo(Duration pos) {
|
_skipState = newIndex > _queueIndex
|
||||||
_audioPlayer.seek(pos);
|
? AudioProcessingState.skippingToNext
|
||||||
}
|
: AudioProcessingState.skippingToPrevious;
|
||||||
|
|
||||||
@override
|
//Skip in player
|
||||||
void onClick(MediaButton button) {
|
await _player.seek(Duration.zero, index: newIndex);
|
||||||
if (_playing) onPause();
|
_skipState = null;
|
||||||
onPlay();
|
onPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onUpdateQueue(List<MediaItem> q) async {
|
Future onPlay() {
|
||||||
this._queue = q;
|
_player.play();
|
||||||
AudioServiceBackground.setQueue(_queue);
|
//Restore position on play
|
||||||
await _saveQueue();
|
if (_lastPosition != null) {
|
||||||
|
onSeekTo(_lastPosition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onPlayFromMediaId(String mediaId) async {
|
Future onPause() => _player.pause();
|
||||||
int pos = this._queue.indexWhere((mi) => mi.id == mediaId);
|
|
||||||
await _skip(pos - _queueIndex);
|
|
||||||
if (_playing == null || !_playing) onPlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onFastForward() async {
|
Future onSeekTo(Duration pos) => _player.seek(pos);
|
||||||
await _seekRelative(fastForwardInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onAddQueueItemAt(MediaItem mi, int index) {
|
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
|
||||||
_queue.insert(index, mi);
|
|
||||||
AudioServiceBackground.setQueue(_queue);
|
|
||||||
_saveQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onAddQueueItem(MediaItem mi) {
|
Future<void> onRewind() => _seekRelative(-rewindInterval);
|
||||||
_queue.add(mi);
|
|
||||||
AudioServiceBackground.setQueue(_queue);
|
|
||||||
_saveQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future onRewind() async {
|
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
|
||||||
await _seekRelative(rewindInterval);
|
|
||||||
|
@override
|
||||||
|
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
|
||||||
|
|
||||||
|
//While seeking, jump 10s every 1s
|
||||||
|
void _seekContinuously(bool begin, int direction) {
|
||||||
|
_seeker?.stop();
|
||||||
|
if (begin) {
|
||||||
|
_seeker = Seeker(_player, Duration(seconds: 10 * direction), Duration(seconds: 1), mediaItem)..start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Relative seek
|
||||||
Future _seekRelative(Duration offset) async {
|
Future _seekRelative(Duration offset) async {
|
||||||
Duration newPos = _audioPlayer.playbackEvent.position + offset;
|
Duration newPos = _player.position + offset;
|
||||||
|
//Out of bounds check
|
||||||
if (newPos < Duration.zero) newPos = Duration.zero;
|
if (newPos < Duration.zero) newPos = Duration.zero;
|
||||||
if (newPos > mediaItem.duration) newPos = mediaItem.duration;
|
if (newPos > mediaItem.duration) newPos = mediaItem.duration;
|
||||||
onSeekTo(_audioPlayer.playbackEvent.position + offset);
|
|
||||||
|
await _player.seek(newPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Update state on all clients
|
||||||
|
Future _broadcastState() async {
|
||||||
|
await AudioServiceBackground.setState(
|
||||||
|
controls: [
|
||||||
|
MediaControl.skipToPrevious,
|
||||||
|
if (_player.playing) MediaControl.pause else MediaControl.play,
|
||||||
|
MediaControl.skipToNext,
|
||||||
|
//MediaControl.stop
|
||||||
|
],
|
||||||
|
systemActions: [
|
||||||
|
MediaAction.seekTo,
|
||||||
|
MediaAction.seekForward,
|
||||||
|
MediaAction.seekBackward
|
||||||
|
],
|
||||||
|
processingState: _getProcessingState(),
|
||||||
|
playing: _player.playing,
|
||||||
|
position: _player.position,
|
||||||
|
bufferedPosition: _player.bufferedPosition,
|
||||||
|
speed: _player.speed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//just_audio state -> audio_service state. If skipping, use _skipState
|
||||||
|
AudioProcessingState _getProcessingState() {
|
||||||
|
if (_skipState != null) return _skipState;
|
||||||
|
//SRC: audio_service example
|
||||||
|
switch (_player.processingState) {
|
||||||
|
case ProcessingState.none:
|
||||||
|
return AudioProcessingState.stopped;
|
||||||
|
case ProcessingState.loading:
|
||||||
|
return AudioProcessingState.connecting;
|
||||||
|
case ProcessingState.buffering:
|
||||||
|
return AudioProcessingState.buffering;
|
||||||
|
case ProcessingState.ready:
|
||||||
|
return AudioProcessingState.ready;
|
||||||
|
case ProcessingState.completed:
|
||||||
|
return AudioProcessingState.completed;
|
||||||
|
default:
|
||||||
|
throw Exception("Invalid state: ${_player.processingState}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Replace current queue
|
||||||
@override
|
@override
|
||||||
Future onUpdateMediaItem(MediaItem mediaItem) async {
|
Future onUpdateQueue(List<MediaItem> q) async {
|
||||||
_queue[_queueIndex] = mediaItem;
|
//just_audio
|
||||||
|
_player.stop();
|
||||||
|
if (_audioSource != null) _audioSource.clear();
|
||||||
|
//audio_service
|
||||||
|
this._queue = q;
|
||||||
|
AudioServiceBackground.setQueue(_queue);
|
||||||
|
//Load
|
||||||
|
await _loadQueue();
|
||||||
|
await _player.seek(Duration.zero, index: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load queue to just_audio
|
||||||
|
Future _loadQueue() async {
|
||||||
|
List<AudioSource> sources = [];
|
||||||
|
for(int i=0; i<_queue.length; i++) {
|
||||||
|
sources.add(await _mediaItemToAudioSource(_queue[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
_audioSource = ConcatenatingAudioSource(children: sources);
|
||||||
|
//Load in just_audio
|
||||||
|
try {
|
||||||
|
await _player.load(_audioSource);
|
||||||
|
} catch (e) {
|
||||||
|
//Error loading tracks
|
||||||
|
}
|
||||||
AudioServiceBackground.setMediaItem(mediaItem);
|
AudioServiceBackground.setMediaItem(mediaItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Audio interruptions
|
Future<AudioSource> _mediaItemToAudioSource(MediaItem mi) async {
|
||||||
@override
|
String url = await _getTrackUrl(mi);
|
||||||
void onAudioFocusLost(AudioInterruption interruption) {
|
if (url.startsWith('http')) return ProgressiveAudioSource(Uri.parse(url));
|
||||||
if (_playing) _interrupted = true;
|
return AudioSource.uri(Uri.parse(url));
|
||||||
switch (interruption) {
|
|
||||||
case AudioInterruption.pause:
|
|
||||||
case AudioInterruption.temporaryPause:
|
|
||||||
case AudioInterruption.unknownPause:
|
|
||||||
if (_playing) onPause();
|
|
||||||
break;
|
|
||||||
case AudioInterruption.temporaryDuck:
|
|
||||||
_audioPlayer.setVolume(0.5);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future _getTrackUrl(MediaItem mediaItem, {int quality}) async {
|
||||||
void onAudioFocusGained(AudioInterruption interruption) {
|
//Check if offline
|
||||||
switch (interruption) {
|
String _offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||||
case AudioInterruption.temporaryPause:
|
File f = File(p.join(_offlinePath, mediaItem.id));
|
||||||
if (!_playing && _interrupted) onPlay();
|
if (await f.exists()) {
|
||||||
break;
|
return f.path;
|
||||||
case AudioInterruption.temporaryDuck:
|
|
||||||
_audioPlayer.setVolume(1.0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_interrupted = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
|
||||||
void onAudioBecomingNoisy() {
|
//This just returns fake url that contains metadata
|
||||||
onPause();
|
List playbackDetails = jsonDecode(mediaItem.extras['playbackDetails']);
|
||||||
|
//Quality
|
||||||
|
ConnectivityResult conn = await Connectivity().checkConnectivity();
|
||||||
|
quality = mobileQuality;
|
||||||
|
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
|
||||||
|
|
||||||
|
String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Custom actions
|
||||||
@override
|
@override
|
||||||
Future onCustomAction(String name, dynamic args) async {
|
Future onCustomAction(String name, dynamic args) async {
|
||||||
if (name == 'updateQuality') {
|
if (name == 'updateQuality') {
|
||||||
@ -457,228 +410,178 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
this.wifiQuality = args['wifiQuality'];
|
this.wifiQuality = args['wifiQuality'];
|
||||||
this.mobileQuality = args['mobileQuality'];
|
this.mobileQuality = args['mobileQuality'];
|
||||||
}
|
}
|
||||||
if (name == 'saveQueue') {
|
|
||||||
await this._saveQueue();
|
|
||||||
}
|
|
||||||
//Load queue, called after start
|
|
||||||
if (name == 'load') {
|
|
||||||
await _loadQueue();
|
|
||||||
}
|
|
||||||
//Change queue source
|
//Change queue source
|
||||||
if (name == 'queueSource') {
|
if (name == 'queueSource') {
|
||||||
this.queueSource = QueueSource.fromJson(Map<String, dynamic>.from(args));
|
this.queueSource = QueueSource.fromJson(Map<String, dynamic>.from(args));
|
||||||
}
|
}
|
||||||
//Shuffle
|
//Looping
|
||||||
if (name == 'shuffleQueue') {
|
|
||||||
MediaItem mi = mediaItem;
|
|
||||||
shuffle(this._queue);
|
|
||||||
_queueIndex = _queue.indexOf(mi);
|
|
||||||
AudioServiceBackground.setQueue(this._queue);
|
|
||||||
}
|
|
||||||
//Repeating
|
|
||||||
if (name == 'repeatType') {
|
if (name == 'repeatType') {
|
||||||
this.repeatType = args;
|
_player.setLoopMode(LoopMode.values[args]);
|
||||||
}
|
}
|
||||||
|
if (name == 'saveQueue') await this._saveQueue();
|
||||||
|
//Load queue after some initialization in frontend
|
||||||
|
if (name == 'load') await this._loadQueueFile();
|
||||||
|
//Shuffle
|
||||||
|
if (name == 'shuffle') await _player.setShuffleModeEnabled(args);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getArtUri(String url) async {
|
//Audio interruptions
|
||||||
//Load from cache
|
@override
|
||||||
if (url.startsWith('http')) {
|
Future onAudioFocusLost(AudioInterruption interruption) {
|
||||||
//Prepare db
|
if (_player.playing) _interrupted = true;
|
||||||
if (imagesDB == null) {
|
switch (interruption) {
|
||||||
imagesDB = ImagesDatabase();
|
case AudioInterruption.pause:
|
||||||
await imagesDB.init();
|
case AudioInterruption.temporaryPause:
|
||||||
|
case AudioInterruption.unknownPause:
|
||||||
|
if (_player.playing) onPause();
|
||||||
|
break;
|
||||||
|
case AudioInterruption.temporaryDuck:
|
||||||
|
_player.setVolume(0.5);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = await imagesDB.getImage(url);
|
|
||||||
return 'file://$path';
|
|
||||||
}
|
|
||||||
//If file
|
|
||||||
if (url.startsWith('/')) return 'file://' + url;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _getTrackUri(MediaItem mi, {int quality}) async {
|
|
||||||
String prefix = 'DEEZER|${mi.id}|';
|
|
||||||
|
|
||||||
//Check if song is available offline
|
|
||||||
String _offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
|
||||||
File f = File(p.join(_offlinePath, mi.id));
|
|
||||||
if (await f.exists()) return f.path;
|
|
||||||
|
|
||||||
//Get online url
|
|
||||||
Track t = Track(
|
|
||||||
id: mi.id,
|
|
||||||
playbackDetails: jsonDecode(mi.extras['playbackDetails']) //JSON Because of audio_service bug
|
|
||||||
);
|
|
||||||
|
|
||||||
//Check connection
|
|
||||||
if (quality == null) {
|
|
||||||
ConnectivityResult conn = await Connectivity().checkConnectivity();
|
|
||||||
quality = mobileQuality;
|
|
||||||
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
|
|
||||||
}
|
|
||||||
String url = t.getUrl(quality);
|
|
||||||
|
|
||||||
//Quality fallback
|
|
||||||
Dio dio = Dio();
|
|
||||||
try {
|
|
||||||
await dio.head(url);
|
|
||||||
return prefix + url;
|
|
||||||
} catch (e) {
|
|
||||||
if (quality == 9) return _getTrackUri(mi, quality: 3);
|
|
||||||
if (quality == 3) return _getTrackUri(mi, quality: 1);
|
|
||||||
throw Exception('No available quality!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<String> _getQualityString(String uri, Duration duration) async {
|
|
||||||
//Get url/path
|
|
||||||
String url = uri;
|
|
||||||
List<String> split = uri.split('|');
|
|
||||||
if (split.length >= 3) url = split[2];
|
|
||||||
|
|
||||||
int size;
|
|
||||||
String format;
|
|
||||||
String source;
|
|
||||||
|
|
||||||
//Local file
|
|
||||||
if (url.startsWith('/')) {
|
|
||||||
//Read first 4 bytes of file, get format
|
|
||||||
File f = File(url);
|
|
||||||
Stream<List<int>> reader = f.openRead(0, 4);
|
|
||||||
List<int> magic = await reader.first;
|
|
||||||
format = _magicToFormat(magic);
|
|
||||||
size = await f.length();
|
|
||||||
source = 'Offline';
|
|
||||||
}
|
|
||||||
|
|
||||||
//URL
|
|
||||||
if (url.startsWith('http')) {
|
|
||||||
Dio dio = Dio();
|
|
||||||
Response response = await dio.head(url);
|
|
||||||
size = int.parse(response.headers['Content-Length'][0]);
|
|
||||||
//Parse format
|
|
||||||
format = response.headers['Content-Type'][0];
|
|
||||||
if (format.trim() == 'audio/mpeg') format = 'MP3';
|
|
||||||
if (format.trim() == 'audio/flac') format = 'FLAC';
|
|
||||||
source = 'Stream';
|
|
||||||
}
|
|
||||||
//Calculate
|
|
||||||
return '$format ${_bitrateString(size, duration.inSeconds)} ($source)';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _bitrateString(int size, int duration) {
|
|
||||||
int bitrate = ((size / 125) / duration).floor();
|
|
||||||
//Prettify
|
|
||||||
if (bitrate > 315 && bitrate < 325) return '320kbps';
|
|
||||||
if (bitrate > 125 && bitrate < 135) return '128kbps';
|
|
||||||
return '${bitrate}kbps';
|
|
||||||
}
|
|
||||||
|
|
||||||
//Magic number to string, source: https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
||||||
String _magicToFormat(List<int> magic) {
|
|
||||||
Function eq = const ListEquality().equals;
|
|
||||||
if (eq(magic.sublist(0, 4), [0x66, 0x4c, 0x61, 0x43])) return 'FLAC';
|
|
||||||
//MP3 With ID3
|
|
||||||
if (eq(magic.sublist(0, 3), [0x49, 0x44, 0x33])) return 'MP3';
|
|
||||||
//MP3
|
|
||||||
List<int> m = magic.sublist(0, 2);
|
|
||||||
if (eq(m, [0xff, 0xfb]) ||eq(m, [0xff, 0xf3]) || eq(m, [0xff, 0xf2])) return 'MP3';
|
|
||||||
//Unknown
|
|
||||||
return 'UNK';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onTaskRemoved() async {
|
Future onAudioFocusGained(AudioInterruption interruption) {
|
||||||
|
switch (interruption) {
|
||||||
|
case AudioInterruption.temporaryPause:
|
||||||
|
if (!_player.playing && _interrupted) onPlay();
|
||||||
|
break;
|
||||||
|
case AudioInterruption.temporaryDuck:
|
||||||
|
_player.setVolume(1.0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_interrupted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future onAudioBecomingNoisy() {
|
||||||
|
onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future onTaskRemoved() async {
|
||||||
await onStop();
|
await onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Future onClose() async {
|
||||||
|
await onStop();
|
||||||
|
}
|
||||||
|
|
||||||
Future onStop() async {
|
Future onStop() async {
|
||||||
_audioPlayer.stop();
|
|
||||||
if (_playerStateSub != null) _playerStateSub.cancel();
|
|
||||||
if (_eventSub != null) _eventSub.cancel();
|
|
||||||
await _saveQueue();
|
await _saveQueue();
|
||||||
|
_player.stop();
|
||||||
|
if (_eventSub != null) _eventSub.cancel();
|
||||||
|
|
||||||
await super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
//Get queue save file path
|
||||||
void onClose() async {
|
|
||||||
//await _saveQueue();
|
|
||||||
//Gets saved in onStop()
|
|
||||||
await onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Update state
|
|
||||||
void _setState(AudioProcessingState state, {Duration pos}) {
|
|
||||||
AudioServiceBackground.setState(
|
|
||||||
controls: _getControls(),
|
|
||||||
systemActions: (_playing == null) ? [] : [MediaAction.seekTo],
|
|
||||||
processingState: state ?? AudioServiceBackground.state.processingState,
|
|
||||||
playing: _playing ?? false,
|
|
||||||
position: pos ?? _audioPlayer.playbackEvent.position,
|
|
||||||
bufferedPosition: pos ?? _audioPlayer.playbackEvent.position,
|
|
||||||
speed: _audioPlayer.speed
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MediaControl> _getControls() {
|
|
||||||
if (_playing == null || !_playing) {
|
|
||||||
//Paused / not-started
|
|
||||||
return [
|
|
||||||
previousControl,
|
|
||||||
playControl,
|
|
||||||
nextControl
|
|
||||||
];
|
|
||||||
}
|
|
||||||
//Playing
|
|
||||||
return [
|
|
||||||
previousControl,
|
|
||||||
pauseControl,
|
|
||||||
nextControl
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get queue saved file path
|
|
||||||
Future<String> _getQueuePath() async {
|
Future<String> _getQueuePath() async {
|
||||||
Directory dir = await getApplicationDocumentsDirectory();
|
Directory dir = await getApplicationDocumentsDirectory();
|
||||||
return p.join(dir.path, 'offline.json');
|
return p.join(dir.path, 'playback.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Export queue to JSON
|
//Export queue to JSON
|
||||||
Future _saveQueue() async {
|
Future _saveQueue() async {
|
||||||
print('save');
|
String path = await _getQueuePath();
|
||||||
File f = File(await _getQueuePath());
|
File f = File(path);
|
||||||
await f.writeAsString(jsonEncode({
|
//Create if doesnt exist
|
||||||
'index': _queueIndex,
|
if (! await File(path).exists()) {
|
||||||
'queue': _queue.map<Map<String, dynamic>>((mi) => mi.toJson()).toList(),
|
f = await f.create();
|
||||||
'position': _audioPlayer.playbackEvent.position.inMilliseconds,
|
|
||||||
'queueSource': (queueSource??QueueSource()).toJson(),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _loadQueue() async {
|
Map data = {
|
||||||
|
'index': _queueIndex,
|
||||||
|
'queue': _queue.map<Map<String, dynamic>>((mi) => mi.toJson()).toList(),
|
||||||
|
'position': _player.position.inMilliseconds,
|
||||||
|
'queueSource': (queueSource??QueueSource()).toJson(),
|
||||||
|
};
|
||||||
|
await f.writeAsString(jsonEncode(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Restore queue & playback info from path
|
||||||
|
Future _loadQueueFile() async {
|
||||||
File f = File(await _getQueuePath());
|
File f = File(await _getQueuePath());
|
||||||
if (await f.exists()) {
|
if (await f.exists()) {
|
||||||
Map<String, dynamic> json = jsonDecode(await f.readAsString());
|
Map<String, dynamic> json = jsonDecode(await f.readAsString());
|
||||||
this._queue = (json['queue']??[]).map<MediaItem>((mi) => MediaItem.fromJson(mi)).toList();
|
this._queue = (json['queue']??[]).map<MediaItem>((mi) => MediaItem.fromJson(mi)).toList();
|
||||||
this._queueIndex = json['index'] ?? -1;
|
this._queueIndex = json['index'] ?? 0;
|
||||||
this._lastPosition = Duration(milliseconds: json['position']??0);
|
this._lastPosition = Duration(milliseconds: json['position']??0);
|
||||||
this.queueSource = QueueSource.fromJson(json['queueSource']??{});
|
this.queueSource = QueueSource.fromJson(json['queueSource']??{});
|
||||||
|
//Restore queue
|
||||||
if (_queue != null) {
|
if (_queue != null) {
|
||||||
AudioServiceBackground.setQueue(_queue);
|
await AudioServiceBackground.setQueue(_queue);
|
||||||
|
await _loadQueue();
|
||||||
AudioServiceBackground.setMediaItem(mediaItem);
|
AudioServiceBackground.setMediaItem(mediaItem);
|
||||||
//Update state to allow play button in notification
|
}
|
||||||
this._setState(AudioProcessingState.none, pos: _lastPosition);
|
|
||||||
}
|
}
|
||||||
//Send restored queue source to ui
|
//Send restored queue source to ui
|
||||||
AudioServiceBackground.sendCustomEvent({'action': 'onRestore', 'queueSource': (queueSource??QueueSource()).toJson()});
|
AudioServiceBackground.sendCustomEvent({
|
||||||
|
'action': 'onRestore',
|
||||||
|
'queueSource': (queueSource??QueueSource()).toJson()
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future onAddQueueItemAt(MediaItem mi, int index) async {
|
||||||
|
//-1 == play next
|
||||||
|
if (index == -1) index = _queueIndex + 1;
|
||||||
|
|
||||||
|
|
||||||
|
_queue.insert(index, mi);
|
||||||
|
await AudioServiceBackground.setQueue(_queue);
|
||||||
|
await _audioSource.insert(index, await _mediaItemToAudioSource(mi));
|
||||||
|
|
||||||
|
_saveQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add at end of queue
|
||||||
|
@override
|
||||||
|
Future onAddQueueItem(MediaItem mi) async {
|
||||||
|
_queue.add(mi);
|
||||||
|
await AudioServiceBackground.setQueue(_queue);
|
||||||
|
await _audioSource.add(await _mediaItemToAudioSource(mi));
|
||||||
|
_saveQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future onPlayFromMediaId(String mediaId) async {
|
||||||
|
//Does the same thing
|
||||||
|
await this.onSkipToQueueItem(mediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Seeker from audio_service example (why reinvent the wheel?)
|
||||||
|
//While holding seek button, will continuously seek
|
||||||
|
class Seeker {
|
||||||
|
final AudioPlayer player;
|
||||||
|
final Duration positionInterval;
|
||||||
|
final Duration stepInterval;
|
||||||
|
final MediaItem mediaItem;
|
||||||
|
bool _running = false;
|
||||||
|
|
||||||
|
Seeker(this.player, this.positionInterval, this.stepInterval, this.mediaItem);
|
||||||
|
|
||||||
|
Future start() async {
|
||||||
|
_running = true;
|
||||||
|
while (_running) {
|
||||||
|
Duration newPosition = player.position + positionInterval;
|
||||||
|
if (newPosition < Duration.zero) newPosition = Duration.zero;
|
||||||
|
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
|
||||||
|
player.seek(newPosition);
|
||||||
|
await Future.delayed(stepInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
_running = false;
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ void main() async {
|
|||||||
|
|
||||||
//Initialize globals
|
//Initialize globals
|
||||||
settings = await Settings().loadSettings();
|
settings = await Settings().loadSettings();
|
||||||
await imagesDatabase.init();
|
//await imagesDatabase.init();
|
||||||
await downloadManager.init();
|
await downloadManager.init();
|
||||||
|
|
||||||
runApp(FreezerApp());
|
runApp(FreezerApp());
|
||||||
@ -44,9 +44,6 @@ class _FreezerAppState extends State<FreezerApp> {
|
|||||||
//Make update theme global
|
//Make update theme global
|
||||||
updateTheme = _updateTheme;
|
updateTheme = _updateTheme;
|
||||||
|
|
||||||
//Precache placeholder
|
|
||||||
precacheImage(imagesDatabase.placeholderThumb, context);
|
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,209 +1,66 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:sqflite/sqflite.dart';
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
ImagesDatabase imagesDatabase = ImagesDatabase();
|
ImagesDatabase imagesDatabase = ImagesDatabase();
|
||||||
|
|
||||||
|
|
||||||
class ImagesDatabase {
|
class ImagesDatabase {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
images.db:
|
!!! Using the wrappers so i don't have to rewrite most of the code, because of migration to cached network image
|
||||||
Table: images
|
|
||||||
Fields:
|
|
||||||
id - id
|
|
||||||
name - md5 hash of url. also filename
|
|
||||||
url - url
|
|
||||||
permanent - 0/1 - if image is cached or offline
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
void saveImage(String url) {
|
||||||
Database db;
|
CachedNetworkImageProvider(url);
|
||||||
String imagesPath;
|
|
||||||
|
|
||||||
ImageProvider placeholderThumb = new AssetImage('assets/cover_thumb.jpg');
|
|
||||||
|
|
||||||
//Prepare database
|
|
||||||
Future init() async {
|
|
||||||
String dir = await getDatabasesPath();
|
|
||||||
String path = p.join(dir, 'images.db');
|
|
||||||
db = await openDatabase(
|
|
||||||
path,
|
|
||||||
version: 1,
|
|
||||||
singleInstance: false,
|
|
||||||
onCreate: (Database db, int version) async {
|
|
||||||
//Create table on db created
|
|
||||||
await db.execute('CREATE TABLE images (id INTEGER PRIMARY KEY, name TEXT, url TEXT, permanent INTEGER)');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
//Prepare folders
|
|
||||||
imagesPath = p.join((await getApplicationDocumentsDirectory()).path, 'images/');
|
|
||||||
Directory imagesDir = Directory(imagesPath);
|
|
||||||
await imagesDir.create(recursive: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPath(String name) {
|
Future<PaletteGenerator> getPaletteGenerator(String url) {
|
||||||
return p.join(imagesPath, name);
|
return PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get image url/path, cache it
|
|
||||||
Future<String> getImage(String url, {bool permanent = false}) async {
|
|
||||||
//Already file
|
|
||||||
if (!url.startsWith('http')) {
|
|
||||||
url = url.replaceFirst('file://', '');
|
|
||||||
if (!permanent) return url;
|
|
||||||
//Update in db to permanent
|
|
||||||
String name = p.basenameWithoutExtension(url);
|
|
||||||
await db.rawUpdate('UPDATE images SET permanent == 1 WHERE name == ?', [name]);
|
|
||||||
}
|
|
||||||
//Filename = md5 hash
|
|
||||||
String hash = md5.convert(utf8.encode(url)).toString();
|
|
||||||
List<Map> results = await db.rawQuery('SELECT * FROM images WHERE name == ?', [hash]);
|
|
||||||
String path = getPath(hash);
|
|
||||||
if (results.length > 0) {
|
|
||||||
//Image in database
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
//Save image
|
|
||||||
Dio dio = Dio();
|
|
||||||
try {
|
|
||||||
await dio.download(url, path);
|
|
||||||
await db.insert('images', {'url': url, 'name': hash, 'permanent': permanent?1:0});
|
|
||||||
return path;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<PaletteGenerator> getPaletteGenerator(String url) async {
|
|
||||||
String path = await getImage(url);
|
|
||||||
//Get image provider
|
|
||||||
ImageProvider provider = placeholderThumb;
|
|
||||||
if (path != null) {
|
|
||||||
provider = FileImage(File(path));
|
|
||||||
}
|
|
||||||
PaletteGenerator paletteGenerator = await PaletteGenerator.fromImageProvider(provider);
|
|
||||||
return paletteGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get primary color from album art
|
|
||||||
Future<Color> getPrimaryColor(String url) async {
|
Future<Color> getPrimaryColor(String url) async {
|
||||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||||
return paletteGenerator.colors.first;
|
return paletteGenerator.colors.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if is dark
|
|
||||||
Future<bool> isDark(String url) async {
|
Future<bool> isDark(String url) async {
|
||||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||||
return paletteGenerator.colors.first.computeLuminance() > 0.5 ? false : true;
|
return paletteGenerator.colors.first.computeLuminance() > 0.5 ? false : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CachedImage extends StatefulWidget {
|
class CachedImage extends StatefulWidget {
|
||||||
|
|
||||||
final String url;
|
final String url;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final bool circular;
|
final bool circular;
|
||||||
|
final bool fullThumb;
|
||||||
|
|
||||||
const CachedImage({Key key, this.url, this.height, this.width, this.circular = false}): super(key: key);
|
const CachedImage({Key key, this.url, this.height, this.width, this.circular = false, this.fullThumb = false}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CachedImageState createState() => _CachedImageState();
|
_CachedImageState createState() => _CachedImageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CachedImageState extends State<CachedImage> {
|
class _CachedImageState extends State<CachedImage> {
|
||||||
|
|
||||||
ImageProvider _image = imagesDatabase.placeholderThumb;
|
|
||||||
double _opacity = 0.0;
|
|
||||||
bool _disposed = false;
|
|
||||||
String _prevUrl;
|
|
||||||
|
|
||||||
Future<ImageProvider> _getImage() async {
|
|
||||||
//Image already path
|
|
||||||
if (!widget.url.startsWith('http')) {
|
|
||||||
//Remove file://, if used in audio_service
|
|
||||||
if (widget.url.startsWith('/')) return FileImage(File(widget.url));
|
|
||||||
return FileImage(File(widget.url.replaceFirst('file://', '')));
|
|
||||||
}
|
|
||||||
//Load image from db
|
|
||||||
String path = await imagesDatabase.getImage(widget.url);
|
|
||||||
if (path == null) return imagesDatabase.placeholderThumb;
|
|
||||||
return FileImage(File(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
//Load image and fade
|
|
||||||
void _load() async {
|
|
||||||
if (_prevUrl == widget.url) return;
|
|
||||||
|
|
||||||
ImageProvider image = await _getImage();
|
|
||||||
if (_disposed) return;
|
|
||||||
setState(() {
|
|
||||||
_image = image;
|
|
||||||
_opacity = 1.0;
|
|
||||||
});
|
|
||||||
_prevUrl = widget.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_disposed = true;
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_load();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(CachedImage oldWidget) {
|
|
||||||
_load();
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
if (widget.circular) return ClipOval(
|
||||||
children: <Widget>[
|
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false)
|
||||||
widget.circular ?
|
);
|
||||||
CircleAvatar(
|
|
||||||
radius: (widget.width??widget.height),
|
|
||||||
backgroundImage: imagesDatabase.placeholderThumb,
|
|
||||||
):
|
|
||||||
Image(
|
|
||||||
image: imagesDatabase.placeholderThumb,
|
|
||||||
height: widget.height,
|
|
||||||
width: widget.width,
|
|
||||||
),
|
|
||||||
|
|
||||||
AnimatedOpacity(
|
return CachedNetworkImage(
|
||||||
duration: Duration(milliseconds: 250),
|
imageUrl: widget.url,
|
||||||
opacity: _opacity,
|
|
||||||
child: widget.circular ?
|
|
||||||
CircleAvatar(
|
|
||||||
radius: (widget.width??widget.height),
|
|
||||||
backgroundImage: _image,
|
|
||||||
):
|
|
||||||
Image(
|
|
||||||
image: _image,
|
|
||||||
height: widget.height,
|
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
),
|
height: widget.height,
|
||||||
)
|
placeholder: (context, url) {
|
||||||
],
|
if (widget.fullThumb) return Image.asset('assets/cover.jpg', width: widget.width, height: widget.height,);
|
||||||
|
return Image.asset('assets/cover_thumb.jpg', width: widget.width, height: widget.height);
|
||||||
|
},
|
||||||
|
errorWidget: (context, url, error) => Image.asset('assets/cover_thumb.jpg', width: widget.width, height: widget.height),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -394,7 +394,19 @@ class ArtistDetails extends StatelessWidget {
|
|||||||
fontSize: 22.0
|
fontSize: 22.0
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...List.generate(artist.albums.length, (i) {
|
...List.generate(artist.albums.length > 10 ? 11 : artist.albums.length + 1, (i) {
|
||||||
|
//Show discography
|
||||||
|
if (i == 10 || i == artist.albums.length) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text('Show all albums'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (context) => DiscographyScreen(artist: artist,))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//Top albums
|
||||||
Album a = artist.albums[i];
|
Album a = artist.albums[i];
|
||||||
return AlbumTile(
|
return AlbumTile(
|
||||||
a,
|
a,
|
||||||
@ -419,6 +431,103 @@ class ArtistDetails extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DiscographyScreen extends StatefulWidget {
|
||||||
|
|
||||||
|
Artist artist;
|
||||||
|
DiscographyScreen({@required this.artist, Key key}): super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DiscographyScreenState createState() => _DiscographyScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DiscographyScreenState extends State<DiscographyScreen> {
|
||||||
|
|
||||||
|
Artist artist;
|
||||||
|
bool _loading = false;
|
||||||
|
bool _error = false;
|
||||||
|
ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
Future _load() async {
|
||||||
|
if (artist.albums.length >= artist.albumCount || _loading) return;
|
||||||
|
setState(() => _loading = true);
|
||||||
|
|
||||||
|
//Fetch data
|
||||||
|
List<Album> data;
|
||||||
|
try {
|
||||||
|
data = await deezerAPI.discographyPage(artist.id, start: artist.albums.length);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = true;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Save
|
||||||
|
setState(() {
|
||||||
|
artist.albums.addAll(data);
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
artist = widget.artist;
|
||||||
|
|
||||||
|
//Lazy loading scroll
|
||||||
|
_scrollController.addListener(() {
|
||||||
|
double off = _scrollController.position.maxScrollExtent * 0.90;
|
||||||
|
if (_scrollController.position.pixels > off) {
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text('Discography'),),
|
||||||
|
body: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount: artist.albums.length + 1,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
//Loading
|
||||||
|
if (i == artist.albums.length) {
|
||||||
|
if (_loading)
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [CircularProgressIndicator()],
|
||||||
|
);
|
||||||
|
//Error
|
||||||
|
if (_error)
|
||||||
|
return ErrorScreen();
|
||||||
|
//Success
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
Album a = artist.albums[i];
|
||||||
|
return AlbumTile(
|
||||||
|
a,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onHold: () {
|
||||||
|
MenuSheet m = MenuSheet(context);
|
||||||
|
m.defaultAlbumMenu(a);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistDetails extends StatefulWidget {
|
class PlaylistDetails extends StatefulWidget {
|
||||||
|
|
||||||
|
@ -140,15 +140,8 @@ class MenuSheet {
|
|||||||
title: Text('Play next'),
|
title: Text('Play next'),
|
||||||
leading: Icon(Icons.playlist_play),
|
leading: Icon(Icons.playlist_play),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (playerHelper.queueIndex == -1) {
|
//-1 = next
|
||||||
//First track
|
await AudioService.addQueueItemAt(t.toMediaItem(), -1);
|
||||||
await AudioService.addQueueItem(t.toMediaItem());
|
|
||||||
await AudioService.play();
|
|
||||||
} else {
|
|
||||||
//Normal
|
|
||||||
await AudioService.addQueueItemAt(
|
|
||||||
t.toMediaItem(), playerHelper.queueIndex + 1);
|
|
||||||
}
|
|
||||||
_close();
|
_close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class PlayerBar extends StatelessWidget {
|
|||||||
leading: CachedImage(
|
leading: CachedImage(
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
url: AudioService.currentMediaItem.artUri,
|
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
AudioService.currentMediaItem.displayTitle,
|
AudioService.currentMediaItem.displayTitle,
|
||||||
|
@ -6,10 +6,12 @@ import 'package:audio_service/audio_service.dart';
|
|||||||
import 'package:flutter_screenutil/screenutil.dart';
|
import 'package:flutter_screenutil/screenutil.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/player.dart';
|
import 'package:freezer/api/player.dart';
|
||||||
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/ui/settings_screen.dart';
|
import 'package:freezer/ui/settings_screen.dart';
|
||||||
import 'package:freezer/ui/tiles.dart';
|
import 'package:freezer/ui/tiles.dart';
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:marquee/marquee.dart';
|
import 'package:marquee/marquee.dart';
|
||||||
|
|
||||||
import 'cached_image.dart';
|
import 'cached_image.dart';
|
||||||
@ -84,9 +86,10 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CachedImage(
|
CachedImage(
|
||||||
url: AudioService.currentMediaItem.artUri,
|
url: AudioService.currentMediaItem.artUri,
|
||||||
|
fullThumb: true,
|
||||||
),
|
),
|
||||||
if (_lyrics) LyricsWidget(
|
if (_lyrics) LyricsWidget(
|
||||||
artUri: AudioService.currentMediaItem.artUri,
|
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||||
trackId: AudioService.currentMediaItem.id,
|
trackId: AudioService.currentMediaItem.id,
|
||||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||||
height: ScreenUtil().setWidth(500),
|
height: ScreenUtil().setWidth(500),
|
||||||
@ -188,7 +191,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||||||
MaterialPageRoute(builder: (context) => QualitySettings())
|
MaterialPageRoute(builder: (context) => QualitySettings())
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
AudioService.currentMediaItem.extras['qualityString'],
|
AudioService.currentMediaItem.extras['qualityString'] ?? '',
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(24)),
|
style: TextStyle(fontSize: ScreenUtil().setSp(24)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -242,9 +245,10 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CachedImage(
|
CachedImage(
|
||||||
url: AudioService.currentMediaItem.artUri,
|
url: AudioService.currentMediaItem.artUri,
|
||||||
|
fullThumb: true,
|
||||||
),
|
),
|
||||||
if (_lyrics) LyricsWidget(
|
if (_lyrics) LyricsWidget(
|
||||||
artUri: AudioService.currentMediaItem.artUri,
|
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||||
trackId: AudioService.currentMediaItem.id,
|
trackId: AudioService.currentMediaItem.id,
|
||||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||||
height: ScreenUtil().setHeight(1050),
|
height: ScreenUtil().setHeight(1050),
|
||||||
@ -322,7 +326,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||||||
MaterialPageRoute(builder: (context) => QualitySettings())
|
MaterialPageRoute(builder: (context) => QualitySettings())
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
AudioService.currentMediaItem.extras['qualityString'],
|
AudioService.currentMediaItem.extras['qualityString'] ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: ScreenUtil().setSp(32),
|
fontSize: ScreenUtil().setSp(32),
|
||||||
),
|
),
|
||||||
@ -574,15 +578,15 @@ class _RepeatButtonState extends State<RepeatButton> {
|
|||||||
|
|
||||||
Icon get icon {
|
Icon get icon {
|
||||||
switch (playerHelper.repeatType) {
|
switch (playerHelper.repeatType) {
|
||||||
case RepeatType.NONE:
|
case LoopMode.off:
|
||||||
return Icon(Icons.repeat, size: widget.size??_size);
|
return Icon(Icons.repeat, size: widget.size??_size);
|
||||||
case RepeatType.LIST:
|
case LoopMode.all:
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.repeat,
|
Icons.repeat,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
size: widget.size??_size
|
size: widget.size??_size
|
||||||
);
|
);
|
||||||
case RepeatType.TRACK:
|
case LoopMode.one:
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.repeat_one,
|
Icons.repeat_one,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@ -708,6 +712,18 @@ class QueueScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _QueueScreenState extends State<QueueScreen> {
|
class _QueueScreenState extends State<QueueScreen> {
|
||||||
|
|
||||||
|
//Get proper icon color by theme
|
||||||
|
Color get shuffleIconColor {
|
||||||
|
Color og = Theme.of(context).primaryColor;
|
||||||
|
if (og.computeLuminance() > 0.5) {
|
||||||
|
if (playerHelper.shuffle) return Theme.of(context).primaryColorLight;
|
||||||
|
return Colors.black;
|
||||||
|
}
|
||||||
|
if (playerHelper.shuffle) return Theme.of(context).primaryColorDark;
|
||||||
|
return Colors.white;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -715,10 +731,13 @@ class _QueueScreenState extends State<QueueScreen> {
|
|||||||
title: Text('Queue'),
|
title: Text('Queue'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.shuffle),
|
icon: Icon(
|
||||||
|
Icons.shuffle,
|
||||||
|
color: shuffleIconColor
|
||||||
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await AudioService.customAction('shuffleQueue');
|
await playerHelper.toggleShuffle();
|
||||||
setState(() => {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:connectivity/connectivity.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezer/api/download.dart';
|
import 'package:freezer/api/download.dart';
|
||||||
import 'package:freezer/api/player.dart';
|
import 'package:freezer/api/player.dart';
|
||||||
@ -7,7 +8,6 @@ import 'package:freezer/ui/menu.dart';
|
|||||||
import 'tiles.dart';
|
import 'tiles.dart';
|
||||||
import '../api/deezer.dart';
|
import '../api/deezer.dart';
|
||||||
import '../api/definitions.dart';
|
import '../api/definitions.dart';
|
||||||
import '../settings.dart';
|
|
||||||
import 'error.dart';
|
import 'error.dart';
|
||||||
|
|
||||||
class SearchScreen extends StatefulWidget {
|
class SearchScreen extends StatefulWidget {
|
||||||
@ -18,7 +18,7 @@ class SearchScreen extends StatefulWidget {
|
|||||||
class _SearchScreenState extends State<SearchScreen> {
|
class _SearchScreenState extends State<SearchScreen> {
|
||||||
|
|
||||||
String _query;
|
String _query;
|
||||||
bool _offline = settings.offlineMode;
|
bool _offline = false;
|
||||||
|
|
||||||
void _submit(BuildContext context, {String query}) {
|
void _submit(BuildContext context, {String query}) {
|
||||||
if (query != null) _query = query;
|
if (query != null) _query = query;
|
||||||
@ -27,6 +27,19 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
//Check for connectivity and enable offline mode
|
||||||
|
Connectivity().checkConnectivity().then((res) {
|
||||||
|
if (res == ConnectivityResult.none) setState(() {
|
||||||
|
_offline = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -59,11 +72,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
leading: Switch(
|
leading: Switch(
|
||||||
value: _offline,
|
value: _offline,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (settings.offlineMode) {
|
setState(() => _offline = !_offline);
|
||||||
setState(() => _offline = true);
|
|
||||||
} else {
|
|
||||||
setState(() => _offline = v);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
826
pubspec.lock
826
pubspec.lock
@ -1,826 +0,0 @@
|
|||||||
# Generated by pub
|
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
||||||
packages:
|
|
||||||
_fe_analyzer_shared:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: _fe_analyzer_shared
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.0"
|
|
||||||
analyzer:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: analyzer
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.39.10"
|
|
||||||
archive:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: archive
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.13"
|
|
||||||
args:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: args
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.6.0"
|
|
||||||
async:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: async
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.1"
|
|
||||||
audio_service:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: audio_service
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.11.0"
|
|
||||||
boolean_selector:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: boolean_selector
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
build:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
build_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_config
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.2"
|
|
||||||
build_daemon:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_daemon
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
build_resolvers:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_resolvers
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.9"
|
|
||||||
build_runner:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: build_runner
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.10.0"
|
|
||||||
build_runner_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_runner_core
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.2.0"
|
|
||||||
built_collection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: built_collection
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.3.2"
|
|
||||||
built_value:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: built_value
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "7.1.0"
|
|
||||||
charcode:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: charcode
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.3"
|
|
||||||
checked_yaml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: checked_yaml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
clock:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: clock
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
code_builder:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: code_builder
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.3.0"
|
|
||||||
collection:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: collection
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.14.12"
|
|
||||||
connectivity:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: connectivity
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.8+6"
|
|
||||||
connectivity_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_macos
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.0+3"
|
|
||||||
connectivity_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.6"
|
|
||||||
convert:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: convert
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.1"
|
|
||||||
cookie_jar:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: cookie_jar
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
country_pickers:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: country_pickers
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
crypto:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: crypto
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
csslib:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: csslib
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.16.1"
|
|
||||||
custom_navigator:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: custom_navigator
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.0"
|
|
||||||
dart_style:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dart_style
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.6"
|
|
||||||
dio:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: dio
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.9"
|
|
||||||
dio_cookie_manager:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: dio_cookie_manager
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
disk_space:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: disk_space
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.3"
|
|
||||||
ext_storage:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: ext_storage
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.3"
|
|
||||||
fading_edge_scrollview:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: fading_edge_scrollview
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.4"
|
|
||||||
file:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: file
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.1.0"
|
|
||||||
filesize:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: filesize
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
fixnum:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: fixnum
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.11"
|
|
||||||
flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_cache_manager:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_cache_manager
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
flutter_inappwebview:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_inappwebview
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.3.0+3"
|
|
||||||
flutter_isolate:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_isolate
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0+14"
|
|
||||||
flutter_local_notifications:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_local_notifications
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.4+1"
|
|
||||||
flutter_local_notifications_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_local_notifications_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
flutter_material_color_picker:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_material_color_picker
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
flutter_screenutil:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_screenutil
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.3.0"
|
|
||||||
flutter_test:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_web_plugins:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
fluttertoast:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: fluttertoast
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.1"
|
|
||||||
glob:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: glob
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
graphs:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: graphs
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
|
||||||
hex:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: hex
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.2"
|
|
||||||
html:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: html
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.0+3"
|
|
||||||
http:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.1"
|
|
||||||
http_multi_server:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_multi_server
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.0"
|
|
||||||
http_parser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_parser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.4"
|
|
||||||
image:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.12"
|
|
||||||
intl:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: intl
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.16.1"
|
|
||||||
io:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: io
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.4"
|
|
||||||
js:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: js
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.2"
|
|
||||||
json_annotation:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: json_annotation
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.1"
|
|
||||||
json_serializable:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: json_serializable
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.3.0"
|
|
||||||
just_audio:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "."
|
|
||||||
ref: HEAD
|
|
||||||
resolved-ref: "70392a52590c95bd4b1ca35c7e92d30793c7c4d3"
|
|
||||||
url: "https://notabug.org/exttex/just_audio.git"
|
|
||||||
source: git
|
|
||||||
version: "0.1.10"
|
|
||||||
language_pickers:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: language_pickers
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0+1"
|
|
||||||
logging:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: logging
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.11.4"
|
|
||||||
marquee:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: marquee
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.5.2"
|
|
||||||
matcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: matcher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.6"
|
|
||||||
meta:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: meta
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.8"
|
|
||||||
mime:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: mime
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.6+3"
|
|
||||||
move_to_background:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: move_to_background
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
node_interop:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_interop
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
node_io:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_io
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
package_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_config
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.3"
|
|
||||||
package_info:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: package_info
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.1"
|
|
||||||
palette_generator:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: palette_generator
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.3"
|
|
||||||
path:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: path
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.6.4"
|
|
||||||
path_provider:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: path_provider
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.6.10"
|
|
||||||
path_provider_ex:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: path_provider_ex
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
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"
|
|
||||||
pedantic:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pedantic
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.0"
|
|
||||||
permission_handler:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: permission_handler
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.1"
|
|
||||||
permission_handler_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: permission_handler_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
petitparser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: petitparser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.0"
|
|
||||||
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"
|
|
||||||
pointycastle:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: pointycastle
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
pool:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pool
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
process:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: process
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.13"
|
|
||||||
pub_semver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pub_semver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.4"
|
|
||||||
pubspec_parse:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pubspec_parse
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.5"
|
|
||||||
quiver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: quiver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
random_string:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: random_string
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
rxdart:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: rxdart
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.24.1"
|
|
||||||
shelf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.5"
|
|
||||||
shelf_web_socket:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_web_socket
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.3"
|
|
||||||
sky_engine:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.99"
|
|
||||||
source_gen:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_gen
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.5"
|
|
||||||
source_span:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_span
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.7.0"
|
|
||||||
sqflite:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: sqflite
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.1"
|
|
||||||
sqflite_common:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sqflite_common
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stack_trace
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.3"
|
|
||||||
stream_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_channel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
stream_transform:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_transform
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
string_scanner:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: string_scanner
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
synchronized:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: synchronized
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.0"
|
|
||||||
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.15"
|
|
||||||
timing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: timing
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.1+2"
|
|
||||||
typed_data:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: typed_data
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.6"
|
|
||||||
uuid:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: uuid
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.4"
|
|
||||||
vector_math:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vector_math
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.8"
|
|
||||||
watcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: watcher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.7+15"
|
|
||||||
web_socket_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web_socket_channel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
xdg_directories:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xdg_directories
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.0"
|
|
||||||
xml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.6.1"
|
|
||||||
yaml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: yaml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.1"
|
|
||||||
sdks:
|
|
||||||
dart: ">=2.7.0 <3.0.0"
|
|
||||||
flutter: ">=1.15.21 <2.0.0"
|
|
10
pubspec.yaml
10
pubspec.yaml
@ -39,10 +39,10 @@ dependencies:
|
|||||||
connectivity: ^0.4.8+6
|
connectivity: ^0.4.8+6
|
||||||
intl: ^0.16.1
|
intl: ^0.16.1
|
||||||
filesize: ^1.0.4
|
filesize: ^1.0.4
|
||||||
fluttertoast: ^4.0.1
|
fluttertoast: ^7.0.2
|
||||||
palette_generator: ^0.2.3
|
palette_generator: ^0.2.3
|
||||||
flutter_material_color_picker: ^1.0.5
|
flutter_material_color_picker: ^1.0.5
|
||||||
flutter_inappwebview: ^3.3.0+3
|
flutter_inappwebview: ^4.0.0
|
||||||
custom_navigator: ^0.3.0
|
custom_navigator: ^0.3.0
|
||||||
language_pickers: ^0.2.0+1
|
language_pickers: ^0.2.0+1
|
||||||
country_pickers: ^1.3.0
|
country_pickers: ^1.3.0
|
||||||
@ -51,16 +51,18 @@ dependencies:
|
|||||||
flutter_local_notifications: ^1.4.4+1
|
flutter_local_notifications: ^1.4.4+1
|
||||||
collection: ^1.14.12
|
collection: ^1.14.12
|
||||||
disk_space: ^0.0.3
|
disk_space: ^0.0.3
|
||||||
audio_service: ^0.11.0
|
|
||||||
path_provider_ex: ^1.0.1
|
path_provider_ex: ^1.0.1
|
||||||
random_string: ^2.0.1
|
random_string: ^2.0.1
|
||||||
async: ^2.4.1
|
async: ^2.4.1
|
||||||
html: ^0.14.0+3
|
html: ^0.14.0+3
|
||||||
flutter_screenutil: ^2.3.0
|
flutter_screenutil: ^2.3.0
|
||||||
marquee: ^1.5.2
|
marquee: ^1.5.2
|
||||||
|
flutter_cache_manager: ^1.4.1
|
||||||
|
cached_network_image: ^2.2.0+1
|
||||||
|
|
||||||
|
audio_service: ^0.13.0
|
||||||
just_audio:
|
just_audio:
|
||||||
git: https://notabug.org/exttex/just_audio.git
|
path: ./just_audio
|
||||||
|
|
||||||
# cupertino_icons: ^0.1.3
|
# cupertino_icons: ^0.1.3
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user