diff --git a/assets_audio_player_web/.gitignore b/assets_audio_player_web/.gitignore new file mode 100644 index 00000000..e9dc58d3 --- /dev/null +++ b/assets_audio_player_web/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/assets_audio_player_web/.metadata b/assets_audio_player_web/.metadata new file mode 100644 index 00000000..ec5d4108 --- /dev/null +++ b/assets_audio_player_web/.metadata @@ -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: b8bd09db210d2c6299555643af8db4b8ff3e8d92 + channel: master + +project_type: plugin diff --git a/assets_audio_player_web/CHANGELOG.md b/assets_audio_player_web/CHANGELOG.md new file mode 100644 index 00000000..083446c8 --- /dev/null +++ b/assets_audio_player_web/CHANGELOG.md @@ -0,0 +1,436 @@ +## 3.1.2 + +- Migrates to `package:web` to support WASM + +## 3.1.1 + +- fix startup crash issue for some Android devices. + +## 3.1.0 + +- revert back changes + +## 3.0.9 + +- fix notification package naming and build issue + +## 3.0.8 + +- fix warnings + +## 3.0.7 + +- Update dependencies and fix notification issue + +## 3.0.6 + +- Update kotling version and fix minor issues + +## 3.0.5 + +- Breaking change, with Flutter 3.0 removed null aware for WidgetsBinding + +## 3.0.4+5 + +- Fixed null aware + +## 3.0.4+4 + +- Fix issues and update exoplayer + +## 3.0.4+3 + +- Fix warnings and abstract issue + +## 3.0.4+2 + +- Fix flutter 3.0 issues + +## 3.0.4+1 + +- Fix andorid 12 issues and update exoplayer and gradles + +## 3.0.4 + +- Fix web open player issue +- update example app Android 12 compatable + +## 3.0.3+9 + +- Fix mimType issue mp3 files from urls without extension #630 +- Fix web Null issue + +## 3.0.3+8 + +- Fix android 12 / api 31 issue. +- Fix macOs build issue +- Fix web assets issue for Web + +## 3.0.3+7 + +- Fix android 12 / api 31 issue. +- Fix macOs build issue +- Fix web assets issue for Web + +## 3.0.3+6 + +- Added DRM supports +- Fix playSpeed for WEB. + +## 3.0.3+5 + +- Added pitch controller + +## 3.0.3+4 + +- Updated dependencies +- Fix duplicate class issue +- Fix: assetsAudioPlayer.open playSpeed is not work + +## 3.0.3+3 + +- fix duration issue + +## 3.0.3+2 + +- update build number + +## 3.0.3+1 + +- fixed no function for stopForeground + +## 3.0.3 + +- Fix notification issue + +## 3.0.2 + +- Fix version issue + +## 3.0.1 + +- Fix web player + +## 3.0.0 + +- Fix some issues +- Migrate to null safety + +## 2.0.15 + +- update android 30 and fixed local assets issue +- should fix android alarm manager issue + +## 2.0.14 + +- update packages + +## 2.0.13+9 + +- fix opening multiple audio player. + +## 2.0.13+8 + +- fix opening multiple audio player. + +## 2.0.13+7 + +- fix version conflicts + +## 2.0.13+6 + +- fix android crash issue + +## 2.0.13+5 + +- fix opened multiple instance for android problem. + +## 2.0.13+2 + +- fixed some issues on ios +- fix crash issue on android + +## 2.0.13+1 + +- fixed some innues on macos/ios + +## 2.0.12 + +- Fixed AudioType.network networkHeaders +- Improve documentation +- CustomPrevIcon fixed + +## 2.0.9+2 + +- Renamed PhoneCallStrategy to AudioFocusStrategy +- Allow on android to resume native players after focus lost + +## 2.0.8+5 + +- Added Android HeadPhoneStrategy +- Fix local path file uri (android) +- Added open multiple calls protection +- Open uri content on androids + +## 2.0.6+7 + +- Cache now use `http` instead of `dio` +- Added live tag on notification for LiveStream play (ios) +- Added audio session id (android only) + +## 2.0.5+7 + +- Added custom error handling (beta) +- Dispose is now a future +- Fixed playlist insert / replace + +## 2.0.5 + +- Added Cache management (beta), with Audio.network(url, cached: true) + +## 2.0.4+2 + +- Added HLS, Dash, SmoothStream support on Android +- Added `laylist.replaceAt` method + +## 2.0.3+6 + +- ExoPlayer network now set `allowCrossProtocolRedirect=true` by default +- Fixed notification hide on livestream pause (android) +- Added custom icons for android from drawable names +- Fixed notification texts on Samsung devices + +## 2.0.3+1 + +- Added custom notification icons for Android (in AndroidManifest.xml) +- Fixed `seek` and `seekBy` not working on the web +- `PlayList.startIndex` is now mutable +- Stop player then call `play` reopen it at `playlist.startIndex` +- Increased buffer size on android/exoplayer +- Added keepLoopMode on prev/next + +## 2.0.2 + +- Breaking change : `loop` boolean now enumerate 3 values : `none`, `single` and `playlist` + +## 2.0.1+9 + +- Added `.showNotification = true/false` to hide dynamically displayed notification +- Added custom action on notif click(android) +- Added `isBuffering` to `RealtimePlayingInfos` +- Added `AssetsAudioPlayerGroup` (beta) +- Added Headers in `Audio.network` & `Audio.liveStream` + +## 2.0.1 + +- Added `.playerState` (play/pause/stop) +- Stop now ping finish listeners + +## 2.0.0+6 + +- Added MacOS support +- Fixed gapeless loop (single audio) +- Fixed audio file notification + +## 1.7.0 + +- Fixed bluetooth on android on some devices +- Fallback to android native MediaPlayer if exoplayer can't read the file +- Added `audio.updateMetas` to update notification content after creation +- Android Seekbar notification is now optional +- Android usable notification Seekbar +- Added stop custom notification action + +## 1.6.3 + +- Custom notification icon (android) +- Custom notification actions +- Fixed notification close on android +- Fixed android auto-focus +- Added playInBackground mode +- Added shuffle + +## 1.6.1 + +- Playlist is now mutable, we can add audios after creation +- renamed `ReadingPlaylist get playlist` to `ReadingPlaylist get readingPlaylist` +- added `Playlist get playlist` + +## 1.6.0+4 + +- Fixed playlist issue on android +- Fixed issue on bluetooth android play/pause +- Fixed PlayerBuilder currentPosition +- Added extra map into audio + +## 1.6.0 + +- Added some checks on swift code +- Fixed totalDuration or liveStream +- Fixed ios notifications +- Added bluetooth headset actions (play/pause/next/prev/stop) + +## 1.5.0 + +- Added `Audio.liveStream(url)` +- Fixed notification image from assets on android +- Fixed android notification actions on playlist +- Added `AudioWidget` + +## 1.4.7 + +- added `package` on assets audios (& notif images) +- all methods return Future +- open can throw an exception if the url is not found + +## 1.4.6+1 + +- fixed android notifications actions +- refactored package, added `src/` and `package` keyword +- added player_builders + +## 1.4.5 + +- fixed implementation of local file play on iOS + +## 1.4.4 + +- Added notifications on android + +## 1.4+3+6 + +- Beta fix for audio focus + +## 1.4+3+5 + +- Beta implementation of local file play on iOS + +## 1.4.3+4 + +- Moved to last flutter version `>=1.12.13+hotfix.6` +- Implemented new android `FlutterPlugin` +- Stop all players while getting a phone call +- Added `playspeed` as optional parameter on on open() + +## 1.4.2+1 + +- Moved to android ExoPlayer +- Added `playSpeed` (beta) +- Added `forwardRewind` (beta) +- Added `seekBy` + +## 1.4.0+1 + +- Bump gradle versions : `wrapper`=(5.4.1-all) `build:gradle`=(3.5.3) + +## 1.4.0 + +- Added `respectSilentMode` as open optional argument +- Added `showNotification` on iOS to map with MPNowPlayingInfoCenter (default: false) +- Added `metas` on audios (title, artist, ...) for notifications +- Use new plugin build format for iOS + +## 1.3.9 + +- Empty constructor now create a new player +- Added factory AssetsAudioPlayer.withId() +- Added `playAndForget` witch create, open, play & dispose the player on finish +- Added AssetsAudioPlayer.allPlayers() witch returns a map of all players +- Reworked the android player + +## 1.3.8+1 + +- Added `seek` as optional parameter on `open` method + +## 1.3.8 + +- Fully rebased the web support on html.AudioElement (instead of howler) +- Fully rebases the ios support on AvPlayer (instead of AvAudioPlayer) +- Added support for network audios with `.open(Audio.network(url))` on Android/ios/web + +## 1.3.7+1 + +- Added `RealtimePlayingInfos` stream + +## 1.3.6+1 + +- Added volume as optional parameter on open() + +## 1.3.6 + +- Extracted web support to assets_audio_player_web: 1.3.6 + +## 1.3.5+1 + +- Volume does not reset anymore on looping audios + +## 1.3.4 + +- Fixed player on Android + +## 1.3.3 + +- Fixed build on Android & iOS + +## 1.3.2 + +- Rewritten the web support, using now https://github.com/florent37/flutter_web_howl + +## 1.3.1+2 + +- Upgraded RxDart dependency +- fixed lint issues +- lowerCamelCase AssetsAudioPlayer volumes consts + +## 1.3.1 + +- Fixed build on iOS + +## 1.3.0 + +- Added web support, works only on debug mode + +## 1.2.8 + +- Added constructors + +* AssetsAudioPlayer.newPlayer +* AssetsAudioPlayer(id: "PLAYER_ID") + +to create new players and play multiples songs in parallel + +the default constructor AssetsAudioPlayer() still works as usual + +## 1.2.7 + +- Added "volume" property (listen/set) + +## 1.2.6 + +- Added an "autoPlay" optional attribute to open methods + +## 1.2.5 + +- Compatible with Swift 5 + +## 1.2.4 + +- Added playlist + +## 1.2.3 + +- Added playlist (beta) + +## 1.2.1 + +- Added looping setter/getter + +## 1.2.0 + +- Upgraded RxDart to 0.23.1 +- Fixed assets playing on iOS +- Fixed playing location on Android + +## 0.0.1 + +- initial release. diff --git a/assets_audio_player_web/LICENSE b/assets_audio_player_web/LICENSE new file mode 100644 index 00000000..d9e3d01d --- /dev/null +++ b/assets_audio_player_web/LICENSE @@ -0,0 +1,201 @@ + 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 2019 Florent37 + + 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. diff --git a/assets_audio_player_web/README.md b/assets_audio_player_web/README.md new file mode 100644 index 00000000..a55c6f83 --- /dev/null +++ b/assets_audio_player_web/README.md @@ -0,0 +1,1022 @@ +# ๐ŸŽง assets_audio_player ๐Ÿ”Š + +[![pub package](https://img.shields.io/pub/v/assets_audio_player.svg)]( +https://pub.dartlang.org/packages/assets_audio_player) + + Awesome Flutter + + + + +[![Codemagic build status](https://api.codemagic.io/apps/5ed8002fe1907b001c67db52/5ed8002fe1907b001c67db51/status_badge.svg)](https://codemagic.io/apps/5ed8002fe1907b001c67db52/5ed8002fe1907b001c67db51/latest_build) +[![CodeFactor](https://www.codefactor.io/repository/github/florent37/flutter-assetsaudioplayer/badge)](https://www.codefactor.io/repository/github/florent37/flutter-assetsaudioplayer) + +Play music/audio stored in assets files (simultaneously) directly from Flutter (android / ios / web / macos). + +You can also use play audio files from **network** using their url, **radios/livestream** and **local files** + +**Notification can be displayed on Android & iOS, and bluetooth actions are handled** + +```yaml +flutter: + assets: + - assets/audios/ +``` + +```Dart +AssetsAudioPlayer.newPlayer().open( + Audio("assets/audios/song1.mp3"), + autoPlay: true, + showNotification: true, +); +``` + +[![sample1](./medias/sample1.png)](https://github.com/florent37/Flutter-AssetsAudioPlayer) +[![sample1](./medias/sample2.png)](https://github.com/florent37/Flutter-AssetsAudioPlayer) + +# ๐Ÿ“ฅ Import + +```yaml +dependencies: + assets_audio_player: ^2.0.13 + +or + +assets_audio_player: +git: +url: https://github.com/florent37/Flutter-AssetsAudioPlayer.git +ref: master + +ref can be latest commit id. +``` + +**Works with `flutter: ">=1.12.13+hotfix.6 <2.0.0"`, be sure to upgrade your sdk** + +You like the package ? buy me a kofi :) + +Buy Me a Coffee at ko-fi.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Audio SourceAndroidiOSWebMacOS
๐Ÿ—„๏ธ Asset file (asset path)โœ…โœ…โœ…โœ…
๐ŸŒ Network file (url)โœ…โœ…โœ…โœ…
๐Ÿ“ Local file (path)โœ…โœ…โœ…โœ…
๐Ÿ“ป Network LiveStream / radio (url)
(Default, HLS, Dash, SmoothStream)
โœ…โœ…โœ…โœ…
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureAndroidiOSWebMacOS
๐ŸŽถ Multiple playersโœ…โœ…โœ…โœ…
๐Ÿ’ฝ Open Playlistโœ…โœ…โœ…โœ…
๐Ÿ’ฌSystem notificationโœ…โœ…๐Ÿšซ๐Ÿšซ
๐ŸŽง Bluetooth actionsโœ…โœ…๐Ÿšซ๐Ÿšซ
๐Ÿ”• Respect System silent modeโœ…โœ…๐Ÿšซ๐Ÿšซ
๐Ÿ“ž Pause on phone callโœ…โœ…๐Ÿšซ๐Ÿšซ
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandsAndroidiOSWebMacOS
โ–ถ Playโœ…โœ…โœ…โœ…
โธ Pauseโœ…โœ…โœ…โœ…
โน Stopโœ…โœ…โœ…โœ…
โฉ Seek(position)โœ…โœ…โœ…โœ…
โชโฉ SeekBy(position)โœ…โœ…โœ…โœ…
โฉ Forward(speed)โœ…โœ…โœ…โœ…
โช Rewind(speed)โœ…โœ…โœ…โœ…
โญ Nextโœ…โœ…โœ…โœ…
โฎ Prevโœ…โœ…โœ…โœ…
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WidgetsAndroidiOSWebMacOS
๐Ÿฆ Audio Widgetโœ…โœ…โœ…โœ…
๐Ÿฆ Widget Buildersโœ…โœ…โœ…โœ…
๐Ÿฆ AudioPlayer Builders Extensionโœ…โœ…โœ…โœ…
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertiesAndroidiOSWebMacOS
๐Ÿ” Loopโœ…โœ…โœ…โœ…
๐Ÿ”€ Shuffleโœ…โœ…โœ…โœ…
๐Ÿ”Š get/set Volumeโœ…โœ…โœ…โœ…
โฉ get/set Play Speedโœ…โœ…โœ…โœ…
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ListenersAndroidiOSWebMacOS
๐Ÿฆป Listener onReady(completeDuration)โœ…โœ…โœ…โœ…
๐Ÿฆป Listener currentPositionโœ…โœ…โœ…โœ…
๐Ÿฆป Listener finishedโœ…โœ…โœ…โœ…
๐Ÿฆป Listener bufferingโœ…โœ…โœ…โœ…
๐Ÿฆป Listener volumeโœ…โœ…โœ…โœ…
๐ŸฆปListener Play Speedโœ…โœ…โœ…โœ…
+ +# ๐Ÿ“ Import assets files + +No needed to copy songs to a media cache, with assets_audio_player you can open them directly from the assets. + +1. Create an audio directory in your assets (not necessary named "audios") +2. Declare it inside your pubspec.yaml + +```yaml +flutter: + assets: + - assets/audios/ +``` + +## ๐Ÿ› ๏ธ Getting Started + +```Dart +final assetsAudioPlayer = AssetsAudioPlayer(); + +assetsAudioPlayer.open( + Audio("assets/audios/song1.mp3"), +); +``` + +You can also play *network songs* from *url* + +```Dart +final assetsAudioPlayer = AssetsAudioPlayer(); + +try { + await assetsAudioPlayer.open( + Audio.network("http://www.mysite.com/myMp3file.mp3"), + ); +} catch (t) { + //mp3 unreachable +} +``` + +*LiveStream / Radio* from *url* + +**The main difference with network, if you pause/play, on livestream it will resume to present duration** + +```Dart +final assetsAudioPlayer = AssetsAudioPlayer(); + +try { + await assetsAudioPlayer.open( + Audio.liveStream(MY_LIVESTREAM_URL), + ); +} catch (t) { + //stream unreachable +} +``` + +And play *songs from file* + +```Dart +//create a new player +final assetsAudioPlayer = AssetsAudioPlayer(); + +assetsAudioPlayer.open( + Audio.file(FILE_URI), +); +``` + +for file uri, please look at https://pub.dev/packages/path_provider + +```Dart +assetsAudioPlayer.playOrPause(); +assetsAudioPlayer.play(); +assetsAudioPlayer.pause(); +``` + +```Dart +assetsAudioPlayer.seek(Duration to); +assetsAudioPlayer.seekBy(Duration by); +``` + +```Dart +assetsAudioPlayer.forwardRewind(double speed); +//if positive, forward, if negative, rewind +``` + +```Dart +assetsAudioPlayer.stop(); +``` + + +# Notifications + + +[![notification](./medias/notification_android.png)](https://github.com/florent37/Flutter-AssetsAudioPlayer) + +[![notification](./medias/notification_iOS.png)](https://github.com/florent37/Flutter-AssetsAudioPlayer) + +on iOS, it will use `MPNowPlayingInfoCenter` + +1. Add metas inside your audio + +```dart +final audio = Audio("/assets/audio/country.mp3", + metas: Metas( + title: "Country", + artist: "Florent Champigny", + album: "CountryAlbum", + image: MetasImage.asset("assets/images/country.jpg"), //can be MetasImage.network + ), + ); +``` + +2. open with `showNotification: true` + +```dart +_player.open(audio, showNotification: true) +``` + +## Custom notification + +Custom icon (android only) + +### By ResourceName + +Make sur you added those icons inside your `android/res/drawable` **!!! not on flutter assets !!!!** + +```dart +await _assetsAudioPlayer.open( + myAudio, + showNotification: true, + notificationSettings: NotificationSettings( + customStopIcon: AndroidResDrawable(name: "ic_stop_custom"), + customPauseIcon: AndroidResDrawable(name:"ic_pause_custom"), + customPlayIcon: AndroidResDrawable(name:"ic_play_custom"), + customPrevIcon: AndroidResDrawable(name:"ic_prev_custom"), + customNextIcon: AndroidResDrawable(name:"ic_next_custom"), + ) + +``` + +And don't forget tell proguard to keep those resources for release mode + +(part Keeping Resources) + +https://sites.google.com/a/android.com/tools/tech-docs/new-build-system/resource-shrinking + +```xml + + + +``` + +### By Manifest + +1. Add your icon into your android's `res` folder (android/app/src/main/res) + +2. Reference this icon into your AndroidManifest (android/app/src/main/AndroidManifest.xml) + +```xml + +``` + +You can also change actions icons + +``` + + + + + +``` + +## Handle notification click (android) + +Add in main +```dart +AssetsAudioPlayer.setupNotificationsOpenAction((notification) { + //custom action + return true; //true : handled, does not notify others listeners + //false : enable others listeners to handle it +}); +``` + +Then if you want a custom action on widget + +```dart +AssetsAudioPlayer.addNotificationOpenAction((notification) { + //custom action + return false; //true : handled, does not notify others listeners + //false : enable others listeners to handle it +}); +``` + +## Custom actions + +You can enable/disable a notification action + +```dart +open(AUDIO, + showNotification: true, + notificationSettings: NotificationSettings( + prevEnabled: false, //disable the previous button + + //and have a custom next action (will disable the default action) + customNextAction: (player) { + print("next"); + } + ) + +) +``` + +## Update audio's metas / notification content + +After your audio creation, just call + +```dart +audio.updateMetas( + player: _assetsAudioPlayer, //add the player if the audio is actually played + title: "My new title", + artist: "My new artist", + //if I not provide a new album, it keep the old one + image: MetasImage.network( + //my new image url + ), +); +``` + +## Bluetooth Actions + +You have to enable notification to make them work + +Available remote commands : + +- Play / Pause +- Next +- Prev +- Stop + +## HeadPhone Strategy + +(Only for Android for now) + +while opening a song/playlist, add a strategy + +```dart +assetsAudioPlayer.open( + ... + headPhoneStrategy: HeadPhoneStrategy.pauseOnUnplug, + //headPhoneStrategy: HeadPhoneStrategy.none, //default + //headPhoneStrategy: HeadPhoneStrategy.pauseOnUnplugPlayOnPlug, +) +``` + +If you want to make it work on bluetooth too, you'll have to add the BLUETOOTH permission inside your AndroidManifest.xml + +```xml + +``` + +# โ›“ Play in parallel / simultaneously + +You can create new AssetsAudioPlayer using AssetsAudioPlayer.newPlayer(), +which will play songs in a different native Media Player + +This will enable to play two songs simultaneously + +You can have as many player as you want ! + +```dart +///play 3 songs in parallel +AssetsAudioPlayer.newPlayer().open( + Audio("assets/audios/song1.mp3") +); +AssetsAudioPlayer.newPlayer().open( + Audio("assets/audios/song2.mp3") +); + +//another way, with create, open, play & dispose the player on finish +AssetsAudioPlayer.playAndForget( + Audio("assets/audios/song3.mp3") +); +``` + +Each player has an unique generated `id`, you can retrieve or create them manually using + +```dart +final player = AssetsAudioPlayer.withId(id: "MY_UNIQUE_ID"); +``` + +# ๐Ÿ—„๏ธ Playlist +```Dart +assetsAudioPlayer.open( + Playlist( + audios: [ + Audio("assets/audios/song1.mp3"), + Audio("assets/audios/song2.mp3") + ] + ), + loopMode: LoopMode.playlist //loop the full playlist +); + +assetsAudioPlayer.next(); +assetsAudioPlayer.prev(); +assetsAudioPlayer.playlistPlayAtIndex(1); +``` + +## Audio Widget + +If you want a more flutter way to play audio, try the `AudioWidget` ! + +[![sample](./medias/audio_widget.gif)](https://github.com/florent37/Flutter-AssetsAudioPlayer) + +```dart +//inside a stateful widget + +bool _play = false; + +@override +Widget build(BuildContext context) { + return AudioWidget.assets( + path: "assets/audios/country.mp3", + play: _play, + child: RaisedButton( + child: Text( + _play ? "pause" : "play", + ), + onPressed: () { + setState(() { + _play = !_play; + }); + } + ), + onReadyToPlay: (duration) { + //onReadyToPlay + }, + onPositionChanged: (current, duration) { + //onPositionChanged + }, + ); +} +``` + +How to ๐Ÿ›‘ stop ๐Ÿ›‘ the AudioWidget ? + +Just remove the Audio from the tree ! +Or simply keep `play: false` + +## ๐ŸŽง Listeners + +All listeners exposes Streams +Using RxDart, AssetsAudioPlayer exposes some listeners as ValueObservable (Observable that provides synchronous access to the last emitted item); + +### ๐ŸŽต Current song +```Dart +//The current playing audio, filled with the total song duration +assetsAudioPlayer.current //ValueObservable + +//Retrieve directly the current played asset +final PlayingAudio playing = assetsAudioPlayer.current.value; + +//Listen to the current playing song +assetsAudioPlayer.current.listen((playingAudio){ + final asset = playingAudio.assetAudio; + final songDuration = playingAudio.duration; +}) +``` + +### โŒ› Current song duration + +```Dart +//Listen to the current playing song +final duration = assetsAudioPlayer.current.value.duration; +``` + +### โณ Current position (in seconds) + +```Dart +assetsAudioPlayer.currentPosition //ValueObservable + +//retrieve directly the current song position +final Duration position = assetsAudioPlayer.currentPosition.value; + +return StreamBuilder( + stream: assetsAudioPlayer.currentPosition, + builder: (context, asyncSnapshot) { + final Duration duration = asyncSnapshot.data; + return Text(duration.toString()); + }), +``` + +or use a PlayerBuilder ! + +```dart +PlayerBuilder.currentPosition( + player: _assetsAudioPlayer, + builder: (context, duration) { + return Text(duration.toString()); + } +) +``` + +or Player Builder Extension + +```dart +_assetsAudioPlayer.builderCurrentPosition( + builder: (context, duration) { + return Text(duration.toString()); + } +) +``` + +### โ–ถ IsPlaying +boolean observable representing the current mediaplayer playing state +```Dart +assetsAudioPlayer.isPlaying // ValueObservable + +//retrieve directly the current player state +final bool playing = assetsAudioPlayer.isPlaying.value; + +//will follow the AssetsAudioPlayer playing state +return StreamBuilder( + stream: assetsAudioPlayer.isPlaying, + builder: (context, asyncSnapshot) { + final bool isPlaying = asyncSnapshot.data; + return Text(isPlaying ? "Pause" : "Play"); + }), +``` + +or use a PlayerBuilder ! + +```dart +PlayerBuilder.isPlaying( + player: _assetsAudioPlayer, + builder: (context, isPlaying) { + return Text(isPlaying ? "Pause" : "Play"); + } +) +``` + +or Player Builder Extension + +```dart +_assetsAudioPlayer.builderIsPlaying( + builder: (context, isPlaying) { + return Text(isPlaying ? "Pause" : "Play"); + } +) +``` + +### ๐Ÿ”Š Volume + +Change the volume (between 0.0 & 1.0) +```Dart +assetsAudioPlayer.setVolume(0.5); +``` + +The media player can follow the system "volume mode" (vibrate, muted, normal) +Simply set the `respectSilentMode` optional parameter as `true` + +```dart +_player.open(PLAYABLE, respectSilentMode: true); +``` + +https://developer.android.com/reference/android/media/AudioManager.html?hl=fr#getRingerMode() + +https://developer.apple.com/documentation/avfoundation/avaudiosessioncategorysoloambient + + +Listen the volume + +```dart +return StreamBuilder( + stream: assetsAudioPlayer.volume, + builder: (context, asyncSnapshot) { + final double volume = asyncSnapshot.data; + return Text("volume : $volume"); + }), +``` + +or use a PlayerBuilder ! + +```dart +PlayerBuilder.volume( + player: _assetsAudioPlayer, + builder: (context, volume) { + return Text("volume : $volume"); + } +) +``` + +### โœ‹ Finished + +Called when the current song has finished to play, + +it gives the Playing audio that just finished + +```Dart +assetsAudioPlayer.playlistAudioFinished //ValueObservable + +assetsAudioPlayer.playlistAudioFinished.listen((Playing playing){ + +}) +``` + +Called when the complete playlist has finished to play + +```Dart +assetsAudioPlayer.playlistFinished //ValueObservable + +assetsAudioPlayer.playlistFinished.listen((finished){ + +}) +``` + +### ๐Ÿ” Looping + +```Dart +final LoopMode loopMode = assetsAudioPlayer.loop; +// possible values +// LoopMode.none : not looping +// LoopMode.single : looping a single audio +// LoopMode.playlist : looping the fyll playlist + +assetsAudioPlayer.setLoopMode(LoopMode.single); + +assetsAudioPlayer.loopMode.listen((loopMode){ + //listen to loop +}) + +assetsAudioPlayer.toggleLoop(); //toggle the value of looping +``` + + +# Error Handling + +By default, on playing error, it stop the audio + +BUT you can add a custom behavior + +```dart +_player.onErrorDo = (handler){ + handler.player.stop(); +}; +``` + +Open another audio + +```dart +_player.onErrorDo = (handler){ + handler.player.open(ANOTHER_AUDIO); +}; +``` + +Try to open again on same position + +```dart +_player.onErrorDo = (handler){ + handler.player.open( + handler.playlist.copyWith( + startIndex: handler.playlistIndex + ), + seek: handler.currentPosition + ); +}; +``` + +# Network Policies (android/iOS/macOS) + +Android only allow HTTPS calls, you will have an error if you're using HTTP, +don't forget to add INTERNET permission and seet `usesCleartextTraffic="true"` in your **AndroidManifest.xml** + +``` + + + + + ... + + +``` + +iOS only allow HTTPS calls, you will have an error if you're using HTTP, +don't forget to edit your **info.plist** and set `NSAppTransportSecurity` to `NSAllowsArbitraryLoads` + +``` +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + +``` + +To enable http calls on macOs, you have to add input/output calls capabilities into `info.plist` + +``` +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + +UIBackgroundModes + + audio + fetch + +com.apple.security.network.client + +``` + +and in your + +`Runner/DebugProfile.entitlements` + +add + +``` +com.apple.security.network.client + +``` + +Complete `Runner/DebugProfile.entitlements` + +``` + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + +``` + +# ๐ŸŽถ Musics + +All musics used in the samples came from https://www.freemusicarchive.org/ diff --git a/assets_audio_player_web/analysis_options.yaml b/assets_audio_player_web/analysis_options.yaml new file mode 100644 index 00000000..a3be6b82 --- /dev/null +++ b/assets_audio_player_web/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml \ No newline at end of file diff --git a/assets_audio_player_web/lib/assets_audio_player_web.dart b/assets_audio_player_web/lib/assets_audio_player_web.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/assets_audio_player_web/lib/assets_audio_player_web.dart @@ -0,0 +1 @@ + diff --git a/assets_audio_player_web/lib/generated/i18n.dart b/assets_audio_player_web/lib/generated/i18n.dart new file mode 100644 index 00000000..6d0e90c8 --- /dev/null +++ b/assets_audio_player_web/lib/generated/i18n.dart @@ -0,0 +1,130 @@ +// import 'dart:async'; + +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; + +// // ignore_for_file: non_constant_identifier_names +// // ignore_for_file: camel_case_types +// // ignore_for_file: prefer_single_quotes + +// // This file is automatically generated. DO NOT EDIT, all your changes would be lost. +// class S implements WidgetsLocalizations { +// const S(); + +// static S current; + +// static const GeneratedLocalizationsDelegate delegate = +// GeneratedLocalizationsDelegate(); + +// static S of(BuildContext context) => Localizations.of(context, S); + +// @override +// TextDirection get textDirection => TextDirection.ltr; +// } + +// class $en extends S { +// const $en(); +// } + +// class GeneratedLocalizationsDelegate extends LocalizationsDelegate { +// const GeneratedLocalizationsDelegate(); + +// List get supportedLocales { +// return const [ +// Locale('en', ''), +// ]; +// } + +// LocaleListResolutionCallback listResolution( +// {Locale fallback, bool withCountry = true}) { +// return (List locales, Iterable supported) { +// if (locales == null || locales.isEmpty) { +// return fallback ?? supported.first; +// } else { +// return _resolve(locales.first, fallback, supported, withCountry); +// } +// }; +// } + +// LocaleResolutionCallback resolution( +// {Locale fallback, bool withCountry = true}) { +// return (Locale locale, Iterable supported) { +// return _resolve(locale, fallback, supported, withCountry); +// }; +// } + +// @override +// Future load(Locale locale) { +// final String lang = getLang(locale); +// if (lang != null) { +// switch (lang) { +// case 'en': +// S.current = const $en(); +// return SynchronousFuture(S.current); +// default: +// // NO-OP. +// } +// } +// S.current = const S(); +// return SynchronousFuture(S.current); +// } + +// @override +// bool isSupported(Locale locale) => _isSupported(locale, true); + +// @override +// bool shouldReload(GeneratedLocalizationsDelegate old) => false; + +// /// +// /// Internal method to resolve a locale from a list of locales. +// /// +// Locale _resolve(Locale locale, Locale fallback, Iterable supported, +// bool withCountry) { +// if (locale == null || !_isSupported(locale, withCountry)) { +// return fallback ?? supported.first; +// } + +// final Locale languageLocale = Locale(locale.languageCode, ''); +// if (supported.contains(locale)) { +// return locale; +// } else if (supported.contains(languageLocale)) { +// return languageLocale; +// } else { +// final Locale fallbackLocale = fallback ?? supported.first; +// return fallbackLocale; +// } +// } + +// /// +// /// Returns true if the specified locale is supported, false otherwise. +// /// +// bool _isSupported(Locale locale, bool withCountry) { +// if (locale != null) { +// for (Locale supportedLocale in supportedLocales) { +// // Language must always match both locales. +// if (supportedLocale.languageCode != locale.languageCode) { +// continue; +// } + +// // If country code matches, return this locale. +// if (supportedLocale.countryCode == locale.countryCode) { +// return true; +// } + +// // If no country requirement is requested, check if this locale has no country. +// if (true != withCountry && +// (supportedLocale.countryCode == null || +// supportedLocale.countryCode.isEmpty)) { +// return true; +// } +// } +// } +// return false; +// } +// } + +// String getLang(Locale l) => l == null +// ? null +// : l.countryCode != null && l.countryCode.isEmpty +// ? l.languageCode +// : l.toString(); diff --git a/assets_audio_player_web/lib/web/abstract_web_player.dart b/assets_audio_player_web/lib/web/abstract_web_player.dart new file mode 100644 index 00000000..5ecc9e5b --- /dev/null +++ b/assets_audio_player_web/lib/web/abstract_web_player.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:web/web.dart' as web; +import 'package:flutter/services.dart'; + +/// Web Player +abstract class WebPlayer { + final MethodChannel channel; + + static const methodPosition = 'player.position'; + static const methodVolume = 'player.volume'; + static const methodPlaySpeed = 'player.playSpeed'; + static const methodFinished = 'player.finished'; + static const methodIsPlaying = 'player.isPlaying'; + static const methodIsBuffering = 'player.isBuffering'; + static const methodCurrent = 'player.current'; + static const methodForwardRewindSpeed = 'player.forwardRewind'; + + WebPlayer({required this.channel}); + + num get volume; + + set volume(num volume); + + num get playSpeed; + + set playSpeed(num playSpeed); + + bool get isPlaying; + + set isPlaying(bool value); + + num get currentPosition; + + void play(); + + void pause(); + + void stop(); + + String findAssetPath(String path, String audioType, {String? package}) { + if (audioType == 'network' || + audioType == 'liveStream' || + audioType == 'file') { + return path; + } + // in web, assets are packaged in a /assets/ folder + // if you want '/asset/3' as described in pubspec + // it will be in /assets/asset/3 + + /* for release mode, need to change the 'url', remove the /#/ and add /asset before */ + if (path.startsWith('/')) { + path = path.replaceFirst('/', ''); + } + if (package != null) { + path = 'packages/$package/$path'; + } + + path = ('${web.window.location.href.replaceAll('/#/', '')}/assets/$path'); + return path; + } + + Future open({ + required String path, + required String audioType, + bool autoStart = false, + double volume = 1, + double? seek, + double? playSpeed, + Map? networkHeaders, + String? package, + }); + + void seek({double to}); + + void forwardRewind(double speed); + + void loopSingleAudio(bool loop); +} diff --git a/assets_audio_player_web/lib/web/assets_audio_player_web.dart b/assets_audio_player_web/lib/web/assets_audio_player_web.dart new file mode 100644 index 00000000..21a6f68d --- /dev/null +++ b/assets_audio_player_web/lib/web/assets_audio_player_web.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:assets_audio_player_web/web/web_player_html.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'abstract_web_player.dart'; + +/// Web plugin +class AssetsAudioPlayerWebPlugin { + final Map _players = {}; + final BinaryMessenger messenger; + + AssetsAudioPlayerWebPlugin({required this.messenger}); + + WebPlayer _newPlayer(String id, MethodChannel channel) { + return WebPlayerHtml( + channel: channel, + ); + } + + WebPlayer _getOrCreate(String id) { + if (_players.containsKey(id)) { + return _players[id]!; + } else { + final newPlayer = _newPlayer( + id, + MethodChannel( + 'assets_audio_player/' + id, + const StandardMethodCodec(), + messenger, + )); + _players[id] = newPlayer; + return newPlayer; + } + } + + static void registerWith(Registrar registrar) { + final channel = MethodChannel( + 'assets_audio_player', + const StandardMethodCodec(), + registrar, + ); + final instance = AssetsAudioPlayerWebPlugin(messenger: registrar); + channel.setMethodCallHandler(instance.handleMethodCall); + } + + Future handleMethodCall(MethodCall call) async { + //print(call.method); + switch (call.method) { + case 'isPlaying': + final String id = call.arguments['id']; + return Future.value(_getOrCreate(id).isPlaying); + case 'play': + final String id = call.arguments['id']; + _getOrCreate(id).play(); + return Future.value(true); + case 'pause': + final String id = call.arguments['id']; + _getOrCreate(id).pause(); + return Future.value(true); + case 'stop': + final String id = call.arguments['id']; + _getOrCreate(id).stop(); + return Future.value(true); + case 'volume': + final String id = call.arguments['id']; + final double volume = (call.arguments['volume'] as num).toDouble(); + _getOrCreate(id).volume = volume; + return Future.value(true); + case 'playSpeed': + final String id = call.arguments['id']; + final double playSpeed = + (call.arguments['playSpeed'] as num).toDouble(); + _getOrCreate(id).playSpeed = playSpeed; + return Future.value(true); + case 'forwardRewind': + final String id = call.arguments['id']; + final double speed = (call.arguments['speed'] as num).toDouble(); + _getOrCreate(id).forwardRewind(speed); + return Future.value(true); + case 'loopSingleAudio': + final String id = call.arguments['id']; + final bool loop = call.arguments['loop']; + _getOrCreate(id).loopSingleAudio(loop); + return Future.value(true); + case 'seek': + final String id = call.arguments['id']; + final double to = (call.arguments['to'] as num).toDouble(); + _getOrCreate(id).seek(to: to); + return Future.value(true); + case 'open': + final String id = call.arguments['id']; + final String path = call.arguments['path']; + final String audioType = call.arguments['audioType']; + final double volume = (call.arguments['volume'] as num).toDouble(); + final double? seek = (call.arguments['seek'] as num?)?.toDouble(); + final double playSpeed = + (call.arguments['playSpeed'] as num).toDouble(); + final bool autoStart = call.arguments['autoStart'] ?? false; + final Map? networkHeaders = call.arguments['networkHeaders']; + final String? package = call.arguments['package']; + return _getOrCreate(id).open( + path: path, + audioType: audioType, + volume: volume, + seek: seek, + playSpeed: playSpeed, + autoStart: autoStart, + networkHeaders: networkHeaders, + package: package, + ); + } + } +} diff --git a/assets_audio_player_web/lib/web/web_player_html.dart b/assets_audio_player_web/lib/web/web_player_html.dart new file mode 100644 index 00000000..9d1a313d --- /dev/null +++ b/assets_audio_player_web/lib/web/web_player_html.dart @@ -0,0 +1,235 @@ +import 'dart:async'; + +import 'package:web/web.dart' as web; +import 'package:flutter/services.dart'; + +import 'abstract_web_player.dart'; + +/// Web Player +class WebPlayerHtml extends WebPlayer { + @override + WebPlayerHtml({required MethodChannel channel}) : super(channel: channel); + + StreamSubscription? _onEndListener; + StreamSubscription? _onCanPlayListener; + + void _clearListeners() { + _onEndListener?.cancel(); + _onCanPlayListener?.cancel(); + } + + web.HTMLAudioElement? _audioElement; + + @override + num get volume => _audioElement?.volume ?? 1.0; + + @override + set volume(num volume) { + _audioElement?.volume = volume; + channel.invokeMethod(WebPlayer.methodVolume, volume); + } + + @override + num get playSpeed => _audioElement?.playbackRate ?? 1.0; + + @override + set playSpeed(num playSpeed) { + _audioElement?.playbackRate = playSpeed; + channel.invokeMethod(WebPlayer.methodPlaySpeed, playSpeed); + } + + bool _isPlaying = false; + + @override + bool get isPlaying => _isPlaying; + + @override + set isPlaying(bool value) { + _isPlaying = value; + channel.invokeMethod(WebPlayer.methodIsPlaying, value); + if (value) { + _listenPosition(); + } else { + _stopListenPosition(); + } + } + + @override + num get currentPosition => _audioElement?.currentTime ?? 0; + + var __listenPosition = false; + + num? _durationMs; + num? _position; + + void _listenPosition() async { + __listenPosition = true; + await Future.doWhile(() { + final durationMs = (_audioElement?.duration ?? 0) * 1000; + if (durationMs != _durationMs) { + _durationMs = durationMs; + channel.invokeMethod( + WebPlayer.methodCurrent, + {'totalDurationMs': durationMs}, + ); + } + + if (_position != currentPosition) { + _position = currentPosition; + final positionMs = currentPosition * 1000; + channel.invokeMethod(WebPlayer.methodPosition, positionMs); + } + return Future.delayed(const Duration(milliseconds: 200)).then((value) { + return __listenPosition; + }); + }); + } + + void _stopListenPosition() { + __listenPosition = false; + } + + @override + void play() { + if (_audioElement != null) { + isPlaying = true; + forwardHandler?.stop(); + _audioElement?.play(); + } + } + + @override + void pause() { + if (_audioElement != null) { + isPlaying = false; + forwardHandler?.stop(); + _audioElement?.pause(); + } + } + + @override + void stop() { + forwardHandler?.stop(); + forwardHandler = null; + + _clearListeners(); + + if (_audioElement != null) { + isPlaying = false; + pause(); + _audioElement?.currentTime = 0; + channel.invokeMethod(WebPlayer.methodPosition, 0); + } + } + + @override + Future open({ + required String path, + required String audioType, + String? package, + bool autoStart = false, + double volume = 1, + double? seek, + double? playSpeed, + Map? networkHeaders, + }) async { + stop(); + _durationMs = null; + _position = null; + _audioElement = web.HTMLAudioElement(); + + // it seems html audielement cannot take networkHeaders :'( + + _onEndListener = _audioElement?.onEnded.listen((event) { + channel.invokeMethod(WebPlayer.methodFinished, true); + }); + + _onCanPlayListener = _audioElement?.onCanPlay.listen((event) { + if (autoStart) { + play(); + } + + this.volume = volume; + final durationMs = (_audioElement?.duration ?? 0) * 1000; + + if (durationMs != _durationMs) { + _durationMs = durationMs; + channel.invokeMethod( + WebPlayer.methodCurrent, + {'totalDurationMs': durationMs}, + ); + } + + if (seek != null) { + this.seek(to: seek); + } + + if (playSpeed != null) { + this.playSpeed = playSpeed; + } + + // single event + _onCanPlayListener?.cancel(); + _onCanPlayListener = null; + }); + + // The `src` of the _audioElement is the last property that is set, so all + // the listeners for the events that the plugin cares about are attached. + _audioElement!.src = findAssetPath( + path, + audioType, + package: package, + ); + } + + @override + void seek({double? to}) { + if (_audioElement != null && to != null) { + /// Explainer on the `/1000` + /// The value being sent down from the plugin + /// is in `milliseconds` and `AudioElement` uses seconds. + /// This is to convert it. + final toInSeconds = to / 1000; + _audioElement?.currentTime = toInSeconds; + } + } + + @override + void loopSingleAudio(bool loop) { + _audioElement?.loop = loop; + } + + void seekBy({required double by}) { + final current = currentPosition; + final to = current + by; + seek(to: to); + } + + ForwardHandler? forwardHandler; + @override + void forwardRewind(double speed) { + pause(); + channel.invokeMethod(WebPlayer.methodForwardRewindSpeed, speed); + forwardHandler?.stop(); + forwardHandler = ForwardHandler(); + _listenPosition(); // for this usecase, enable listen position + forwardHandler?.start(this, speed); + } +} + +class ForwardHandler { + bool _isEnabled = false; + static const _timelapse = 300; + + void start(WebPlayerHtml player, double speed) async { + _isEnabled = true; + while (_isEnabled) { + player.seekBy(by: speed * _timelapse); + await Future.delayed(const Duration(milliseconds: _timelapse)); + } + } + + void stop() { + _isEnabled = false; + } +} diff --git a/assets_audio_player_web/publish.sh b/assets_audio_player_web/publish.sh new file mode 100755 index 00000000..ddec5068 --- /dev/null +++ b/assets_audio_player_web/publish.sh @@ -0,0 +1,2 @@ +flutter format lib/ +pub publish --force \ No newline at end of file diff --git a/assets_audio_player_web/pubspec.yaml b/assets_audio_player_web/pubspec.yaml new file mode 100644 index 00000000..e558b8c8 --- /dev/null +++ b/assets_audio_player_web/pubspec.yaml @@ -0,0 +1,36 @@ +name: assets_audio_player_web +description: Web plugin for assets_audio_player, play music/audio stored in assets files directly from Flutter. +version: 3.1.2 +#author: Florent Champigny +homepage: https://github.com/florent37/Flutter-AssetsAudioPlayer + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + web: ">=0.5.1 <2.0.0" + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + web: + pluginClass: AssetsAudioPlayerWebPlugin + fileName: web/assets_audio_player_web.dart diff --git a/assets_audio_player_web/res/values/strings_en.arb b/assets_audio_player_web/res/values/strings_en.arb new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/assets_audio_player_web/res/values/strings_en.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/assets_audio_player_web/test/assets_audio_player_web_test.dart b/assets_audio_player_web/test/assets_audio_player_web_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/assets_audio_player_web/test/assets_audio_player_web_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/pubspec.yaml b/pubspec.yaml index df0211da..cdfb65b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,14 @@ dependencies: http: ">=0.13.0 <2.0.0" path_provider: ^2.0.8 + # The design on https://flutter.dev/go/federated-plugins was to leave + # this constraint as 'any'. We cannot do it right now as it fails pub publish + # validation, so we set a ^ constraint. + # TODO(amirh): Revisit this (either update this part in the design or the pub tool). + # https://github.com/flutter/flutter/issues/46264 + # assets_audio_player_web: ^3.0.0-nullsafety.0 + assets_audio_player_web: ^3.1.1 + dev_dependencies: flutter_test: sdk: flutter @@ -33,3 +41,5 @@ flutter: pluginClass: AssetsAudioPlayerPlugin macos: pluginClass: AssetsAudioPlayerPlugin + web: + default_package: assets_audio_player_web \ No newline at end of file