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
|
||||
android/key.properties
|
||||
|
||||
just_audio/
|
||||
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
|
|
|
@ -27,7 +27,7 @@ https://t.me/freezerandroid
|
|||
Tobs: Beta tester
|
||||
Bas Curtiz: Icon, Logo, Banner, Design suggestions
|
||||
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
|
||||
|
@ -35,11 +35,12 @@ BTC: `14hcr4PGbgqeXd3SoXY9QyJFNpyurgrL9y`
|
|||
ETH: `0xb4D1893195404E1F4b45e5BDA77F202Ac4012288`
|
||||
|
||||
## 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
|
||||
```
|
||||
Freezer was not developed for piracy, but educational and private usage.
|
||||
It may be illegal to use this in your country!
|
||||
I am not responsible in any way for the usage of this app.
|
||||
```
|
||||
```
|
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');
|
||||
// });
|
||||
}
|
|
@ -360,6 +360,18 @@ class DeezerAPI {
|
|||
//Return playlistId
|
||||
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,
|
||||
extras: {
|
||||
"playbackDetails": jsonEncode(this.playbackDetails),
|
||||
"thumb": this.albumArt.thumb,
|
||||
"lyrics": jsonEncode(this.lyrics.toJson()),
|
||||
"albumId": this.album.id,
|
||||
"artists": jsonEncode(this.artists.map<Map>((art) => art.toJson()).toList())
|
||||
|
@ -102,7 +103,10 @@ class Track {
|
|||
artists: artists,
|
||||
album: album,
|
||||
id: mi.id,
|
||||
albumArt: ImageDetails(fullUrl: mi.artUri),
|
||||
albumArt: ImageDetails(
|
||||
fullUrl: mi.artUri,
|
||||
thumbUrl: mi.extras['thumb']
|
||||
),
|
||||
duration: mi.duration,
|
||||
playbackDetails: null, // So it gets updated from api
|
||||
lyrics: Lyrics.fromJson(jsonDecode(((mi.extras??{})['lyrics'])??"{}"))
|
||||
|
@ -116,7 +120,7 @@ class Track {
|
|||
title = "${json['SNG_TITLE']} ${json['VERSION']}";
|
||||
}
|
||||
return Track(
|
||||
id: json['SNG_ID'],
|
||||
id: json['SNG_ID'].toString(),
|
||||
title: title,
|
||||
duration: Duration(seconds: int.parse(json['DURATION'])),
|
||||
albumArt: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
||||
|
@ -180,7 +184,7 @@ class Album {
|
|||
|
||||
//JSON
|
||||
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'],
|
||||
art: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
||||
artists: (json['ARTISTS']??[json]).map<Artist>((dynamic art) => Artist.fromPrivateJson(art)).toList(),
|
||||
|
@ -240,7 +244,7 @@ class Artist {
|
|||
Map<dynamic, dynamic> topJson = const {},
|
||||
bool library = false
|
||||
}) => Artist(
|
||||
id: json['ART_ID'],
|
||||
id: json['ART_ID'].toString(),
|
||||
name: json['ART_NAME'],
|
||||
fans: json['NB_FAN'],
|
||||
picture: ImageDetails.fromPrivateString(json['ART_PICTURE'], type: 'artist'),
|
||||
|
@ -299,7 +303,7 @@ class Playlist {
|
|||
|
||||
//JSON
|
||||
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'],
|
||||
trackCount: json['NB_SONG']??songsJson['total'],
|
||||
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]);
|
||||
if (duplicate.length != 0) return;
|
||||
//Save art
|
||||
await imagesDatabase.getImage(track.albumArt.full, permanent: true);
|
||||
//await imagesDatabase.getImage(track.albumArt.full);
|
||||
imagesDatabase.saveImage(track.albumArt.full);
|
||||
//Save to db
|
||||
b.insert('tracks', track.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
b.insert('albums', track.album.toSQL(), conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/ui/cached_image.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:connectivity/connectivity.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
@ -23,7 +20,8 @@ class PlayerHelper {
|
|||
StreamSubscription _customEventSubscription;
|
||||
StreamSubscription _playbackStateStreamSubscription;
|
||||
QueueSource queueSource;
|
||||
RepeatType repeatType = RepeatType.NONE;
|
||||
LoopMode repeatType = LoopMode.off;
|
||||
bool shuffle = false;
|
||||
|
||||
//Find queue index by id
|
||||
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 last song is played, load more queue
|
||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||
print(queueSource.toJson());
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
@ -74,20 +71,24 @@ class PlayerHelper {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
Future toggleShuffle() async {
|
||||
this.shuffle = !this.shuffle;
|
||||
await AudioService.customAction('shuffle', this.shuffle);
|
||||
}
|
||||
|
||||
//Repeat toggle
|
||||
Future changeRepeat() async {
|
||||
//Change to next repeat type
|
||||
switch (repeatType) {
|
||||
case RepeatType.NONE:
|
||||
repeatType = RepeatType.LIST; break;
|
||||
case RepeatType.LIST:
|
||||
repeatType = RepeatType.TRACK; break;
|
||||
case LoopMode.one:
|
||||
repeatType = LoopMode.off; break;
|
||||
case LoopMode.all:
|
||||
repeatType = LoopMode.one; break;
|
||||
default:
|
||||
repeatType = RepeatType.NONE; break;
|
||||
repeatType = LoopMode.all; break;
|
||||
}
|
||||
//Set repeat type
|
||||
await AudioService.customAction("repeatType", RepeatType.values.indexOf(repeatType));
|
||||
await AudioService.customAction("repeatType", LoopMode.values.indexOf(repeatType));
|
||||
}
|
||||
|
||||
//Executed before exit
|
||||
|
@ -101,7 +102,7 @@ class PlayerHelper {
|
|||
await startService();
|
||||
await settings.updateAudioServiceQuality();
|
||||
await AudioService.updateQueue(queue);
|
||||
await AudioService.playFromMediaId(trackId);
|
||||
await AudioService.skipToQueueItem(trackId);
|
||||
}
|
||||
|
||||
//Play track from album
|
||||
|
@ -178,277 +179,229 @@ void backgroundTaskEntrypoint() async {
|
|||
}
|
||||
|
||||
class AudioPlayerTask extends BackgroundAudioTask {
|
||||
AudioPlayer _player = AudioPlayer();
|
||||
|
||||
AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
//Queue
|
||||
List<MediaItem> _queue = <MediaItem>[];
|
||||
int _queueIndex = -1;
|
||||
int _queueIndex = 0;
|
||||
ConcatenatingAudioSource _audioSource;
|
||||
|
||||
bool _playing;
|
||||
bool _interrupted;
|
||||
AudioProcessingState _skipState;
|
||||
Duration _lastPosition;
|
||||
bool _interrupted;
|
||||
Seeker _seeker;
|
||||
|
||||
ImagesDatabase imagesDB;
|
||||
//Stream subscriptions
|
||||
StreamSubscription _eventSub;
|
||||
|
||||
//Loaded from file/frontend
|
||||
int mobileQuality;
|
||||
int wifiQuality;
|
||||
|
||||
StreamSubscription _eventSub;
|
||||
StreamSubscription _playerStateSub;
|
||||
|
||||
QueueSource queueSource;
|
||||
int repeatType = 0;
|
||||
Duration _lastPosition;
|
||||
|
||||
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
|
||||
Future onStart(Map<String, dynamic> params) async {
|
||||
_playerStateSub = _audioPlayer.playbackStateStream
|
||||
.where((state) => state == AudioPlaybackState.completed)
|
||||
.listen((_event) {
|
||||
if (_queue.length > _queueIndex + 1) {
|
||||
onSkipToNext();
|
||||
return;
|
||||
} else {
|
||||
//Repeat whole list (if enabled)
|
||||
if (repeatType == 1) {
|
||||
_skip(-_queueIndex);
|
||||
return;
|
||||
}
|
||||
//Ask for more tracks in queue
|
||||
AudioServiceBackground.sendCustomEvent({
|
||||
'action': 'queueEnd',
|
||||
'queueSource': (queueSource??QueueSource()).toJson()
|
||||
});
|
||||
if (_playing) _playing = false;
|
||||
_setState(AudioProcessingState.none);
|
||||
return;
|
||||
Future onStart(Map<String, dynamic> params) {
|
||||
|
||||
//Update track index
|
||||
_player.currentIndexStream.listen((index) {
|
||||
if (index != null) {
|
||||
_queueIndex = index;
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
}
|
||||
});
|
||||
//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({
|
||||
'action': 'queueEnd',
|
||||
'queueSource': (queueSource??QueueSource()).toJson()
|
||||
});
|
||||
break;
|
||||
case ProcessingState.ready:
|
||||
//Ready to play
|
||||
_skipState = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
//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;
|
||||
case AudioPlaybackState.connecting:
|
||||
_setState(_skipState ?? AudioProcessingState.connecting, pos: event.position);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
//Initialize later
|
||||
//await imagesDB.init();
|
||||
|
||||
//Load queue
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.sendCustomEvent({'action': 'onLoad'});
|
||||
}
|
||||
|
||||
@override
|
||||
Future onSkipToNext() async {
|
||||
//If repeating allowed
|
||||
if (repeatType == 2) {
|
||||
await _skip(0);
|
||||
return null;
|
||||
}
|
||||
_skip(1);
|
||||
}
|
||||
Future onSkipToQueueItem(String mediaId) async {
|
||||
_lastPosition = null;
|
||||
|
||||
@override
|
||||
Future onSkipToPrevious() => _skip(-1);
|
||||
//Calculate new index
|
||||
final newIndex = _queue.indexWhere((i) => i.id == mediaId);
|
||||
if (newIndex == -1) return;
|
||||
|
||||
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);
|
||||
//Update buffering state
|
||||
_skipState = newIndex > _queueIndex
|
||||
? AudioProcessingState.skippingToNext
|
||||
: AudioProcessingState.skippingToPrevious;
|
||||
|
||||
//Skip in player
|
||||
await _player.seek(Duration.zero, index: newIndex);
|
||||
_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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_skipState == null) {
|
||||
_playing = true;
|
||||
_audioPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onPause() {
|
||||
if (_skipState == null && _playing) {
|
||||
_playing = false;
|
||||
_audioPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onSeekTo(Duration pos) {
|
||||
_audioPlayer.seek(pos);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClick(MediaButton button) {
|
||||
if (_playing) onPause();
|
||||
onPlay();
|
||||
}
|
||||
|
||||
@override
|
||||
Future onUpdateQueue(List<MediaItem> q) async {
|
||||
this._queue = q;
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
await _saveQueue();
|
||||
Future onPlay() {
|
||||
_player.play();
|
||||
//Restore position on play
|
||||
if (_lastPosition != null) {
|
||||
onSeekTo(_lastPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onPlayFromMediaId(String mediaId) async {
|
||||
int pos = this._queue.indexWhere((mi) => mi.id == mediaId);
|
||||
await _skip(pos - _queueIndex);
|
||||
if (_playing == null || !_playing) onPlay();
|
||||
}
|
||||
Future onPause() => _player.pause();
|
||||
|
||||
@override
|
||||
Future onFastForward() async {
|
||||
await _seekRelative(fastForwardInterval);
|
||||
}
|
||||
Future onSeekTo(Duration pos) => _player.seek(pos);
|
||||
|
||||
@override
|
||||
void onAddQueueItemAt(MediaItem mi, int index) {
|
||||
_queue.insert(index, mi);
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
_saveQueue();
|
||||
}
|
||||
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
|
||||
|
||||
@override
|
||||
void onAddQueueItem(MediaItem mi) {
|
||||
_queue.add(mi);
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
_saveQueue();
|
||||
}
|
||||
Future<void> onRewind() => _seekRelative(-rewindInterval);
|
||||
|
||||
@override
|
||||
Future onRewind() async {
|
||||
await _seekRelative(rewindInterval);
|
||||
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
|
||||
|
||||
@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 {
|
||||
Duration newPos = _audioPlayer.playbackEvent.position + offset;
|
||||
Duration newPos = _player.position + offset;
|
||||
//Out of bounds check
|
||||
if (newPos < Duration.zero) newPos = Duration.zero;
|
||||
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
|
||||
Future onUpdateMediaItem(MediaItem mediaItem) async {
|
||||
_queue[_queueIndex] = mediaItem;
|
||||
Future onUpdateQueue(List<MediaItem> q) async {
|
||||
//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);
|
||||
}
|
||||
|
||||
//Audio interruptions
|
||||
@override
|
||||
void onAudioFocusLost(AudioInterruption interruption) {
|
||||
if (_playing) _interrupted = true;
|
||||
switch (interruption) {
|
||||
case AudioInterruption.pause:
|
||||
case AudioInterruption.temporaryPause:
|
||||
case AudioInterruption.unknownPause:
|
||||
if (_playing) onPause();
|
||||
break;
|
||||
case AudioInterruption.temporaryDuck:
|
||||
_audioPlayer.setVolume(0.5);
|
||||
break;
|
||||
Future<AudioSource> _mediaItemToAudioSource(MediaItem mi) async {
|
||||
String url = await _getTrackUrl(mi);
|
||||
if (url.startsWith('http')) return ProgressiveAudioSource(Uri.parse(url));
|
||||
return AudioSource.uri(Uri.parse(url));
|
||||
}
|
||||
|
||||
Future _getTrackUrl(MediaItem mediaItem, {int quality}) async {
|
||||
//Check if offline
|
||||
String _offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||
File f = File(p.join(_offlinePath, mediaItem.id));
|
||||
if (await f.exists()) {
|
||||
return f.path;
|
||||
}
|
||||
|
||||
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
|
||||
//This just returns fake url that contains metadata
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
void onAudioFocusGained(AudioInterruption interruption) {
|
||||
switch (interruption) {
|
||||
case AudioInterruption.temporaryPause:
|
||||
if (!_playing && _interrupted) onPlay();
|
||||
break;
|
||||
case AudioInterruption.temporaryDuck:
|
||||
_audioPlayer.setVolume(1.0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_interrupted = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onAudioBecomingNoisy() {
|
||||
onPause();
|
||||
}
|
||||
|
||||
|
||||
//Custom actions
|
||||
@override
|
||||
Future onCustomAction(String name, dynamic args) async {
|
||||
if (name == 'updateQuality') {
|
||||
|
@ -457,228 +410,178 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
this.wifiQuality = args['wifiQuality'];
|
||||
this.mobileQuality = args['mobileQuality'];
|
||||
}
|
||||
if (name == 'saveQueue') {
|
||||
await this._saveQueue();
|
||||
}
|
||||
//Load queue, called after start
|
||||
if (name == 'load') {
|
||||
await _loadQueue();
|
||||
}
|
||||
//Change queue source
|
||||
if (name == 'queueSource') {
|
||||
this.queueSource = QueueSource.fromJson(Map<String, dynamic>.from(args));
|
||||
}
|
||||
//Shuffle
|
||||
if (name == 'shuffleQueue') {
|
||||
MediaItem mi = mediaItem;
|
||||
shuffle(this._queue);
|
||||
_queueIndex = _queue.indexOf(mi);
|
||||
AudioServiceBackground.setQueue(this._queue);
|
||||
}
|
||||
//Repeating
|
||||
//Looping
|
||||
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;
|
||||
}
|
||||
|
||||
Future<String> _getArtUri(String url) async {
|
||||
//Load from cache
|
||||
if (url.startsWith('http')) {
|
||||
//Prepare db
|
||||
if (imagesDB == null) {
|
||||
imagesDB = ImagesDatabase();
|
||||
await imagesDB.init();
|
||||
}
|
||||
|
||||
String path = await imagesDB.getImage(url);
|
||||
return 'file://$path';
|
||||
//Audio interruptions
|
||||
@override
|
||||
Future onAudioFocusLost(AudioInterruption interruption) {
|
||||
if (_player.playing) _interrupted = true;
|
||||
switch (interruption) {
|
||||
case AudioInterruption.pause:
|
||||
case AudioInterruption.temporaryPause:
|
||||
case AudioInterruption.unknownPause:
|
||||
if (_player.playing) onPause();
|
||||
break;
|
||||
case AudioInterruption.temporaryDuck:
|
||||
_player.setVolume(0.5);
|
||||
break;
|
||||
}
|
||||
//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
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
Future onClose() async {
|
||||
await onStop();
|
||||
}
|
||||
|
||||
Future onStop() async {
|
||||
_audioPlayer.stop();
|
||||
if (_playerStateSub != null) _playerStateSub.cancel();
|
||||
if (_eventSub != null) _eventSub.cancel();
|
||||
await _saveQueue();
|
||||
_player.stop();
|
||||
if (_eventSub != null) _eventSub.cancel();
|
||||
|
||||
await super.onStop();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@override
|
||||
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
|
||||
//Get queue save file path
|
||||
Future<String> _getQueuePath() async {
|
||||
Directory dir = await getApplicationDocumentsDirectory();
|
||||
return p.join(dir.path, 'offline.json');
|
||||
return p.join(dir.path, 'playback.json');
|
||||
}
|
||||
|
||||
//Export queue to JSON
|
||||
Future _saveQueue() async {
|
||||
print('save');
|
||||
File f = File(await _getQueuePath());
|
||||
await f.writeAsString(jsonEncode({
|
||||
String path = await _getQueuePath();
|
||||
File f = File(path);
|
||||
//Create if doesnt exist
|
||||
if (! await File(path).exists()) {
|
||||
f = await f.create();
|
||||
}
|
||||
|
||||
Map data = {
|
||||
'index': _queueIndex,
|
||||
'queue': _queue.map<Map<String, dynamic>>((mi) => mi.toJson()).toList(),
|
||||
'position': _audioPlayer.playbackEvent.position.inMilliseconds,
|
||||
'position': _player.position.inMilliseconds,
|
||||
'queueSource': (queueSource??QueueSource()).toJson(),
|
||||
}));
|
||||
};
|
||||
await f.writeAsString(jsonEncode(data));
|
||||
}
|
||||
|
||||
Future _loadQueue() async {
|
||||
//Restore queue & playback info from path
|
||||
Future _loadQueueFile() async {
|
||||
File f = File(await _getQueuePath());
|
||||
if (await f.exists()) {
|
||||
Map<String, dynamic> json = jsonDecode(await f.readAsString());
|
||||
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.queueSource = QueueSource.fromJson(json['queueSource']??{});
|
||||
//Restore queue
|
||||
if (_queue != null) {
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
await AudioServiceBackground.setQueue(_queue);
|
||||
await _loadQueue();
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
//Update state to allow play button in notification
|
||||
this._setState(AudioProcessingState.none, pos: _lastPosition);
|
||||
}
|
||||
//Send restored queue source to ui
|
||||
AudioServiceBackground.sendCustomEvent({'action': 'onRestore', 'queueSource': (queueSource??QueueSource()).toJson()});
|
||||
return true;
|
||||
}
|
||||
//Send restored queue source to ui
|
||||
AudioServiceBackground.sendCustomEvent({
|
||||
'action': 'onRestore',
|
||||
'queueSource': (queueSource??QueueSource()).toJson()
|
||||
});
|
||||
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
|
||||
settings = await Settings().loadSettings();
|
||||
await imagesDatabase.init();
|
||||
//await imagesDatabase.init();
|
||||
await downloadManager.init();
|
||||
|
||||
runApp(FreezerApp());
|
||||
|
@ -44,9 +44,6 @@ class _FreezerAppState extends State<FreezerApp> {
|
|||
//Make update theme global
|
||||
updateTheme = _updateTheme;
|
||||
|
||||
//Precache placeholder
|
||||
precacheImage(imagesDatabase.placeholderThumb, context);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,209 +1,66 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
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';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
ImagesDatabase imagesDatabase = ImagesDatabase();
|
||||
|
||||
|
||||
class ImagesDatabase {
|
||||
|
||||
/*
|
||||
images.db:
|
||||
Table: images
|
||||
Fields:
|
||||
id - id
|
||||
name - md5 hash of url. also filename
|
||||
url - url
|
||||
permanent - 0/1 - if image is cached or offline
|
||||
!!! Using the wrappers so i don't have to rewrite most of the code, because of migration to cached network image
|
||||
*/
|
||||
|
||||
|
||||
Database db;
|
||||
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);
|
||||
void saveImage(String url) {
|
||||
CachedNetworkImageProvider(url);
|
||||
}
|
||||
|
||||
String getPath(String name) {
|
||||
return p.join(imagesPath, name);
|
||||
Future<PaletteGenerator> getPaletteGenerator(String url) {
|
||||
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 {
|
||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||
return paletteGenerator.colors.first;
|
||||
}
|
||||
|
||||
//Check if is dark
|
||||
Future<bool> isDark(String url) async {
|
||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||
return paletteGenerator.colors.first.computeLuminance() > 0.5 ? false : true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class CachedImage extends StatefulWidget {
|
||||
|
||||
final String url;
|
||||
final double width;
|
||||
final double height;
|
||||
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
|
||||
_CachedImageState createState() => _CachedImageState();
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
widget.circular ?
|
||||
CircleAvatar(
|
||||
radius: (widget.width??widget.height),
|
||||
backgroundImage: imagesDatabase.placeholderThumb,
|
||||
):
|
||||
Image(
|
||||
image: imagesDatabase.placeholderThumb,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
),
|
||||
if (widget.circular) return ClipOval(
|
||||
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false)
|
||||
);
|
||||
|
||||
AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 250),
|
||||
opacity: _opacity,
|
||||
child: widget.circular ?
|
||||
CircleAvatar(
|
||||
radius: (widget.width??widget.height),
|
||||
backgroundImage: _image,
|
||||
):
|
||||
Image(
|
||||
image: _image,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
),
|
||||
)
|
||||
],
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.url,
|
||||
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
|
||||
),
|
||||
),
|
||||
...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];
|
||||
return AlbumTile(
|
||||
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 {
|
||||
|
||||
|
|
|
@ -140,15 +140,8 @@ class MenuSheet {
|
|||
title: Text('Play next'),
|
||||
leading: Icon(Icons.playlist_play),
|
||||
onTap: () async {
|
||||
if (playerHelper.queueIndex == -1) {
|
||||
//First track
|
||||
await AudioService.addQueueItem(t.toMediaItem());
|
||||
await AudioService.play();
|
||||
} else {
|
||||
//Normal
|
||||
await AudioService.addQueueItemAt(
|
||||
t.toMediaItem(), playerHelper.queueIndex + 1);
|
||||
}
|
||||
//-1 = next
|
||||
await AudioService.addQueueItemAt(t.toMediaItem(), -1);
|
||||
_close();
|
||||
});
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class PlayerBar extends StatelessWidget {
|
|||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: AudioService.currentMediaItem.artUri,
|
||||
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri,
|
||||
),
|
||||
title: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
|
|
|
@ -6,10 +6,12 @@ import 'package:audio_service/audio_service.dart';
|
|||
import 'package:flutter_screenutil/screenutil.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/ui/settings_screen.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
|
||||
import 'cached_image.dart';
|
||||
|
@ -84,9 +86,10 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: AudioService.currentMediaItem.artUri,
|
||||
fullThumb: true,
|
||||
),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.artUri,
|
||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: ScreenUtil().setWidth(500),
|
||||
|
@ -188,7 +191,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
MaterialPageRoute(builder: (context) => QualitySettings())
|
||||
),
|
||||
child: Text(
|
||||
AudioService.currentMediaItem.extras['qualityString'],
|
||||
AudioService.currentMediaItem.extras['qualityString'] ?? '',
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(24)),
|
||||
),
|
||||
),
|
||||
|
@ -242,9 +245,10 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: AudioService.currentMediaItem.artUri,
|
||||
fullThumb: true,
|
||||
),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.artUri,
|
||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: ScreenUtil().setHeight(1050),
|
||||
|
@ -322,7 +326,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
MaterialPageRoute(builder: (context) => QualitySettings())
|
||||
),
|
||||
child: Text(
|
||||
AudioService.currentMediaItem.extras['qualityString'],
|
||||
AudioService.currentMediaItem.extras['qualityString'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(32),
|
||||
),
|
||||
|
@ -574,15 +578,15 @@ class _RepeatButtonState extends State<RepeatButton> {
|
|||
|
||||
Icon get icon {
|
||||
switch (playerHelper.repeatType) {
|
||||
case RepeatType.NONE:
|
||||
case LoopMode.off:
|
||||
return Icon(Icons.repeat, size: widget.size??_size);
|
||||
case RepeatType.LIST:
|
||||
case LoopMode.all:
|
||||
return Icon(
|
||||
Icons.repeat,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: widget.size??_size
|
||||
);
|
||||
case RepeatType.TRACK:
|
||||
case LoopMode.one:
|
||||
return Icon(
|
||||
Icons.repeat_one,
|
||||
color: Theme.of(context).primaryColor,
|
||||
|
@ -708,6 +712,18 @@ class QueueScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -715,10 +731,13 @@ class _QueueScreenState extends State<QueueScreen> {
|
|||
title: Text('Queue'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.shuffle),
|
||||
icon: Icon(
|
||||
Icons.shuffle,
|
||||
color: shuffleIconColor
|
||||
),
|
||||
onPressed: () async {
|
||||
await AudioService.customAction('shuffleQueue');
|
||||
setState(() => {});
|
||||
await playerHelper.toggleShuffle();
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
],
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:connectivity/connectivity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
|
@ -7,7 +8,6 @@ import 'package:freezer/ui/menu.dart';
|
|||
import 'tiles.dart';
|
||||
import '../api/deezer.dart';
|
||||
import '../api/definitions.dart';
|
||||
import '../settings.dart';
|
||||
import 'error.dart';
|
||||
|
||||
class SearchScreen extends StatefulWidget {
|
||||
|
@ -18,7 +18,7 @@ class SearchScreen extends StatefulWidget {
|
|||
class _SearchScreenState extends State<SearchScreen> {
|
||||
|
||||
String _query;
|
||||
bool _offline = settings.offlineMode;
|
||||
bool _offline = false;
|
||||
|
||||
void _submit(BuildContext context, {String 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -59,11 +72,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
leading: Switch(
|
||||
value: _offline,
|
||||
onChanged: (v) {
|
||||
if (settings.offlineMode) {
|
||||
setState(() => _offline = true);
|
||||
} else {
|
||||
setState(() => _offline = v);
|
||||
}
|
||||
setState(() => _offline = !_offline);
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
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
|
||||
intl: ^0.16.1
|
||||
filesize: ^1.0.4
|
||||
fluttertoast: ^4.0.1
|
||||
fluttertoast: ^7.0.2
|
||||
palette_generator: ^0.2.3
|
||||
flutter_material_color_picker: ^1.0.5
|
||||
flutter_inappwebview: ^3.3.0+3
|
||||
flutter_inappwebview: ^4.0.0
|
||||
custom_navigator: ^0.3.0
|
||||
language_pickers: ^0.2.0+1
|
||||
country_pickers: ^1.3.0
|
||||
|
@ -51,16 +51,18 @@ dependencies:
|
|||
flutter_local_notifications: ^1.4.4+1
|
||||
collection: ^1.14.12
|
||||
disk_space: ^0.0.3
|
||||
audio_service: ^0.11.0
|
||||
path_provider_ex: ^1.0.1
|
||||
random_string: ^2.0.1
|
||||
async: ^2.4.1
|
||||
html: ^0.14.0+3
|
||||
flutter_screenutil: ^2.3.0
|
||||
marquee: ^1.5.2
|
||||
flutter_cache_manager: ^1.4.1
|
||||
cached_network_image: ^2.2.0+1
|
||||
|
||||
audio_service: ^0.13.0
|
||||
just_audio:
|
||||
git: https://notabug.org/exttex/just_audio.git
|
||||
path: ./just_audio
|
||||
|
||||
# cupertino_icons: ^0.1.3
|
||||
|
||||
|
|
Loading…
Reference in a new issue