├── .gitattributes
├── .gitignore
├── .metadata
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
├── .classpath
├── .gitignore
├── .project
├── .settings
│ └── org.eclipse.buildship.core.prefs
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── settings.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── cc
│ │ └── dync
│ │ └── audio_manager
│ │ ├── AudioManagerPlugin.java
│ │ ├── ByteMediaDataSource.java
│ │ ├── MediaPlayerHelper.java
│ │ ├── MediaPlayerService.java
│ │ └── VolumeChangeObserver.java
│ └── res
│ ├── drawable
│ └── ic_launcher.png
│ ├── layout
│ └── layout_mediaplayer.xml
│ └── values
│ └── strings.xml
├── example
├── .gitignore
├── .metadata
├── README.md
├── android
│ ├── .gitignore
│ ├── .project
│ ├── .settings
│ │ └── org.eclipse.buildship.core.prefs
│ ├── app
│ │ ├── build.gradle
│ │ └── src
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ │ └── cc
│ │ │ │ │ └── dync
│ │ │ │ │ └── audio_manager_example
│ │ │ │ │ └── MainActivity.java
│ │ │ └── res
│ │ │ │ ├── drawable
│ │ │ │ └── launch_background.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ └── values
│ │ │ │ └── styles.xml
│ │ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ └── settings.gradle
├── assets
│ ├── aLIEz.jpg
│ ├── aLIEz.m4a
│ ├── ic_launcher.png
│ ├── xv.jpg
│ └── xv.mp3
├── ios
│ ├── .gitignore
│ ├── Flutter
│ │ ├── .last_build_id
│ │ ├── AppFrameworkInfo.plist
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Podfile
│ ├── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ └── Runner
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ │ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ └── Runner-Bridging-Header.h
├── lib
│ └── main.dart
├── pubspec.yaml
├── test
│ └── widget_test.dart
└── web
│ ├── favicon.png
│ ├── icons
│ ├── Icon-192.png
│ └── Icon-512.png
│ ├── index.html
│ └── manifest.json
├── ios
├── .gitignore
├── Assets
│ └── .gitkeep
├── Classes
│ ├── AudioManager.swift
│ ├── AudioManagerPlugin.h
│ ├── AudioManagerPlugin.m
│ └── SwiftAudioManagerPlugin.swift
└── audio_manager.podspec
├── lib
├── audio_manager.dart
├── audio_manager_web.dart
└── src
│ ├── AudioInfo.dart
│ └── AudioType.dart
├── pubspec.yaml
├── screenshots
├── android.png
├── android2.png
├── iOS.png
└── iOS2.jpeg
└── test
└── audio_manager_test.dart
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.java linguist-language=Dart
2 | *.kt linguist-language=Dart
3 | *.swift linguist-language=Dart
4 | *.h linguist-language=Dart
5 | *.m linguist-language=Dart
6 | *.js linguist-language=Dart
7 | *.md linguist-language=Dart
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | *.lock
7 | .DS_Store
8 | .atom/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .flutter-plugins-dependencies
13 |
14 | # IntelliJ related
15 | *.iml
16 | *.ipr
17 | *.iws
18 | .idea/
19 |
20 | # The .vscode folder contains launch configuration and tasks you configure in
21 | # VS Code which you may wish to be included in version control, so this line
22 | # is commented out by default.
23 | #.vscode/
24 |
25 | # Flutter/Dart/Pub related
26 | **/doc/api/
27 | .dart_tool/
28 | .flutter-plugins
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Android related
35 | **/android/**/gradle-wrapper.jar
36 | **/android/.gradle
37 | **/android/captures/
38 | **/android/gradlew
39 | **/android/gradlew.bat
40 | **/android/local.properties
41 | **/android/**/GeneratedPluginRegistrant.java
42 |
43 | # iOS/XCode related
44 | **/ios/**/*.mode1v3
45 | **/ios/**/*.mode2v3
46 | **/ios/**/*.moved-aside
47 | **/ios/**/*.pbxuser
48 | **/ios/**/*.perspectivev3
49 | **/ios/**/*sync/
50 | **/ios/**/.sconsign.dblite
51 | **/ios/**/.tags*
52 | **/ios/**/.vagrant/
53 | **/ios/**/DerivedData/
54 | **/ios/**/Icon?
55 | **/ios/**/Pods/
56 | **/ios/**/.symlinks/
57 | **/ios/**/profile
58 | **/ios/**/xcuserdata
59 | **/ios/.generated/
60 | **/ios/Flutter/App.framework
61 | **/ios/Flutter/Flutter.framework
62 | **/ios/Flutter/Generated.xcconfig
63 | **/ios/Flutter/app.flx
64 | **/ios/Flutter/app.zip
65 | **/ios/Flutter/flutter_assets/
66 | **/ios/Flutter/flutter_export_environment.sh
67 | **/ios/ServiceDefinitions.json
68 | **/ios/Runner/GeneratedPluginRegistrant.*
69 |
70 | # Exceptions to above rules.
71 | !**/ios/**/default.mode1v3
72 | !**/ios/**/default.mode2v3
73 | !**/ios/**/default.pbxuser
74 | !**/ios/**/default.perspectivev3
75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
76 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff
8 | channel: stable
9 |
10 | project_type: plugin
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.8.2
2 | - fix `playOrPause` method error
3 | - Add support for loading covers from the app's data dir on Android
4 |
5 | ## 0.8.1
6 | - update to flutter 2.0
7 | - fix to null safety
8 |
9 | ## 0.7.3
10 | - fix ios replay
11 | - support web
12 | - update android notification text to english
13 |
14 | ## 0.5.7+1
15 | - Fix Crush while playing androin audio in offline [#28](https://github.com/jeromexiong/audio_manager/issues/28)
16 | - Fix play local files [#37](https://github.com/jeromexiong/audio_manager/issues/37)
17 | - Fix Repeat one [#40](https://github.com/jeromexiong/audio_manager/issues/40)
18 | - Fix #36, #43, #45, #47
19 | - Fix stop and play
20 |
21 | ## 0.5.5+3
22 |
23 | - Play audio immediately
24 | - `setRate` method to fix num conversion double
25 | - Fix iOS remote Control previous/next error
26 | - Fix iOS remove lock screen control from notification center
27 |
28 | ## 0.5.4+1
29 |
30 | - Fix loading local file crash
31 |
32 | ## 0.5.4
33 |
34 | - fix ios autoplay
35 | - fix ios remote control of pre/next play event
36 |
37 | ## 0.5.3+3
38 |
39 | - fix out of bounds
40 | - fix repeated problem of notification service
41 | - replace android notification chinese text to icon
42 |
43 | ## 0.5.3
44 |
45 | - fix iOS stuck
46 | - add `toPlay` and `toPause` method
47 | - add interrupt pause
48 |
49 | ## 0.5.2+1
50 |
51 | - remove `AndroidManifast.xml` redundant configuration
52 | - Optimize playback status updates
53 |
54 | ## 0.5.1+5
55 |
56 | - Supports playing local directory media files
57 | - add seek completed callback
58 | - Add volume control & volume changed callback
59 |
60 | ## 0.5.1
61 |
62 | - Add internal playlist management
63 |
64 | ## 0.3.1+1
65 |
66 | - Fix iOS remote command error
67 |
68 | ## 0.3.1
69 |
70 | - Add `auto` attribute whether to play automatically, default is true
71 | - Optimize the time type
72 | - Fix repeat callbacks
73 |
74 | ## 0.2.1+hotfix.2
75 |
76 | - Fix iOS loop playback
77 |
78 | ## 0.2.1
79 |
80 | - Add cache hint
81 | - Fix start and stop error
82 | - List loop playback
83 |
84 | ## 0.1.5
85 |
86 | - Optimization error prompt.
87 |
88 | ## 0.1.4
89 |
90 | - Fix pub.dev support.
91 | - Add method to get playback info.
92 |
93 | ## 0.1.3
94 |
95 | - Fix Initialize before playing.
96 |
97 | ## 0.1.2
98 |
99 | - Fix ios timeupdate.
100 | - Customize the style of the slider of the demo
101 |
102 | ## 0.1.1
103 |
104 | - update change log
105 |
106 | ## 0.0.2
107 |
108 | - update `seekTo`, `rate`, `onEvents` callback handle.
109 | - update demo
110 |
111 | ## 0.0.1
112 |
113 | - Initial version, created by Jerome Xiong
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jerome Xiong
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # audio_manager
2 | [](https://pub.dartlang.org/packages/audio_manager)
3 |
4 | A flutter plugin for music playback, including notification handling.
5 | > This plugin is developed for iOS based on AVPlayer, while android is based on mediaplayer
6 |
7 | 


8 |
9 | ## iOS
10 | Add the following permissions in the `info.plist` file
11 | ```xml
12 | UIBackgroundModes
13 |
14 | audio
15 |
16 | NSAppTransportSecurity
17 |
18 | NSAllowsArbitraryLoads
19 |
20 |
21 | ```
22 | - ⚠️ Some methods are invalid in the simulator, please use the real machine
23 |
24 | ## Android
25 | Since `Android9.0 (API 28)`, the application disables HTTP plaintext requests by default. To allow requests, add `android:usesCleartextTraffic="true"` in `AndroidManifest.xml`
26 | ```xml
27 |
32 | ```
33 | - ⚠️ Android minimum supported version 23 `(app/build.gradle -> minSdkVersion: 23)`
34 | - ⚠️ Android minimum supported Gradle version is 5.4.1 `(gradle-wrapper.properties -> gradle-5.4.1-all.zip)`
35 |
36 | ## How to use?
37 | The `audio_manager` plugin is developed in singleton mode. You only need to get`AudioManager.instance` in the method to quickly start using it.
38 |
39 | ## Quick start
40 | you can use local `assets`, `directory file` or `network` resources
41 |
42 | ```dart
43 | // Initial playback. Preloaded playback information
44 | AudioManager.instance
45 | .start(
46 | "assets/audio.mp3",
47 | // "network format resource"
48 | // "local resource (file://${file.path})"
49 | "title",
50 | desc: "desc",
51 | // cover: "network cover image resource"
52 | cover: "assets/ic_launcher.png")
53 | .then((err) {
54 | print(err);
55 | });
56 |
57 | // Play or pause; that is, pause if currently playing, otherwise play
58 | AudioManager.instance.playOrPause()
59 |
60 | // events callback
61 | AudioManager.instance.onEvents((events, args) {
62 | print("$events, $args");
63 | }
64 | ```
65 |
--------------------------------------------------------------------------------
/android/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/android/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | audio_manager
4 | Project android_____ created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | org.eclipse.buildship.core.gradleprojectbuilder
15 |
16 |
17 |
18 |
19 |
20 | org.eclipse.jdt.core.javanature
21 | org.eclipse.buildship.core.gradleprojectnature
22 |
23 |
24 |
--------------------------------------------------------------------------------
/android/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | arguments=
2 | auto.sync=false
3 | build.scans.enabled=false
4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.0))
5 | connection.project.dir=
6 | eclipse.preferences.version=1
7 | gradle.user.home=
8 | java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home
9 | jvm.arguments=
10 | offline.mode=false
11 | override.workspace.settings=true
12 | show.console.view=true
13 | show.executions.view=true
14 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | group 'cc.dync.audio_manager'
2 | version '1.0'
3 |
4 | buildscript {
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 |
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.5.3'
12 | }
13 | }
14 |
15 | rootProject.allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | }
21 |
22 | apply plugin: 'com.android.library'
23 |
24 | android {
25 | compileSdkVersion 28
26 |
27 | defaultConfig {
28 | minSdkVersion 23
29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
30 | }
31 | lintOptions {
32 | disable 'InvalidPackage'
33 | }
34 | compileOptions {
35 | sourceCompatibility = 1.8
36 | targetCompatibility = 1.8
37 | }
38 | dependencies {
39 | implementation 'com.google.android.exoplayer:exoplayer:2.11.1'
40 | implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.1'
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'audio_manager'
2 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/android/src/main/java/cc/dync/audio_manager/AudioManagerPlugin.java:
--------------------------------------------------------------------------------
1 | package cc.dync.audio_manager;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.NonNull;
7 |
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | import io.flutter.embedding.engine.plugins.FlutterPlugin;
12 | import io.flutter.plugin.common.MethodCall;
13 | import io.flutter.plugin.common.MethodChannel;
14 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
15 | import io.flutter.plugin.common.MethodChannel.Result;
16 | import io.flutter.plugin.common.PluginRegistry.Registrar;
17 |
18 | /**
19 | * AudioManagerPlugin
20 | */
21 | public class AudioManagerPlugin implements FlutterPlugin, MethodCallHandler, VolumeChangeObserver.VolumeChangeListener {
22 |
23 | private static AudioManagerPlugin instance;
24 | private Context context;
25 | private MethodChannel channel;
26 | private MediaPlayerHelper helper;
27 | private VolumeChangeObserver volumeChangeObserver;
28 |
29 | private static FlutterAssets flutterAssets;
30 | private static Registrar registrar;
31 |
32 | private static synchronized AudioManagerPlugin getInstance() {
33 | if (instance == null) {
34 | instance = new AudioManagerPlugin();
35 | }
36 | return instance;
37 | }
38 |
39 | public AudioManagerPlugin() {
40 | if (instance == null) {
41 | instance = this;
42 | }
43 | }
44 |
45 | @Override
46 | public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
47 | final MethodChannel channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "audio_manager");
48 |
49 | channel.setMethodCallHandler(getInstance());
50 | setup(flutterPluginBinding.getApplicationContext(), channel);
51 | AudioManagerPlugin.flutterAssets = flutterPluginBinding.getFlutterAssets();
52 | }
53 |
54 | // This static function is optional and equivalent to onAttachedToEngine. It
55 | // supports the old
56 | // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
57 | // plugin registration via this function while apps migrate to use the new
58 | // Android APIs
59 | // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
60 | //
61 | // It is encouraged to share logic between onAttachedToEngine and registerWith
62 | // to keep
63 | // them functionally equivalent. Only one of onAttachedToEngine or registerWith
64 | // will be called
65 | // depending on the user's project. onAttachedToEngine or registerWith must both
66 | // be defined
67 | // in the same class.
68 | public static void registerWith(Registrar registrar) {
69 | MethodChannel channel = new MethodChannel(registrar.messenger(), "audio_manager");
70 |
71 | channel.setMethodCallHandler(getInstance());
72 | instance.setup(registrar.context(), channel);
73 | AudioManagerPlugin.registrar = registrar;
74 | }
75 |
76 | private void setup(Context context, MethodChannel channel) {
77 | instance.context = context;
78 | instance.channel = channel;
79 |
80 | instance.helper = MediaPlayerHelper.getInstance(instance.context);
81 | setupPlayer();
82 | volumeChangeObserver = new VolumeChangeObserver(instance.context);
83 | volumeChangeObserver.setVolumeChangeListener(instance);
84 | volumeChangeObserver.registerReceiver();
85 | }
86 |
87 | private void setupPlayer() {
88 | MediaPlayerHelper helper = instance.helper;
89 | MethodChannel channel = instance.channel;
90 |
91 | helper.setOnStatusCallbackListener((status, args) -> {
92 | Log.v(TAG, "--" + status.toString());
93 | switch (status) {
94 | case ready:
95 | channel.invokeMethod("ready", helper.duration());
96 | break;
97 | case seekComplete:
98 | channel.invokeMethod("seekComplete", helper.position());
99 | break;
100 | case buffering:
101 | if (args.length == 0) return;
102 | Log.v(TAG, "网络缓冲:" + args[1] + "%");
103 |
104 | Map map = new HashMap();
105 | map.put("buffering", !helper.isPlaying());
106 | map.put("buffer", args[1]);
107 | channel.invokeMethod("buffering", map);
108 | break;
109 | case playOrPause:
110 | if (args.length == 0) return;
111 | channel.invokeMethod("playstatus", args[0]);
112 | break;
113 | case progress:
114 | if (args.length == 0) return;
115 | Log.v(TAG, "进度:" + args[0] + "%");
116 |
117 | Map map2 = new HashMap();
118 | map2.put("position", helper.position());
119 | map2.put("duration", helper.duration());
120 | channel.invokeMethod("timeupdate", map2);
121 | break;
122 | case error:
123 | Log.v(TAG, "播放错误:" + args[0]);
124 | channel.invokeMethod("error", args[0]);
125 | helper.stop();
126 | break;
127 | case next:
128 | channel.invokeMethod("next", null);
129 | break;
130 | case previous:
131 | channel.invokeMethod("previous", null);
132 | break;
133 | case ended:
134 | channel.invokeMethod("ended", null);
135 | break;
136 | case stop:
137 | channel.invokeMethod("stop", null);
138 | break;
139 | }
140 | });
141 | }
142 |
143 | private static final String TAG = "AudioManagerPlugin";
144 |
145 | @Override
146 | public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
147 | MediaPlayerHelper helper = instance.helper;
148 | switch (call.method) {
149 | case "getPlatformVersion":
150 | result.success("Android " + android.os.Build.VERSION.RELEASE);
151 | break;
152 | case "start":
153 | String url = call.argument("url");
154 | String title = call.argument("title");
155 | String desc = call.argument("desc");
156 | String cover = call.argument("cover");
157 |
158 | boolean isLocal = call.hasArgument("isLocal") ? call.argument("isLocal") : false;
159 | boolean isLocalCover = call.hasArgument("isLocalCover") ? call.argument("isLocalCover") : false;
160 | boolean isAuto = call.hasArgument("isAuto") ? call.argument("isAuto") : false;
161 | MediaPlayerHelper.MediaInfo info = new MediaPlayerHelper.MediaInfo(title, url);
162 | info.desc = desc;
163 | info.isAsset = isLocal;
164 | info.isAuto = isAuto;
165 | if (isLocal) {
166 | if (registrar != null) {
167 | info.url = registrar.lookupKeyForAsset(url);
168 | } else if (flutterAssets != null) {
169 | info.url = AudioManagerPlugin.flutterAssets.getAssetFilePathByName(url);
170 | }
171 | }
172 | info.cover = cover;
173 | if (isLocalCover) {
174 | if (registrar != null) {
175 | info.cover = registrar.lookupKeyForAsset(cover);
176 | } else if (flutterAssets != null) {
177 | if (helper.isDataDirFile(cover)) {
178 | info.cover = cover;
179 | } else {
180 | info.cover = AudioManagerPlugin.flutterAssets.getAssetFilePathByName(cover);
181 | }
182 | }
183 | }
184 |
185 | try {
186 | helper.start(info);
187 | } catch (Exception e) {
188 | result.success(e.getMessage());
189 | }
190 | break;
191 | case "playOrPause":
192 | helper.playOrPause();
193 | result.success(helper.isPlaying());
194 | break;
195 | case "play":
196 | helper.play();
197 | result.success(helper.isPlaying());
198 | break;
199 | case "pause":
200 | helper.pause();
201 | result.success(helper.isPlaying());
202 | break;
203 | case "stop":
204 | helper.stop();
205 | case "release":
206 | helper.release();
207 | break;
208 | case "updateLrc":
209 | helper.updateLrc(call.argument("lrc"));
210 | break;
211 | case "seekTo":
212 | try {
213 | int position = Integer.parseInt(call.argument("position").toString());
214 | helper.seekTo(position);
215 | } catch (Exception ex) {
216 | result.success("参数错误");
217 | }
218 | break;
219 | case "rate":
220 | try {
221 | double rate = Double.parseDouble(call.argument("rate").toString());
222 | helper.setSpeed((float) rate);
223 | } catch (Exception ex) {
224 | result.success("参数错误");
225 | }
226 | break;
227 | case "setVolume":
228 | try {
229 | double value = Double.parseDouble(call.argument("value").toString());
230 | instance.volumeChangeObserver.setVolume(value);
231 | } catch (Exception ex) {
232 | result.success("参数错误");
233 | }
234 | break;
235 | case "currentVolume":
236 | result.success(instance.volumeChangeObserver.getCurrentMusicVolume());
237 | break;
238 | default:
239 | result.notImplemented();
240 | break;
241 | }
242 | }
243 |
244 | @Override
245 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
246 | }
247 |
248 | @Override
249 | public void onVolumeChanged(double volume) {
250 | instance.channel.invokeMethod("volumeChange", volume);
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/android/src/main/java/cc/dync/audio_manager/ByteMediaDataSource.java:
--------------------------------------------------------------------------------
1 | package cc.dync.audio_manager;
2 |
3 | import android.media.MediaDataSource;
4 | import android.os.Build;
5 |
6 | import androidx.annotation.RequiresApi;
7 |
8 | /**
9 | * MediaPlayer播放字节流的工具类,可用于视频加密解密播放方案
10 | * Created by haide.yin(haide.yin@tcl.com) on 2019/12/13 8:56.
11 | */
12 | @RequiresApi(api = Build.VERSION_CODES.M)
13 | public class ByteMediaDataSource extends MediaDataSource {
14 |
15 | private volatile byte[] videoBuffer;
16 |
17 | public ByteMediaDataSource(byte[] videoBuffer) {
18 | this.videoBuffer = videoBuffer;
19 | }
20 |
21 | @Override
22 | public int readAt(long position, byte[] buffer, int offset, int size) {
23 | synchronized (videoBuffer) {
24 | int length = videoBuffer.length;
25 | if (position >= length) {
26 | return -1; // -1 indicates EOF
27 | }
28 | if (position + size > length) {
29 | size -= (position + size) - length;
30 | }
31 | System.arraycopy(videoBuffer, (int) position, buffer, offset, size);
32 | return size;
33 | }
34 | }
35 |
36 | @Override
37 | public long getSize() {
38 | synchronized (videoBuffer) {
39 | return videoBuffer.length;
40 | }
41 | }
42 |
43 | @Override
44 | public void close() {
45 |
46 | }
47 | }
--------------------------------------------------------------------------------
/android/src/main/java/cc/dync/audio_manager/MediaPlayerHelper.java:
--------------------------------------------------------------------------------
1 | package cc.dync.audio_manager;
2 |
3 | import android.content.Context;
4 | import android.content.res.AssetFileDescriptor;
5 | import android.content.res.AssetManager;
6 | import android.graphics.Bitmap;
7 | import android.graphics.BitmapFactory;
8 | import android.media.MediaDataSource;
9 | import android.media.MediaPlayer;
10 | import android.net.wifi.WifiManager;
11 | import android.os.Build;
12 | import android.os.Handler;
13 | import android.os.Message;
14 | import android.os.PowerManager;
15 | import android.util.Log;
16 | import android.view.SurfaceHolder;
17 | import android.view.SurfaceView;
18 |
19 | import androidx.annotation.RequiresApi;
20 |
21 | import java.io.BufferedInputStream;
22 | import java.io.FileInputStream;
23 | import java.io.IOException;
24 | import java.io.InputStream;
25 | import java.net.HttpURLConnection;
26 | import java.net.URL;
27 | import java.nio.file.FileSystems;
28 | import java.nio.file.Path;
29 | import java.util.Objects;
30 |
31 | /**
32 | * 多媒体播放
33 | */
34 | public class MediaPlayerHelper {
35 | private static final String TAG = MediaPlayerHelper.class.getSimpleName();
36 |
37 | private String[] ext = {".3gp", ".3GP", ".mp4", ".MP4", ".mp3", ".ogg", ".OGG", ".MP3", ".wav", ".WAV"};//定义我们支持的文件格式
38 | private Holder uiHolder;//UI的容器
39 | private Context context;
40 | private MediaInfo mediaInfo = new MediaInfo("title", null);
41 | private static MediaPlayerHelper instance;
42 | private int delaySecondTime = 1000;//进度回调间隔
43 | private boolean isHolderCreate = false;//SurfaceHolder是否准备好了
44 | private WifiManager.WifiLock wifiLock;
45 | private String curUrl = "";//当前初始化url
46 | private boolean isPrepare = false;
47 |
48 | static class MediaInfo {
49 | String title;
50 | /**
51 | * 资源路径
52 | * if isAsset: true (url 名字,带后缀,比如:text.mp3
53 | * else url is file path or network path
54 | */
55 | String url;
56 | /**
57 | * 资源描述
58 | */
59 | String desc;
60 | /**
61 | * 封面图地址
62 | */
63 | String cover;
64 | /**
65 | * 是否是通过Assets文件名播放Assets目录下的音频
66 | */
67 | boolean isAsset = false;
68 | /**
69 | * 是否是视频播放
70 | */
71 | boolean isVideo = false;
72 | /**
73 | * 是否是自动播放
74 | */
75 | boolean isAuto = true;
76 |
77 | MediaInfo(String title, String url) {
78 | this.title = title;
79 | this.url = url;
80 | }
81 | }
82 |
83 | /**
84 | * 状态枚举
85 | */
86 | public enum CallBackState {
87 | buffering("MediaPlayer--更新流媒体缓存状态"),
88 | next("next"),
89 | previous("previous"),
90 | playOrPause("playOrPause"),
91 | stop("stop"),
92 | ended("播放结束"),
93 | error("播放错误"),
94 | FORMAT_NOT_SUPPORT("音视频格式可能不支持"),
95 | INFO("播放开始"),
96 | ready("准备完毕"),
97 | progress("播放进度回调"),
98 | seekComplete("拖动完成"),
99 | VIDEO_SIZE_CHANGE("读取视频大小"),
100 | SURFACE_CREATE("SurfaceView--Holder创建"),
101 | SURFACE_DESTROY("SurfaceView--Holder销毁"),
102 | SURFACE_CHANGE("SurfaceView--Holder改变"),
103 | SURFACE_NULL("SurfaceView--还没初始化");
104 |
105 | private final String state;
106 |
107 | CallBackState(String state) {
108 | this.state = state;
109 | }
110 |
111 | public String toString() {
112 | return this.state;
113 | }
114 | }
115 |
116 | /**
117 | * 获得静态类
118 | *
119 | * @param context 引用
120 | * @return 实例
121 | */
122 | public static synchronized MediaPlayerHelper getInstance(Context context) {
123 | if (instance == null) {
124 | instance = new MediaPlayerHelper(context);
125 | }
126 | return instance;
127 | }
128 |
129 | /**
130 | * 获得流媒体对象
131 | *
132 | * @return 实例
133 | */
134 | public MediaPlayer getMediaPlayer() {
135 | return uiHolder.player;
136 | }
137 |
138 | /**
139 | * 设置播放进度时间间隔
140 | *
141 | * @param time 时间
142 | * @return 实例
143 | */
144 | public MediaPlayerHelper setProgressInterval(int time) {
145 | delaySecondTime = time;
146 | return instance;
147 | }
148 |
149 | private MediaPlayerService service;
150 |
151 | /**
152 | * 绑定服务
153 | *
154 | * @return 实例
155 | */
156 | private MediaPlayerHelper bindService() {
157 | MediaPlayerService.bindService((events, args) -> {
158 | switch (events) {
159 | case binder:
160 | service = (MediaPlayerService) args[0];
161 | service.updateNotification(isPlaying(), mediaInfo.title, mediaInfo.desc);
162 | if (mediaInfo.cover != null) {
163 | updateCover(mediaInfo.cover);
164 | }
165 | break;
166 | case playOrPause:
167 | playOrPause();
168 | break;
169 | case next:
170 | onStatusCallbackNext(CallBackState.next);
171 | break;
172 | case previous:
173 | onStatusCallbackNext(CallBackState.previous);
174 | break;
175 | case stop:
176 | release();
177 | break;
178 | }
179 | });
180 |
181 | keepAlive();
182 | return instance;
183 | }
184 |
185 | /**
186 | * 更新锁屏信息 必须在 bindService 之后调用
187 | */
188 | MediaPlayerHelper updateLrc(String desc) {
189 | if (service == null) return instance;
190 | service.updateNotification(isPlaying(), mediaInfo.title, desc);
191 | return instance;
192 | }
193 |
194 | MediaPlayerHelper updateCover(String url) {
195 | if (service == null) return instance;
196 | if (url.contains("http")) {
197 | new Thread(() -> {
198 | Bitmap bitmap = getBitmapFromUrl(url);
199 | service.updateCover(bitmap);
200 | }).start();
201 | return instance;
202 | }
203 | try {
204 | InputStream inputStream;
205 |
206 | if (isDataDirFile(url)) {
207 | inputStream = new FileInputStream(url);
208 | } else {
209 | AssetManager am = context.getAssets();
210 | inputStream = am.open(url);
211 | }
212 |
213 | service.updateCover(BitmapFactory.decodeStream(inputStream));
214 |
215 | } catch (IOException e) {
216 | onStatusCallbackNext(CallBackState.error, e.toString());
217 | }
218 | return instance;
219 | }
220 |
221 | /**
222 | * 播放音视频
223 | */
224 | void start(MediaInfo info) throws Exception {
225 | if (info.url.equals(curUrl)) {
226 | play();
227 | return;
228 | }
229 | this.mediaInfo = info;
230 | if (mediaInfo.url == null) throw new Exception("you must invoke setInfo method before");
231 |
232 | stop();
233 | uiHolder.player = new MediaPlayer();
234 | keepAlive();
235 | initPlayerListener();
236 |
237 | if (!mediaInfo.isVideo) bindService();
238 |
239 | if (mediaInfo.isAsset) {
240 | // if (!checkAvalable(mediaInfo.url)) {
241 | // onStatusCallbackNext(CallBackState.FORMAT_NOT_SUPPORT, mediaInfo.url);
242 | // return;
243 | // }
244 | if (mediaInfo.isVideo) {
245 | if (isHolderCreate) {
246 | beginPlayAsset(mediaInfo.url);
247 | } else {
248 | setOnHolderCreateListener(() -> beginPlayAsset(mediaInfo.url));
249 | }
250 | } else {
251 | beginPlayAsset(mediaInfo.url);
252 | }
253 | } else {
254 | if (mediaInfo.isVideo) {
255 | if (isHolderCreate) {
256 | beginPlayUrl(mediaInfo.url);
257 | } else {
258 | setOnHolderCreateListener(() -> beginPlayUrl(mediaInfo.url));
259 | }
260 | } else {
261 | beginPlayUrl(mediaInfo.url);
262 | }
263 | }
264 |
265 | curUrl = mediaInfo.url;
266 | isPrepare = false;
267 | }
268 |
269 | /**
270 | * 通过Assets文件名播放Assets目录下的音频
271 | *
272 | * @param assetName 名字,带后缀,比如:text.mp3
273 | */
274 | public void playAsset(String assetName, boolean isVideo) {
275 | // if (!checkAvalable(assetName)) {
276 | // onStatusCallbackNext(CallBackState.FORMAT_NOT_SUPPORT, assetName);
277 | // return;
278 | // }
279 | if (isVideo) {
280 | if (isHolderCreate) {
281 | beginPlayAsset(assetName);
282 | } else {
283 | setOnHolderCreateListener(() -> beginPlayAsset(assetName));
284 | }
285 | } else {
286 | beginPlayAsset(assetName);
287 | }
288 | }
289 |
290 | /**
291 | * 通过文件路径播放音视频
292 | *
293 | * @param path 路径
294 | */
295 | public void playUrl(final String path, boolean isVideo) {
296 | if (isVideo) {
297 | if (isHolderCreate) {
298 | beginPlayUrl(path);
299 | } else {
300 | setOnHolderCreateListener(() -> beginPlayUrl(path));
301 | }
302 | } else {
303 | beginPlayUrl(path);
304 | }
305 | }
306 |
307 |
308 | /**
309 | * 播放流视频
310 | *
311 | * @param videoBuffer videoBuffer
312 | */
313 | @RequiresApi(api = Build.VERSION_CODES.M)
314 | public void playByte(byte[] videoBuffer, boolean isVideo) {
315 | if (isVideo) {
316 | if (isHolderCreate) {
317 | beginPlayDataSource(new ByteMediaDataSource(videoBuffer));
318 | } else {
319 | setOnHolderCreateListener(() -> beginPlayDataSource(new ByteMediaDataSource(videoBuffer)));
320 | }
321 | } else {
322 | beginPlayDataSource(new ByteMediaDataSource(videoBuffer));
323 | }
324 | }
325 |
326 | /**
327 | * Whether the file is inside the application's data directory or not
328 | *
329 | * @param filePath filePath
330 | * @return true if the file is inside the application's data directory, false otherwise
331 | */
332 | public boolean isDataDirFile(String filePath) {
333 | Path path = FileSystems.getDefault().getPath(filePath);
334 | Path dataDirPath = FileSystems.getDefault().getPath(context.getApplicationInfo().dataDir);
335 | return path.startsWith(dataDirPath);
336 | }
337 |
338 | /**
339 | * @param speed 播放速率
340 | * @return 是否设置成功
341 | */
342 | boolean setSpeed(float speed) {
343 | if (!canPlay()) return false;
344 | //倍速设置,必须在23以上
345 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
346 | try {
347 | uiHolder.player.setPlaybackParams(uiHolder.player.getPlaybackParams().setSpeed(speed));
348 | uiHolder.player.pause();
349 | uiHolder.player.start();
350 | return true;
351 | } catch (Exception e) {
352 | Log.e(TAG, "setPlaySpeed: ", e);
353 | return false;
354 | }
355 | } else {
356 | Log.v(TAG, "对不起请升级手机系统至Android6.0及以上");
357 | return false;
358 | }
359 | }
360 |
361 | void play() {
362 | if (!canPlay()) return;
363 | if (isPlaying()) return;
364 | uiHolder.player.start();
365 | onStatusCallbackNext(CallBackState.playOrPause, isPlaying());
366 |
367 | if (service != null)
368 | service.updateNotification(isPlaying(), mediaInfo.title, null);
369 | }
370 |
371 | void pause() {
372 | if (!canPlay()) return;
373 | if (!isPlaying()) return;
374 | uiHolder.player.pause();
375 | onStatusCallbackNext(CallBackState.playOrPause, isPlaying());
376 |
377 | if (service != null)
378 | service.updateNotification(isPlaying(), mediaInfo.title, null);
379 | }
380 |
381 | void playOrPause() {
382 | if (!canPlay()) return;
383 | if (isPlaying()) {
384 | uiHolder.player.pause();
385 | } else {
386 | uiHolder.player.start();
387 | }
388 | onStatusCallbackNext(CallBackState.playOrPause, isPlaying());
389 |
390 | if (service != null)
391 | service.updateNotification(isPlaying(), mediaInfo.title, null);
392 | }
393 |
394 | private boolean canPlay() {
395 | if (!isPrepare) {
396 | Log.e(TAG, "媒体资源加载失败");
397 | onStatusCallbackNext(CallBackState.error, "媒体资源加载失败");
398 | }
399 | return isPrepare;
400 | }
401 |
402 | boolean isPlaying() {
403 | if (uiHolder.player == null) return false;
404 | return uiHolder.player.isPlaying();
405 | }
406 |
407 | int position() {
408 | if (uiHolder.player == null) return 0;
409 | return uiHolder.player.getCurrentPosition();
410 | }
411 |
412 | int duration() {
413 | if (uiHolder.player == null) return 0;
414 | return uiHolder.player.getDuration();
415 | }
416 |
417 | boolean seekTo(int position) {
418 | if (uiHolder.player == null) return false;
419 | uiHolder.player.seekTo(position);
420 | return true;
421 | }
422 |
423 | /**
424 | * 停止资源
425 | */
426 | public void stop() {
427 | if (uiHolder.player != null) {
428 | uiHolder.player.release();
429 | uiHolder.player = null;
430 | }
431 | onStatusCallbackNext(CallBackState.stop);
432 | refress_time_handler.removeCallbacks(refress_time_Thread);
433 |
434 | curUrl = "";
435 | isPrepare = false;
436 | }
437 |
438 | /**
439 | * 释放资源
440 | */
441 | public void release() {
442 | stop();
443 | MediaPlayerService.unBind(context);
444 |
445 | if (wifiLock != null && wifiLock.isHeld())
446 | wifiLock.release();
447 | }
448 |
449 | // /**
450 | // * 重新创建MediaPlayer
451 | // */
452 | // public void reCreateMediaPlayer() {
453 | // if (uiHolder.player != null) {
454 | // if (uiHolder.player.isPlaying()) {
455 | // uiHolder.player.stop();
456 | // }
457 | // uiHolder.player.release();
458 | // uiHolder.player = new MediaPlayer();
459 | // } else {
460 | // uiHolder.player = new MediaPlayer();
461 | // }
462 | // initPlayerListener();
463 | // }
464 |
465 | /**
466 | * 设置SurfaceView
467 | *
468 | * @param surfaceView 控件
469 | * @return 实例
470 | */
471 | public MediaPlayerHelper setSurfaceView(SurfaceView surfaceView) {
472 | if (surfaceView == null) {
473 | onStatusCallbackNext(CallBackState.SURFACE_NULL, uiHolder.player);
474 | } else {
475 | uiHolder.surfaceView = surfaceView;
476 | uiHolder.surfaceHolder = uiHolder.surfaceView.getHolder();
477 | uiHolder.surfaceHolder.addCallback(new SurfaceHolder.Callback() {
478 | @Override
479 | public void surfaceCreated(SurfaceHolder holder) {
480 | isHolderCreate = true;
481 | if (uiHolder.player != null && holder != null) {
482 | //解决部分机型/电视播放的时候有声音没画面的情况
483 | if (uiHolder.surfaceView != null) {
484 | uiHolder.surfaceView.post(() -> {
485 | holder.setFixedSize(uiHolder.surfaceView.getWidth(), uiHolder.surfaceView.getHeight());
486 | uiHolder.player.setDisplay(holder);
487 | });
488 | }
489 | }
490 | onStatusCallbackNext(CallBackState.SURFACE_CREATE, holder);
491 | onHolderCreateNext();
492 | }
493 |
494 | @Override
495 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
496 | onStatusCallbackNext(CallBackState.SURFACE_CHANGE, format, width, height);
497 | }
498 |
499 | @Override
500 | public void surfaceDestroyed(SurfaceHolder holder) {
501 | isHolderCreate = false;
502 | onStatusCallbackNext(CallBackState.SURFACE_DESTROY, holder);
503 | }
504 | });
505 | }
506 | return instance;
507 | }
508 |
509 | /**
510 | * 构造函数
511 | */
512 | private MediaPlayerHelper(Context context) {
513 | if (instance == null) {
514 | instance = this;
515 | }
516 | this.context = context;
517 | this.uiHolder = new Holder();
518 | MediaPlayerService.registerReceiver(context);
519 | // uiHolder.player = new MediaPlayer();
520 | // keepAlive();
521 | // initPlayerListener();
522 | }
523 |
524 | /**
525 | * 时间监听
526 | */
527 | private void initPlayerListener() {
528 | uiHolder.player.setOnCompletionListener(mp -> {
529 | onStatusCallbackNext(CallBackState.progress, 100);
530 | onStatusCallbackNext(CallBackState.ended, mp);
531 | });
532 | uiHolder.player.setOnErrorListener((mp, what, extra) -> {
533 | String errorString = "what:" + what + " extra:" + extra;
534 | onStatusCallbackNext(CallBackState.error, errorString);
535 | return false;
536 | });
537 | uiHolder.player.setOnInfoListener((mp, what, extra) -> {
538 | onStatusCallbackNext(CallBackState.INFO, mp, what, extra);
539 | return false;
540 | });
541 | uiHolder.player.setOnPreparedListener(mp -> {
542 | try {
543 | if (uiHolder.surfaceView != null) {
544 | //解决部分机型/电视播放的时候有声音没画面的情况
545 | uiHolder.surfaceView.post(() -> {
546 | uiHolder.surfaceHolder.setFixedSize(uiHolder.surfaceView.getWidth(), uiHolder.surfaceView.getHeight());
547 | //设置预览区域
548 | uiHolder.player.setDisplay(uiHolder.surfaceHolder);
549 | });
550 | }
551 | isPrepare = true;
552 | if (mediaInfo.isAuto) {
553 | uiHolder.player.start();
554 | }
555 | refress_time_handler.postDelayed(refress_time_Thread, delaySecondTime);
556 | } catch (Exception e) {
557 | onStatusCallbackNext(CallBackState.error, e.toString());
558 | }
559 | String holderMsg = "holder -";
560 | if (uiHolder.surfaceHolder != null) {
561 | holderMsg = holderMsg + " height:" + uiHolder.surfaceHolder.getSurfaceFrame().height();
562 | holderMsg = holderMsg + " width:" + uiHolder.surfaceHolder.getSurfaceFrame().width();
563 | }
564 | onStatusCallbackNext(CallBackState.ready, holderMsg);
565 | });
566 | uiHolder.player.setOnSeekCompleteListener(mp -> onStatusCallbackNext(CallBackState.seekComplete, mp));
567 | uiHolder.player.setOnVideoSizeChangedListener((mp, width, height) -> onStatusCallbackNext(CallBackState.VIDEO_SIZE_CHANGE, width, height));
568 | uiHolder.player.setOnBufferingUpdateListener((mp, percent) -> onStatusCallbackNext(CallBackState.buffering, mp, percent));
569 | }
570 |
571 | /**
572 | * 播放
573 | *
574 | * @param path 参数
575 | */
576 | private void beginPlayUrl(String path) {
577 | /*
578 | * 其实仔细观察优酷app切换播放网络视频时的确像是这样做的:先暂停当前视频,
579 | * 让mediaplayer与先前的surfaceHolder脱离“绑定”,当mediaplayer再次准备好要start时,
580 | * 再次让mediaplayer与surfaceHolder“绑定”在一起,显示下一个要播放的视频。
581 | * 注:MediaPlayer.setDisplay()的作用: 设置SurfaceHolder用于显示的视频部分媒体。
582 | */
583 | try {
584 | //Uri url = Uri.fromFile(new File(path));
585 | uiHolder.player.setDisplay(null);
586 | uiHolder.player.reset();
587 | uiHolder.player.setDataSource(path);
588 | uiHolder.player.prepareAsync();
589 | } catch (Exception e) {
590 | onStatusCallbackNext(CallBackState.error, e.toString());
591 | }
592 | }
593 |
594 | /**
595 | * 播放
596 | *
597 | * @param assetName 参数
598 | */
599 | private void beginPlayAsset(String assetName) {
600 | /*
601 | * 其实仔细观察优酷app切换播放网络视频时的确像是这样做的:先暂停当前视频,
602 | * 让mediaplayer与先前的surfaceHolder脱离“绑定”,当mediaplayer再次准备好要start时,
603 | * 再次让mediaplayer与surfaceHolder“绑定”在一起,显示下一个要播放的视频。
604 | * 注:MediaPlayer.setDisplay()的作用: 设置SurfaceHolder用于显示的视频部分媒体。
605 | */
606 | AssetManager assetMg = context.getAssets();
607 | try {
608 | uiHolder.assetDescriptor = assetMg.openFd(assetName);
609 | uiHolder.player.setDisplay(null);
610 | uiHolder.player.reset();
611 | uiHolder.player.setDataSource(uiHolder.assetDescriptor.getFileDescriptor(), uiHolder.assetDescriptor.getStartOffset(), uiHolder.assetDescriptor.getLength());
612 | uiHolder.player.prepareAsync();
613 | } catch (Exception e) {
614 | onStatusCallbackNext(CallBackState.error, e.toString());
615 | }
616 | }
617 |
618 | /**
619 | * 播放
620 | *
621 | * @param mediaDataSource 参数
622 | */
623 | @RequiresApi(api = Build.VERSION_CODES.M)
624 | private void beginPlayDataSource(MediaDataSource mediaDataSource) {
625 | /*
626 | * 其实仔细观察优酷app切换播放网络视频时的确像是这样做的:先暂停当前视频,
627 | * 让mediaplayer与先前的surfaceHolder脱离“绑定”,当mediaplayer再次准备好要start时,
628 | * 再次让mediaplayer与surfaceHolder“绑定”在一起,显示下一个要播放的视频。
629 | * 注:MediaPlayer.setDisplay()的作用: 设置SurfaceHolder用于显示的视频部分媒体。
630 | */
631 | try {
632 | uiHolder.player.setDisplay(null);
633 | uiHolder.player.reset();
634 | uiHolder.player.setDataSource(mediaDataSource);
635 | uiHolder.player.prepareAsync();
636 | } catch (Exception e) {
637 | onStatusCallbackNext(CallBackState.error, e.toString());
638 | }
639 | }
640 |
641 | /**
642 | * 检查是否可以播放
643 | *
644 | * @param path 参数
645 | * @return 结果
646 | */
647 | private boolean checkAvalable(String path) {
648 | boolean surport = false;
649 | for (String s : ext) {
650 | if (path.endsWith(s)) {
651 | surport = true;
652 | }
653 | }
654 | if (!surport) {
655 | onStatusCallbackNext(CallBackState.FORMAT_NOT_SUPPORT, uiHolder.player);
656 | return false;
657 | }
658 | return true;
659 | }
660 |
661 | private void keepAlive() {
662 | // 设置设备进入锁状态模式-可在后台播放或者缓冲音乐-CPU一直工作
663 | uiHolder.player.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
664 | // 当播放的时候一直让屏幕变亮
665 | // player.setScreenOnWhilePlaying(true);
666 |
667 | // 如果你使用wifi播放流媒体,你还需要持有wifi锁
668 | wifiLock = ((WifiManager) Objects.requireNonNull(context.getApplicationContext().getSystemService(Context.WIFI_SERVICE)))
669 | .createWifiLock(WifiManager.WIFI_MODE_FULL, "wifilock");
670 | wifiLock.acquire();
671 | }
672 |
673 | /**
674 | * 播放进度定时器
675 | */
676 | private Handler refress_time_handler = new Handler(){
677 | @Override
678 | public void handleMessage(Message msg) {
679 | switch (msg.what){
680 | case ERROR:
681 | onStatusCallbackNext(CallBackState.error, msg.obj);
682 | break;
683 | }
684 | }
685 | };
686 | private Runnable refress_time_Thread = new Runnable() {
687 | public void run() {
688 | refress_time_handler.removeCallbacks(refress_time_Thread);
689 | try {
690 | if (uiHolder.player != null && uiHolder.player.isPlaying()) {
691 | int duraction = uiHolder.player.getDuration();
692 | if (duraction > 0) {
693 | onStatusCallbackNext(CallBackState.progress, 100 * uiHolder.player.getCurrentPosition() / duraction);
694 | }
695 | }
696 | } catch (IllegalStateException e) {
697 | onStatusCallbackNext(CallBackState.error, e.toString());
698 | }
699 | refress_time_handler.postDelayed(refress_time_Thread, delaySecondTime);
700 | }
701 | };
702 |
703 | // 网络获取图片
704 | private Bitmap getBitmapFromUrl(String urlString) {
705 | Bitmap bitmap;
706 | InputStream is;
707 | try {
708 | URL url = new URL(urlString);
709 | HttpURLConnection connection = (HttpURLConnection) url.openConnection();
710 | is = new BufferedInputStream(connection.getInputStream());
711 | bitmap = BitmapFactory.decodeStream(is);
712 | connection.disconnect();
713 | is.close();
714 | return bitmap;
715 | } catch (IOException e) {
716 | Message msg = new Message();
717 | msg.what = ERROR;
718 | msg.obj = e.toString();
719 | refress_time_handler.sendMessage(msg);
720 | }
721 | return null;
722 | }
723 |
724 | private static final int ERROR = 0x1;
725 |
726 | /* ***************************** Holder封装UI ***************************** */
727 |
728 | private static final class Holder {
729 | private SurfaceHolder surfaceHolder;
730 | private MediaPlayer player;
731 | private SurfaceView surfaceView;
732 | private AssetFileDescriptor assetDescriptor;
733 | }
734 |
735 | /* ***************************** StatusCallback ***************************** */
736 |
737 | private OnStatusCallbackListener onStatusCallbackListener;
738 |
739 | // 接口类 -> OnStatusCallbackListener
740 | public interface OnStatusCallbackListener {
741 | void onStatusonStatusCallbackNext(CallBackState status, Object... args);
742 | }
743 |
744 | // 对外暴露接口 -> setOnStatusCallbackListener
745 | public MediaPlayerHelper setOnStatusCallbackListener(OnStatusCallbackListener onStatusCallbackListener) {
746 | this.onStatusCallbackListener = onStatusCallbackListener;
747 | return instance;
748 | }
749 |
750 | // 内部使用方法 -> StatusCallbackNext
751 | private void onStatusCallbackNext(CallBackState status, Object... args) {
752 | if (onStatusCallbackListener != null) {
753 | onStatusCallbackListener.onStatusonStatusCallbackNext(status, args);
754 | }
755 | }
756 |
757 | /* ***************************** HolderCreate(内部使用) ***************************** */
758 |
759 | private OnHolderCreateListener onHolderCreateListener;
760 |
761 | // 接口类 -> OnHolderCreateListener
762 | private interface OnHolderCreateListener {
763 | void onHolderCreate();
764 | }
765 |
766 | // 内部露接口 -> setOnHolderCreateListener
767 | private void setOnHolderCreateListener(OnHolderCreateListener onHolderCreateListener) {
768 | this.onHolderCreateListener = onHolderCreateListener;
769 | }
770 |
771 | // 内部使用方法 -> HolderCreateNext
772 | private void onHolderCreateNext() {
773 | if (onHolderCreateListener != null) {
774 | onHolderCreateListener.onHolderCreate();
775 | }
776 | }
777 | }
778 |
--------------------------------------------------------------------------------
/android/src/main/java/cc/dync/audio_manager/MediaPlayerService.java:
--------------------------------------------------------------------------------
1 | package cc.dync.audio_manager;
2 |
3 | import android.app.NotificationChannel;
4 | import android.app.NotificationManager;
5 | import android.app.PendingIntent;
6 | import android.app.Service;
7 | import android.content.BroadcastReceiver;
8 | import android.content.ComponentName;
9 | import android.content.Context;
10 | import android.content.Intent;
11 | import android.content.IntentFilter;
12 | import android.content.ServiceConnection;
13 | import android.graphics.Bitmap;
14 | import android.os.Binder;
15 | import android.os.Build;
16 | import android.os.IBinder;
17 | import android.util.Log;
18 | import android.widget.RemoteViews;
19 |
20 | import androidx.annotation.Nullable;
21 | import androidx.core.app.NotificationCompat;
22 |
23 | import java.util.Objects;
24 |
25 | public class MediaPlayerService extends Service {
26 | private static final String ACTION_NEXT = "MediaPlayerService_next";
27 | private static final String ACTION_PREVIOUS = "MediaPlayerService_previous";
28 | private static final String ACTION_PLAY_OR_PAUSE = "MediaPlayerService_playOrPause";
29 | private static final String ACTION_STOP = "MediaPlayerService_stop";
30 | private static final String NOTIFICATION_CHANNEL_ID = "MediaPlayerService_1100";
31 |
32 | @Nullable
33 | @Override
34 | public IBinder onBind(Intent intent) {
35 | return serviceBinder;
36 | }
37 |
38 | @Override
39 | public void onDestroy() {
40 | super.onDestroy();
41 | // 取消Notification
42 | if (notificationManager != null)
43 | notificationManager.cancel(NOTIFICATION_PENDING_ID);
44 | stopForeground(true);
45 | // 停止服务
46 | stopSelf();
47 | }
48 |
49 | @Override
50 | public void onCreate() {
51 | super.onCreate();
52 | setupNotification();
53 | }
54 |
55 | // 定义Binder类-当然也可以写成外部类
56 | private ServiceBinder serviceBinder = new ServiceBinder();
57 |
58 | public class ServiceBinder extends Binder {
59 | Service getService() {
60 | return MediaPlayerService.this;
61 | }
62 | }
63 |
64 |
65 | public enum Events {
66 | next, previous, playOrPause, stop, binder
67 | }
68 |
69 | public interface ServiceEvents {
70 | void onEvents(Events events, Object... args);
71 | }
72 |
73 | private static ServiceEvents serviceEvents;
74 | private static MediaPlayerService bindService;
75 | private static boolean isBindService = false;
76 | private static Context context;
77 |
78 | // 绑定服务 必须先调用 registerReceiver
79 | public static void bindService(ServiceEvents serviceEvents) {
80 | MediaPlayerService.serviceEvents = serviceEvents;
81 |
82 | if (!MediaPlayerService.isBindService) {
83 | Intent intent = new Intent(context, MediaPlayerService.class);
84 | /*
85 | * Service:Service的桥梁
86 | * ServiceConnection:处理链接状态
87 | * flags:BIND_AUTO_CREATE, BIND_DEBUG_UNBIND, BIND_NOT_FOREGROUND, BIND_ABOVE_CLIENT, BIND_ALLOW_OOM_MANAGEMENT, or BIND_WAIVE_PRIORITY.
88 | */
89 | context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
90 | } else {
91 | if (serviceEvents != null) serviceEvents.onEvents(Events.binder, bindService);
92 | }
93 |
94 | }
95 |
96 | /// 通知事件处理,只能加载一次,否则会重复
97 | public static void registerReceiver(Context context) {
98 | MediaPlayerService.context = context;
99 | // 注册广播
100 | BroadcastReceiver playerReceiver = new BroadcastReceiver() {
101 | @Override
102 | public void onReceive(Context context, Intent intent) {
103 | Log.d("action", intent.getAction());
104 | switch (Objects.requireNonNull(intent.getAction())) {
105 | case ACTION_NEXT:
106 | serviceEvents.onEvents(Events.next);
107 | break;
108 | case ACTION_PREVIOUS:
109 | serviceEvents.onEvents(Events.previous);
110 | break;
111 | case ACTION_PLAY_OR_PAUSE:// 暂停/播放
112 | serviceEvents.onEvents(Events.playOrPause);
113 | break;
114 | case ACTION_STOP:
115 | serviceEvents.onEvents(Events.stop);
116 | break;
117 | }
118 | }
119 | };
120 | IntentFilter intentFilter = new IntentFilter();
121 | intentFilter.addAction(ACTION_NEXT);
122 | intentFilter.addAction(ACTION_PREVIOUS);
123 | intentFilter.addAction(ACTION_PLAY_OR_PAUSE);
124 | intentFilter.addAction(ACTION_STOP);
125 | context.registerReceiver(playerReceiver, intentFilter);
126 | }
127 |
128 | // 解除绑定
129 | public static void unBind(Context context) {
130 | if (isBindService) {
131 | bindService.onDestroy();
132 | context.unbindService(serviceConnection);
133 | isBindService = false;
134 | }
135 | }
136 |
137 | /**
138 | * serviceConnection是一个ServiceConnection类型的对象,它是一个接口,用于监听所绑定服务的状态
139 | */
140 | private static ServiceConnection serviceConnection = new ServiceConnection() {
141 | /**
142 | * 该方法用于处理与服务已连接时的情况。
143 | */
144 | @Override
145 | public void onServiceConnected(ComponentName name, IBinder service) {
146 | ServiceBinder binder = (ServiceBinder) service;
147 | bindService = (MediaPlayerService) binder.getService();
148 | isBindService = true;
149 | if (serviceEvents != null) serviceEvents.onEvents(Events.binder, bindService);
150 | }
151 |
152 | /**
153 | * 该方法用于处理与服务断开连接时的情况。
154 | */
155 | @Override
156 | public void onServiceDisconnected(ComponentName name) {
157 | bindService = null;
158 | }
159 |
160 | };
161 |
162 | // private static final int DELETE_PENDING_REQUESTS = 1022;
163 | private static final int CONTENT_PENDING_REQUESTS = 1023;
164 | private static final int NEXT_PENDING_REQUESTS = 1024;
165 | private static final int PLAY_PENDING_REQUESTS = 1025;
166 | private static final int STOP_PENDING_REQUESTS = 1026;
167 | private static final int NOTIFICATION_PENDING_ID = 1;
168 |
169 | private NotificationManager notificationManager;
170 | private NotificationCompat.Builder builder;
171 | private RemoteViews views;
172 |
173 | private void setupNotification() {
174 | // 设置点击通知结果
175 | // Intent intent = new Intent("android.flutter.audio_manager.activity");
176 | Intent intent = new Intent(this, AudioManagerPlugin.class);
177 | PendingIntent contentPendingIntent = PendingIntent.getActivity(this, CONTENT_PENDING_REQUESTS, intent, PendingIntent.FLAG_UPDATE_CURRENT);
178 |
179 | // 自定义布局
180 | views = new RemoteViews(getPackageName(), R.layout.layout_mediaplayer);
181 | // 下一首
182 | Intent intentNext = new Intent(ACTION_NEXT);
183 | PendingIntent nextPendingIntent = PendingIntent.getBroadcast(this, NEXT_PENDING_REQUESTS, intentNext, PendingIntent.FLAG_CANCEL_CURRENT);
184 | views.setOnClickPendingIntent(R.id.iv_next, nextPendingIntent);
185 |
186 | // 暂停/播放
187 | Intent intentPlay = new Intent(ACTION_PLAY_OR_PAUSE);
188 | PendingIntent playPendingIntent = PendingIntent.getBroadcast(this, PLAY_PENDING_REQUESTS, intentPlay, PendingIntent.FLAG_CANCEL_CURRENT);
189 | views.setOnClickPendingIntent(R.id.iv_pause, playPendingIntent);
190 |
191 | // 停止
192 | Intent intentStop = new Intent(ACTION_STOP);
193 | PendingIntent stopPendingIntent = PendingIntent.getBroadcast(this, STOP_PENDING_REQUESTS, intentStop, PendingIntent.FLAG_CANCEL_CURRENT);
194 | views.setOnClickPendingIntent(R.id.iv_cancel, stopPendingIntent);
195 |
196 | builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
197 | // 设置状态栏小图标
198 | .setSmallIcon(R.drawable.ic_launcher)
199 | // 设置标题
200 | .setContentTitle("")
201 | // 设置内容
202 | .setContentText("")
203 | // 点击通知后自动清除
204 | .setAutoCancel(false)
205 | // 设置点击通知效果
206 | .setContentIntent(contentPendingIntent)
207 | // 设置删除时候出发的动作
208 | // .setDeleteIntent(delPendingIntent)
209 | // 自定义视图
210 | .setContent(views);
211 |
212 | // 获取NotificationManager实例
213 | notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
214 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
215 | NotificationChannel notificationChannel;
216 | notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
217 | "Notification display", NotificationManager.IMPORTANCE_LOW);
218 | notificationManager.createNotificationChannel(notificationChannel);
219 | }
220 |
221 | // 前台服务
222 | startForeground(NOTIFICATION_PENDING_ID, builder.build());
223 | }
224 |
225 | void updateCover(Bitmap bitmap) {
226 | views.setImageViewBitmap(R.id.image, bitmap);
227 | notificationManager.notify(NOTIFICATION_PENDING_ID, builder.build());
228 | }
229 |
230 | void updateCover(int srcId) {
231 | views.setImageViewResource(R.id.image, srcId);
232 | }
233 |
234 | // 更新Notification
235 | void updateNotification(boolean isPlaying, String title, String desc) {
236 | if (views != null) {
237 | views.setTextViewText(R.id.tv_name, title);
238 | if (desc != null) views.setTextViewText(R.id.tv_author, desc);
239 | if (isPlaying) {
240 | views.setImageViewResource(R.id.iv_pause, android.R.drawable.ic_media_pause);
241 | } else {
242 | views.setImageViewResource(R.id.iv_pause, android.R.drawable.ic_media_play);
243 | }
244 | }
245 |
246 | // 刷新notification
247 | notificationManager.notify(NOTIFICATION_PENDING_ID, builder.build());
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/android/src/main/java/cc/dync/audio_manager/VolumeChangeObserver.java:
--------------------------------------------------------------------------------
1 | package cc.dync.audio_manager;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.IntentFilter;
7 | import android.media.AudioManager;
8 | import android.util.Log;
9 |
10 | import java.lang.ref.WeakReference;
11 |
12 |
13 | /**
14 | * 系统音量监听
15 | */
16 | public class VolumeChangeObserver {
17 | public final static String TAG = "volume_watcher";
18 | private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
19 | private static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE";
20 |
21 | public interface VolumeChangeListener {
22 | /**
23 | * 系统媒体音量变化
24 | *
25 | * @param volume
26 | */
27 | void onVolumeChanged(double volume);
28 | }
29 |
30 | private VolumeChangeListener mVolumeChangeListener;
31 | private VolumeBroadcastReceiver mVolumeBroadcastReceiver;
32 | private Context mContext;
33 | private AudioManager mAudioManager;
34 | private boolean mRegistered = false;
35 | // 最大音量
36 | private int mMaxVolume;
37 |
38 | public VolumeChangeObserver(Context context) {
39 | mContext = context;
40 | mAudioManager = (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
41 | mMaxVolume = mAudioManager != null ? mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) : 15;
42 | }
43 |
44 | /**
45 | * 获取当前媒体音量
46 | *
47 | * @return
48 | */
49 | public double getCurrentMusicVolume() {
50 | int currentVolume = mAudioManager != null ? mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC) : -1;
51 | return ((double) currentVolume) / mMaxVolume;
52 | }
53 |
54 | /**
55 | * 获取系统最大媒体音量
56 | *
57 | * @return
58 | */
59 | public double getMaxMusicVolume() {
60 | return 1.0;
61 | }
62 |
63 | /**
64 | * 设置音量
65 | *
66 | * @param value
67 | */
68 | public void setVolume(double value) {
69 | double actualValue;
70 | if (value > 1.0) {
71 | actualValue = 1.0;
72 | } else if (value < 0.0) {
73 | actualValue = 0.0;
74 | } else {
75 | actualValue = value;
76 | }
77 | int volume = (int) Math.round(actualValue * mMaxVolume);
78 | if (mAudioManager != null) {
79 | try {
80 | // 设置音量
81 | mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
82 | if (volume < 1) {
83 | mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, 0);
84 | }
85 | } catch (Exception ex) {
86 | //禁止日志
87 | }
88 | }
89 | }
90 |
91 | public VolumeChangeListener getVolumeChangeListener() {
92 | return mVolumeChangeListener;
93 | }
94 |
95 | public void setVolumeChangeListener(VolumeChangeListener volumeChangeListener) {
96 | this.mVolumeChangeListener = volumeChangeListener;
97 | }
98 |
99 | /**
100 | * 注册音量广播接收器
101 | *
102 | * @return
103 | */
104 | public void registerReceiver() {
105 | mVolumeBroadcastReceiver = new VolumeBroadcastReceiver(this);
106 | IntentFilter filter = new IntentFilter();
107 | filter.addAction(VOLUME_CHANGED_ACTION);
108 | mContext.registerReceiver(mVolumeBroadcastReceiver, filter);
109 | mRegistered = true;
110 | }
111 |
112 | /**
113 | * 解注册音量广播监听器,需要与 registerReceiver 成对使用
114 | */
115 | public void unregisterReceiver() {
116 | if (mRegistered) {
117 | try {
118 | mContext.unregisterReceiver(mVolumeBroadcastReceiver);
119 | mVolumeChangeListener = null;
120 | mRegistered = false;
121 | } catch (Exception e) {
122 | Log.e(TAG, "unregisterReceiver: ", e);
123 | }
124 | }
125 | }
126 |
127 | private static class VolumeBroadcastReceiver extends BroadcastReceiver {
128 | private WeakReference mObserverWeakReference;
129 |
130 | public VolumeBroadcastReceiver(VolumeChangeObserver volumeChangeObserver) {
131 | mObserverWeakReference = new WeakReference<>(volumeChangeObserver);
132 | }
133 |
134 | @Override
135 | public void onReceive(Context context, Intent intent) {
136 | //媒体音量改变才通知
137 | if (VOLUME_CHANGED_ACTION.equals(intent.getAction()) && (intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1) == AudioManager.STREAM_MUSIC)) {
138 | VolumeChangeObserver observer = mObserverWeakReference.get();
139 | if (observer != null) {
140 | VolumeChangeListener listener = observer.getVolumeChangeListener();
141 | if (listener != null) {
142 | double volume = observer.getCurrentMusicVolume();
143 | if (volume >= 0) {
144 | listener.onVolumeChanged(volume);
145 | }
146 |
147 | if (BuildConfig.DEBUG) {
148 | Log.d(TAG, "volume=" + volume);
149 | }
150 | }
151 | }
152 | }
153 |
154 | }
155 | }
156 |
157 | }
--------------------------------------------------------------------------------
/android/src/main/res/drawable/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/android/src/main/res/drawable/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/layout/layout_mediaplayer.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
23 |
24 |
32 |
33 |
41 |
42 |
43 |
47 |
48 |
56 |
57 |
65 |
66 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | music name
4 | artist
5 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 | .packages
29 | .pub-cache/
30 | .pub/
31 | /build/
32 |
33 | # Web related
34 | lib/generated_plugin_registrant.dart
35 |
36 | # Exceptions to above rules.
37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
38 |
--------------------------------------------------------------------------------
/example/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # audio_manager_example
2 |
3 | Demonstrates how to use the audio_manager plugin.
4 |
5 | ## Getting Started
6 |
7 | This project is a starting point for a Flutter application.
8 |
9 | A few resources to get you started if this is your first Flutter project:
10 |
11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
13 |
14 | For help getting started with Flutter, view our
15 | [online documentation](https://flutter.dev/docs), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/example/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
--------------------------------------------------------------------------------
/example/android/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | android____
4 | Project android____ created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.buildship.core.gradleprojectbuilder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.buildship.core.gradleprojectnature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/example/android/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | connection.project.dir=
2 | eclipse.preferences.version=1
3 |
--------------------------------------------------------------------------------
/example/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
26 |
27 | android {
28 | compileSdkVersion 28
29 |
30 | lintOptions {
31 | disable 'InvalidPackage'
32 | }
33 |
34 | defaultConfig {
35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
36 | applicationId "cc.dync.audio_manager_example"
37 | minSdkVersion 23
38 | targetSdkVersion 29
39 | versionCode flutterVersionCode.toInteger()
40 | versionName flutterVersionName
41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
42 | }
43 |
44 | buildTypes {
45 | release {
46 | // TODO: Add your own signing config for the release build.
47 | // Signing with the debug keys for now, so `flutter run --release` works.
48 | signingConfig signingConfigs.debug
49 | }
50 | }
51 | }
52 |
53 | flutter {
54 | source '../..'
55 | }
56 |
57 | dependencies {
58 | testImplementation 'junit:junit:4.12'
59 | androidTestImplementation 'androidx.test:runner:1.1.1'
60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
61 | }
62 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
10 |
16 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/cc/dync/audio_manager_example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package cc.dync.audio_manager_example;
2 |
3 | import androidx.annotation.NonNull;
4 | import io.flutter.embedding.android.FlutterActivity;
5 | import io.flutter.embedding.engine.FlutterEngine;
6 | import io.flutter.plugins.GeneratedPluginRegistrant;
7 |
8 | public class MainActivity extends FlutterActivity {
9 | @Override
10 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
11 | GeneratedPluginRegistrant.registerWith(flutterEngine);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/example/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | jcenter()
5 | }
6 |
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:3.5.3'
9 | }
10 | }
11 |
12 | allprojects {
13 | repositories {
14 | google()
15 | jcenter()
16 | }
17 | }
18 |
19 | rootProject.buildDir = '../build'
20 | subprojects {
21 | project.buildDir = "${rootProject.buildDir}/${project.name}"
22 | }
23 | subprojects {
24 | project.evaluationDependsOn(':app')
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/example/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/example/assets/aLIEz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/assets/aLIEz.jpg
--------------------------------------------------------------------------------
/example/assets/aLIEz.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/assets/aLIEz.m4a
--------------------------------------------------------------------------------
/example/assets/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/assets/ic_launcher.png
--------------------------------------------------------------------------------
/example/assets/xv.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/assets/xv.jpg
--------------------------------------------------------------------------------
/example/assets/xv.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/assets/xv.mp3
--------------------------------------------------------------------------------
/example/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/example/ios/Flutter/.last_build_id:
--------------------------------------------------------------------------------
1 | 09ec4c0538acad21c5b01a6b969eeef3
--------------------------------------------------------------------------------
/example/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
16 | AD82FFA292D3843F697366D0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025B77AC1DEE2467B92987B7 /* Pods_Runner.framework */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXCopyFilesBuildPhase section */
20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
21 | isa = PBXCopyFilesBuildPhase;
22 | buildActionMask = 2147483647;
23 | dstPath = "";
24 | dstSubfolderSpec = 10;
25 | files = (
26 | );
27 | name = "Embed Frameworks";
28 | runOnlyForDeploymentPostprocessing = 0;
29 | };
30 | /* End PBXCopyFilesBuildPhase section */
31 |
32 | /* Begin PBXFileReference section */
33 | 025B77AC1DEE2467B92987B7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
34 | 06CDF74C170D3236F1FA0E87 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
35 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
36 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
37 | 3A42FEFE8C615508802EABF1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
39 | 617F3A7C05742AA07CA2F110 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
40 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
41 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
42 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
46 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
47 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
48 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
49 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
50 | /* End PBXFileReference section */
51 |
52 | /* Begin PBXFrameworksBuildPhase section */
53 | 97C146EB1CF9000F007C117D /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | AD82FFA292D3843F697366D0 /* Pods_Runner.framework in Frameworks */,
58 | );
59 | runOnlyForDeploymentPostprocessing = 0;
60 | };
61 | /* End PBXFrameworksBuildPhase section */
62 |
63 | /* Begin PBXGroup section */
64 | 259515BFDA98CE8C19E04E65 /* Frameworks */ = {
65 | isa = PBXGroup;
66 | children = (
67 | 025B77AC1DEE2467B92987B7 /* Pods_Runner.framework */,
68 | );
69 | name = Frameworks;
70 | sourceTree = "";
71 | };
72 | 91A4C579648190EF8E519E76 /* Pods */ = {
73 | isa = PBXGroup;
74 | children = (
75 | 06CDF74C170D3236F1FA0E87 /* Pods-Runner.debug.xcconfig */,
76 | 617F3A7C05742AA07CA2F110 /* Pods-Runner.release.xcconfig */,
77 | 3A42FEFE8C615508802EABF1 /* Pods-Runner.profile.xcconfig */,
78 | );
79 | path = Pods;
80 | sourceTree = "";
81 | };
82 | 9740EEB11CF90186004384FC /* Flutter */ = {
83 | isa = PBXGroup;
84 | children = (
85 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
86 | 9740EEB21CF90195004384FC /* Debug.xcconfig */,
87 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
88 | 9740EEB31CF90195004384FC /* Generated.xcconfig */,
89 | );
90 | name = Flutter;
91 | sourceTree = "";
92 | };
93 | 97C146E51CF9000F007C117D = {
94 | isa = PBXGroup;
95 | children = (
96 | 9740EEB11CF90186004384FC /* Flutter */,
97 | 97C146F01CF9000F007C117D /* Runner */,
98 | 97C146EF1CF9000F007C117D /* Products */,
99 | 91A4C579648190EF8E519E76 /* Pods */,
100 | 259515BFDA98CE8C19E04E65 /* Frameworks */,
101 | );
102 | sourceTree = "";
103 | };
104 | 97C146EF1CF9000F007C117D /* Products */ = {
105 | isa = PBXGroup;
106 | children = (
107 | 97C146EE1CF9000F007C117D /* Runner.app */,
108 | );
109 | name = Products;
110 | sourceTree = "";
111 | };
112 | 97C146F01CF9000F007C117D /* Runner */ = {
113 | isa = PBXGroup;
114 | children = (
115 | 97C146FA1CF9000F007C117D /* Main.storyboard */,
116 | 97C146FD1CF9000F007C117D /* Assets.xcassets */,
117 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
118 | 97C147021CF9000F007C117D /* Info.plist */,
119 | 97C146F11CF9000F007C117D /* Supporting Files */,
120 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
121 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
122 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
123 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
124 | );
125 | path = Runner;
126 | sourceTree = "";
127 | };
128 | 97C146F11CF9000F007C117D /* Supporting Files */ = {
129 | isa = PBXGroup;
130 | children = (
131 | );
132 | name = "Supporting Files";
133 | sourceTree = "";
134 | };
135 | /* End PBXGroup section */
136 |
137 | /* Begin PBXNativeTarget section */
138 | 97C146ED1CF9000F007C117D /* Runner */ = {
139 | isa = PBXNativeTarget;
140 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
141 | buildPhases = (
142 | 8361AE34748BDE474260018F /* [CP] Check Pods Manifest.lock */,
143 | 9740EEB61CF901F6004384FC /* Run Script */,
144 | 97C146EA1CF9000F007C117D /* Sources */,
145 | 97C146EB1CF9000F007C117D /* Frameworks */,
146 | 97C146EC1CF9000F007C117D /* Resources */,
147 | 9705A1C41CF9048500538489 /* Embed Frameworks */,
148 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
149 | B9E43E89B91EE218972775DB /* [CP] Embed Pods Frameworks */,
150 | );
151 | buildRules = (
152 | );
153 | dependencies = (
154 | );
155 | name = Runner;
156 | productName = Runner;
157 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
158 | productType = "com.apple.product-type.application";
159 | };
160 | /* End PBXNativeTarget section */
161 |
162 | /* Begin PBXProject section */
163 | 97C146E61CF9000F007C117D /* Project object */ = {
164 | isa = PBXProject;
165 | attributes = {
166 | LastUpgradeCheck = 1020;
167 | ORGANIZATIONNAME = "The Chromium Authors";
168 | TargetAttributes = {
169 | 97C146ED1CF9000F007C117D = {
170 | CreatedOnToolsVersion = 7.3.1;
171 | DevelopmentTeam = 52ZMP45QS5;
172 | LastSwiftMigration = 1100;
173 | };
174 | };
175 | };
176 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
177 | compatibilityVersion = "Xcode 3.2";
178 | developmentRegion = en;
179 | hasScannedForEncodings = 0;
180 | knownRegions = (
181 | en,
182 | Base,
183 | );
184 | mainGroup = 97C146E51CF9000F007C117D;
185 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
186 | projectDirPath = "";
187 | projectRoot = "";
188 | targets = (
189 | 97C146ED1CF9000F007C117D /* Runner */,
190 | );
191 | };
192 | /* End PBXProject section */
193 |
194 | /* Begin PBXResourcesBuildPhase section */
195 | 97C146EC1CF9000F007C117D /* Resources */ = {
196 | isa = PBXResourcesBuildPhase;
197 | buildActionMask = 2147483647;
198 | files = (
199 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
200 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
201 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
202 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
203 | );
204 | runOnlyForDeploymentPostprocessing = 0;
205 | };
206 | /* End PBXResourcesBuildPhase section */
207 |
208 | /* Begin PBXShellScriptBuildPhase section */
209 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
210 | isa = PBXShellScriptBuildPhase;
211 | buildActionMask = 2147483647;
212 | files = (
213 | );
214 | inputPaths = (
215 | );
216 | name = "Thin Binary";
217 | outputPaths = (
218 | );
219 | runOnlyForDeploymentPostprocessing = 0;
220 | shellPath = /bin/sh;
221 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
222 | };
223 | 8361AE34748BDE474260018F /* [CP] Check Pods Manifest.lock */ = {
224 | isa = PBXShellScriptBuildPhase;
225 | buildActionMask = 2147483647;
226 | files = (
227 | );
228 | inputFileListPaths = (
229 | );
230 | inputPaths = (
231 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
232 | "${PODS_ROOT}/Manifest.lock",
233 | );
234 | name = "[CP] Check Pods Manifest.lock";
235 | outputFileListPaths = (
236 | );
237 | outputPaths = (
238 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
239 | );
240 | runOnlyForDeploymentPostprocessing = 0;
241 | shellPath = /bin/sh;
242 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
243 | showEnvVarsInLog = 0;
244 | };
245 | 9740EEB61CF901F6004384FC /* Run Script */ = {
246 | isa = PBXShellScriptBuildPhase;
247 | buildActionMask = 2147483647;
248 | files = (
249 | );
250 | inputPaths = (
251 | );
252 | name = "Run Script";
253 | outputPaths = (
254 | );
255 | runOnlyForDeploymentPostprocessing = 0;
256 | shellPath = /bin/sh;
257 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
258 | };
259 | B9E43E89B91EE218972775DB /* [CP] Embed Pods Frameworks */ = {
260 | isa = PBXShellScriptBuildPhase;
261 | buildActionMask = 2147483647;
262 | files = (
263 | );
264 | inputPaths = (
265 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
266 | "${BUILT_PRODUCTS_DIR}/audio_manager/audio_manager.framework",
267 | "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
268 | );
269 | name = "[CP] Embed Pods Frameworks";
270 | outputPaths = (
271 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audio_manager.framework",
272 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
273 | );
274 | runOnlyForDeploymentPostprocessing = 0;
275 | shellPath = /bin/sh;
276 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
277 | showEnvVarsInLog = 0;
278 | };
279 | /* End PBXShellScriptBuildPhase section */
280 |
281 | /* Begin PBXSourcesBuildPhase section */
282 | 97C146EA1CF9000F007C117D /* Sources */ = {
283 | isa = PBXSourcesBuildPhase;
284 | buildActionMask = 2147483647;
285 | files = (
286 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
287 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
288 | );
289 | runOnlyForDeploymentPostprocessing = 0;
290 | };
291 | /* End PBXSourcesBuildPhase section */
292 |
293 | /* Begin PBXVariantGroup section */
294 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
295 | isa = PBXVariantGroup;
296 | children = (
297 | 97C146FB1CF9000F007C117D /* Base */,
298 | );
299 | name = Main.storyboard;
300 | sourceTree = "";
301 | };
302 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
303 | isa = PBXVariantGroup;
304 | children = (
305 | 97C147001CF9000F007C117D /* Base */,
306 | );
307 | name = LaunchScreen.storyboard;
308 | sourceTree = "";
309 | };
310 | /* End PBXVariantGroup section */
311 |
312 | /* Begin XCBuildConfiguration section */
313 | 249021D3217E4FDB00AE95B9 /* Profile */ = {
314 | isa = XCBuildConfiguration;
315 | buildSettings = {
316 | ALWAYS_SEARCH_USER_PATHS = NO;
317 | CLANG_ANALYZER_NONNULL = YES;
318 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
319 | CLANG_CXX_LIBRARY = "libc++";
320 | CLANG_ENABLE_MODULES = YES;
321 | CLANG_ENABLE_OBJC_ARC = YES;
322 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
323 | CLANG_WARN_BOOL_CONVERSION = YES;
324 | CLANG_WARN_COMMA = YES;
325 | CLANG_WARN_CONSTANT_CONVERSION = YES;
326 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
327 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
328 | CLANG_WARN_EMPTY_BODY = YES;
329 | CLANG_WARN_ENUM_CONVERSION = YES;
330 | CLANG_WARN_INFINITE_RECURSION = YES;
331 | CLANG_WARN_INT_CONVERSION = YES;
332 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
333 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
334 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
335 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
336 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
337 | CLANG_WARN_STRICT_PROTOTYPES = YES;
338 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
339 | CLANG_WARN_UNREACHABLE_CODE = YES;
340 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
341 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
342 | COPY_PHASE_STRIP = NO;
343 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
344 | ENABLE_NS_ASSERTIONS = NO;
345 | ENABLE_STRICT_OBJC_MSGSEND = YES;
346 | GCC_C_LANGUAGE_STANDARD = gnu99;
347 | GCC_NO_COMMON_BLOCKS = YES;
348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
350 | GCC_WARN_UNDECLARED_SELECTOR = YES;
351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
352 | GCC_WARN_UNUSED_FUNCTION = YES;
353 | GCC_WARN_UNUSED_VARIABLE = YES;
354 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
355 | MTL_ENABLE_DEBUG_INFO = NO;
356 | SDKROOT = iphoneos;
357 | SUPPORTED_PLATFORMS = iphoneos;
358 | TARGETED_DEVICE_FAMILY = "1,2";
359 | VALIDATE_PRODUCT = YES;
360 | };
361 | name = Profile;
362 | };
363 | 249021D4217E4FDB00AE95B9 /* Profile */ = {
364 | isa = XCBuildConfiguration;
365 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
366 | buildSettings = {
367 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
368 | CLANG_ENABLE_MODULES = YES;
369 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
370 | DEVELOPMENT_TEAM = 52ZMP45QS5;
371 | ENABLE_BITCODE = NO;
372 | FRAMEWORK_SEARCH_PATHS = (
373 | "$(inherited)",
374 | "$(PROJECT_DIR)/Flutter",
375 | );
376 | INFOPLIST_FILE = Runner/Info.plist;
377 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
378 | LIBRARY_SEARCH_PATHS = (
379 | "$(inherited)",
380 | "$(PROJECT_DIR)/Flutter",
381 | );
382 | PRODUCT_BUNDLE_IDENTIFIER = jerome.audioManagerExample;
383 | PRODUCT_NAME = "$(TARGET_NAME)";
384 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
385 | SWIFT_VERSION = 5.0;
386 | VERSIONING_SYSTEM = "apple-generic";
387 | };
388 | name = Profile;
389 | };
390 | 97C147031CF9000F007C117D /* Debug */ = {
391 | isa = XCBuildConfiguration;
392 | buildSettings = {
393 | ALWAYS_SEARCH_USER_PATHS = NO;
394 | CLANG_ANALYZER_NONNULL = YES;
395 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
396 | CLANG_CXX_LIBRARY = "libc++";
397 | CLANG_ENABLE_MODULES = YES;
398 | CLANG_ENABLE_OBJC_ARC = YES;
399 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
400 | CLANG_WARN_BOOL_CONVERSION = YES;
401 | CLANG_WARN_COMMA = YES;
402 | CLANG_WARN_CONSTANT_CONVERSION = YES;
403 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
404 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
405 | CLANG_WARN_EMPTY_BODY = YES;
406 | CLANG_WARN_ENUM_CONVERSION = YES;
407 | CLANG_WARN_INFINITE_RECURSION = YES;
408 | CLANG_WARN_INT_CONVERSION = YES;
409 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
410 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
411 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
412 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
413 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
414 | CLANG_WARN_STRICT_PROTOTYPES = YES;
415 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
416 | CLANG_WARN_UNREACHABLE_CODE = YES;
417 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
418 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
419 | COPY_PHASE_STRIP = NO;
420 | DEBUG_INFORMATION_FORMAT = dwarf;
421 | ENABLE_STRICT_OBJC_MSGSEND = YES;
422 | ENABLE_TESTABILITY = YES;
423 | GCC_C_LANGUAGE_STANDARD = gnu99;
424 | GCC_DYNAMIC_NO_PIC = NO;
425 | GCC_NO_COMMON_BLOCKS = YES;
426 | GCC_OPTIMIZATION_LEVEL = 0;
427 | GCC_PREPROCESSOR_DEFINITIONS = (
428 | "DEBUG=1",
429 | "$(inherited)",
430 | );
431 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
432 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
433 | GCC_WARN_UNDECLARED_SELECTOR = YES;
434 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
435 | GCC_WARN_UNUSED_FUNCTION = YES;
436 | GCC_WARN_UNUSED_VARIABLE = YES;
437 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
438 | MTL_ENABLE_DEBUG_INFO = YES;
439 | ONLY_ACTIVE_ARCH = YES;
440 | SDKROOT = iphoneos;
441 | TARGETED_DEVICE_FAMILY = "1,2";
442 | };
443 | name = Debug;
444 | };
445 | 97C147041CF9000F007C117D /* Release */ = {
446 | isa = XCBuildConfiguration;
447 | buildSettings = {
448 | ALWAYS_SEARCH_USER_PATHS = NO;
449 | CLANG_ANALYZER_NONNULL = YES;
450 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
451 | CLANG_CXX_LIBRARY = "libc++";
452 | CLANG_ENABLE_MODULES = YES;
453 | CLANG_ENABLE_OBJC_ARC = YES;
454 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
455 | CLANG_WARN_BOOL_CONVERSION = YES;
456 | CLANG_WARN_COMMA = YES;
457 | CLANG_WARN_CONSTANT_CONVERSION = YES;
458 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
459 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
460 | CLANG_WARN_EMPTY_BODY = YES;
461 | CLANG_WARN_ENUM_CONVERSION = YES;
462 | CLANG_WARN_INFINITE_RECURSION = YES;
463 | CLANG_WARN_INT_CONVERSION = YES;
464 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
465 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
466 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
467 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
468 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
469 | CLANG_WARN_STRICT_PROTOTYPES = YES;
470 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
471 | CLANG_WARN_UNREACHABLE_CODE = YES;
472 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
473 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
474 | COPY_PHASE_STRIP = NO;
475 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
476 | ENABLE_NS_ASSERTIONS = NO;
477 | ENABLE_STRICT_OBJC_MSGSEND = YES;
478 | GCC_C_LANGUAGE_STANDARD = gnu99;
479 | GCC_NO_COMMON_BLOCKS = YES;
480 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
481 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
482 | GCC_WARN_UNDECLARED_SELECTOR = YES;
483 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
484 | GCC_WARN_UNUSED_FUNCTION = YES;
485 | GCC_WARN_UNUSED_VARIABLE = YES;
486 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
487 | MTL_ENABLE_DEBUG_INFO = NO;
488 | SDKROOT = iphoneos;
489 | SUPPORTED_PLATFORMS = iphoneos;
490 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
491 | TARGETED_DEVICE_FAMILY = "1,2";
492 | VALIDATE_PRODUCT = YES;
493 | };
494 | name = Release;
495 | };
496 | 97C147061CF9000F007C117D /* Debug */ = {
497 | isa = XCBuildConfiguration;
498 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
499 | buildSettings = {
500 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
501 | CLANG_ENABLE_MODULES = YES;
502 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
503 | DEVELOPMENT_TEAM = 52ZMP45QS5;
504 | ENABLE_BITCODE = NO;
505 | FRAMEWORK_SEARCH_PATHS = (
506 | "$(inherited)",
507 | "$(PROJECT_DIR)/Flutter",
508 | );
509 | INFOPLIST_FILE = Runner/Info.plist;
510 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
511 | LIBRARY_SEARCH_PATHS = (
512 | "$(inherited)",
513 | "$(PROJECT_DIR)/Flutter",
514 | );
515 | PRODUCT_BUNDLE_IDENTIFIER = jerome.audioManagerExample;
516 | PRODUCT_NAME = "$(TARGET_NAME)";
517 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
518 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
519 | SWIFT_VERSION = 5.0;
520 | VERSIONING_SYSTEM = "apple-generic";
521 | };
522 | name = Debug;
523 | };
524 | 97C147071CF9000F007C117D /* Release */ = {
525 | isa = XCBuildConfiguration;
526 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
527 | buildSettings = {
528 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
529 | CLANG_ENABLE_MODULES = YES;
530 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
531 | DEVELOPMENT_TEAM = 52ZMP45QS5;
532 | ENABLE_BITCODE = NO;
533 | FRAMEWORK_SEARCH_PATHS = (
534 | "$(inherited)",
535 | "$(PROJECT_DIR)/Flutter",
536 | );
537 | INFOPLIST_FILE = Runner/Info.plist;
538 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
539 | LIBRARY_SEARCH_PATHS = (
540 | "$(inherited)",
541 | "$(PROJECT_DIR)/Flutter",
542 | );
543 | PRODUCT_BUNDLE_IDENTIFIER = jerome.audioManagerExample;
544 | PRODUCT_NAME = "$(TARGET_NAME)";
545 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
546 | SWIFT_VERSION = 5.0;
547 | VERSIONING_SYSTEM = "apple-generic";
548 | };
549 | name = Release;
550 | };
551 | /* End XCBuildConfiguration section */
552 |
553 | /* Begin XCConfigurationList section */
554 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
555 | isa = XCConfigurationList;
556 | buildConfigurations = (
557 | 97C147031CF9000F007C117D /* Debug */,
558 | 97C147041CF9000F007C117D /* Release */,
559 | 249021D3217E4FDB00AE95B9 /* Profile */,
560 | );
561 | defaultConfigurationIsVisible = 0;
562 | defaultConfigurationName = Release;
563 | };
564 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
565 | isa = XCConfigurationList;
566 | buildConfigurations = (
567 | 97C147061CF9000F007C117D /* Debug */,
568 | 97C147071CF9000F007C117D /* Release */,
569 | 249021D4217E4FDB00AE95B9 /* Profile */,
570 | );
571 | defaultConfigurationIsVisible = 0;
572 | defaultConfigurationName = Release;
573 | };
574 | /* End XCConfigurationList section */
575 | };
576 | rootObject = 97C146E61CF9000F007C117D /* Project object */;
577 | }
578 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | audio_manager_example
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | NSAppTransportSecurity
26 |
27 | NSAllowsArbitraryLoads
28 |
29 |
30 | UIBackgroundModes
31 |
32 | audio
33 |
34 | UILaunchStoryboardName
35 | LaunchScreen
36 | UIMainStoryboardFile
37 | Main
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UISupportedInterfaceOrientations~ipad
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationPortraitUpsideDown
48 | UIInterfaceOrientationLandscapeLeft
49 | UIInterfaceOrientationLandscapeRight
50 |
51 | UIViewControllerBasedStatusBarAppearance
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/example/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'dart:async';
3 | import 'dart:io';
4 |
5 | import 'package:flutter/services.dart';
6 | import 'package:audio_manager/audio_manager.dart';
7 | import 'package:path_provider/path_provider.dart';
8 |
9 | void main() => runApp(MyApp());
10 |
11 | class MyApp extends StatefulWidget {
12 | @override
13 | _MyAppState createState() => _MyAppState();
14 | }
15 |
16 | class _MyAppState extends State {
17 | String _platformVersion = 'Unknown';
18 | bool isPlaying = false;
19 | Duration _duration;
20 | Duration _position;
21 | double _slider;
22 | double _sliderVolume;
23 | String _error;
24 | num curIndex = 0;
25 | PlayMode playMode = AudioManager.instance.playMode;
26 |
27 | final list = [
28 | {
29 | "title": "Assets",
30 | "desc": "assets playback",
31 | "url": "assets/xv.mp3",
32 | "coverUrl": "assets/ic_launcher.png"
33 | },
34 | {
35 | "title": "network",
36 | "desc": "network resouce playback",
37 | "url": "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.m4a",
38 | "coverUrl": "https://homepages.cae.wisc.edu/~ece533/images/airplane.png"
39 | }
40 | ];
41 |
42 | @override
43 | void initState() {
44 | super.initState();
45 |
46 | initPlatformState();
47 | setupAudio();
48 | loadFile();
49 | }
50 |
51 | @override
52 | void dispose() {
53 | // 释放所有资源
54 | AudioManager.instance.release();
55 | super.dispose();
56 | }
57 |
58 | void setupAudio() {
59 | List _list = [];
60 | list.forEach((item) => _list.add(AudioInfo(item["url"],
61 | title: item["title"], desc: item["desc"], coverUrl: item["coverUrl"])));
62 |
63 | AudioManager.instance.audioList = _list;
64 | AudioManager.instance.intercepter = true;
65 | AudioManager.instance.play(auto: false);
66 |
67 | AudioManager.instance.onEvents((events, args) {
68 | print("$events, $args");
69 | switch (events) {
70 | case AudioManagerEvents.start:
71 | print(
72 | "start load data callback, curIndex is ${AudioManager.instance.curIndex}");
73 | _position = AudioManager.instance.position;
74 | _duration = AudioManager.instance.duration;
75 | _slider = 0;
76 | setState(() {});
77 | AudioManager.instance.updateLrc("audio resource loading....");
78 | break;
79 | case AudioManagerEvents.ready:
80 | print("ready to play");
81 | _error = null;
82 | _sliderVolume = AudioManager.instance.volume;
83 | _position = AudioManager.instance.position;
84 | _duration = AudioManager.instance.duration;
85 | setState(() {});
86 | // if you need to seek times, must after AudioManagerEvents.ready event invoked
87 | // AudioManager.instance.seekTo(Duration(seconds: 10));
88 | break;
89 | case AudioManagerEvents.seekComplete:
90 | _position = AudioManager.instance.position;
91 | _slider = _position.inMilliseconds / _duration.inMilliseconds;
92 | setState(() {});
93 | print("seek event is completed. position is [$args]/ms");
94 | break;
95 | case AudioManagerEvents.buffering:
96 | print("buffering $args");
97 | break;
98 | case AudioManagerEvents.playstatus:
99 | isPlaying = AudioManager.instance.isPlaying;
100 | setState(() {});
101 | break;
102 | case AudioManagerEvents.timeupdate:
103 | _position = AudioManager.instance.position;
104 | _slider = _position.inMilliseconds / _duration.inMilliseconds;
105 | setState(() {});
106 | AudioManager.instance.updateLrc(args["position"].toString());
107 | break;
108 | case AudioManagerEvents.error:
109 | _error = args;
110 | setState(() {});
111 | break;
112 | case AudioManagerEvents.ended:
113 | AudioManager.instance.next();
114 | break;
115 | case AudioManagerEvents.volumeChange:
116 | _sliderVolume = AudioManager.instance.volume;
117 | setState(() {});
118 | break;
119 | default:
120 | break;
121 | }
122 | });
123 | }
124 |
125 | void loadFile() async {
126 | // read bundle file to local path
127 | final audioFile = await rootBundle.load("assets/aLIEz.m4a");
128 | final audio = audioFile.buffer.asUint8List();
129 |
130 | final appDocDir = await getApplicationDocumentsDirectory();
131 | print(appDocDir);
132 |
133 | final file = File("${appDocDir.path}/aLIEz.m4a");
134 | file.writeAsBytesSync(audio);
135 |
136 | AudioInfo info = AudioInfo("file://${file.path}",
137 | title: "file", desc: "local file", coverUrl: "assets/aLIEz.jpg");
138 |
139 | list.add(info.toJson());
140 | AudioManager.instance.audioList.add(info);
141 | setState(() {});
142 | }
143 |
144 | Future initPlatformState() async {
145 | String platformVersion;
146 | try {
147 | platformVersion = await AudioManager.instance.platformVersion;
148 | } on PlatformException {
149 | platformVersion = 'Failed to get platform version.';
150 | }
151 | if (!mounted) return;
152 |
153 | setState(() {
154 | _platformVersion = platformVersion;
155 | });
156 | }
157 |
158 | @override
159 | Widget build(BuildContext context) {
160 | return MaterialApp(
161 | home: Scaffold(
162 | appBar: AppBar(
163 | title: const Text('Plugin audio player'),
164 | ),
165 | body: Center(
166 | child: Column(
167 | children: [
168 | Text('Running on: $_platformVersion\n'),
169 | Padding(
170 | padding: EdgeInsets.symmetric(horizontal: 16),
171 | child: volumeFrame(),
172 | ),
173 | Expanded(
174 | child: ListView.separated(
175 | itemBuilder: (context, index) {
176 | return ListTile(
177 | title: Text(list[index]["title"],
178 | style: TextStyle(fontSize: 18)),
179 | subtitle: Text(list[index]["desc"]),
180 | onTap: () => AudioManager.instance.play(index: index),
181 | );
182 | },
183 | separatorBuilder: (BuildContext context, int index) =>
184 | Divider(),
185 | itemCount: list.length),
186 | ),
187 | Center(
188 | child: Text(_error != null
189 | ? _error
190 | : "${AudioManager.instance.info.title} lrc text: $_position")),
191 | bottomPanel()
192 | ],
193 | ),
194 | ),
195 | ),
196 | );
197 | }
198 |
199 | Widget bottomPanel() {
200 | return Column(children: [
201 | Padding(
202 | padding: EdgeInsets.symmetric(horizontal: 16),
203 | child: songProgress(context),
204 | ),
205 | Container(
206 | padding: EdgeInsets.symmetric(vertical: 16),
207 | child: Row(
208 | mainAxisAlignment: MainAxisAlignment.spaceAround,
209 | children: [
210 | IconButton(
211 | icon: getPlayModeIcon(playMode),
212 | onPressed: () {
213 | playMode = AudioManager.instance.nextMode();
214 | setState(() {});
215 | }),
216 | IconButton(
217 | iconSize: 36,
218 | icon: Icon(
219 | Icons.skip_previous,
220 | color: Colors.black,
221 | ),
222 | onPressed: () => AudioManager.instance.previous()),
223 | IconButton(
224 | onPressed: () async {
225 | bool playing = await AudioManager.instance.playOrPause();
226 | print("await -- $playing");
227 | },
228 | padding: const EdgeInsets.all(0.0),
229 | icon: Icon(
230 | isPlaying ? Icons.pause : Icons.play_arrow,
231 | size: 48.0,
232 | color: Colors.black,
233 | ),
234 | ),
235 | IconButton(
236 | iconSize: 36,
237 | icon: Icon(
238 | Icons.skip_next,
239 | color: Colors.black,
240 | ),
241 | onPressed: () => AudioManager.instance.next()),
242 | IconButton(
243 | icon: Icon(
244 | Icons.stop,
245 | color: Colors.black,
246 | ),
247 | onPressed: () => AudioManager.instance.stop()),
248 | ],
249 | ),
250 | ),
251 | ]);
252 | }
253 |
254 | Widget getPlayModeIcon(PlayMode playMode) {
255 | switch (playMode) {
256 | case PlayMode.sequence:
257 | return Icon(
258 | Icons.repeat,
259 | color: Colors.black,
260 | );
261 | case PlayMode.shuffle:
262 | return Icon(
263 | Icons.shuffle,
264 | color: Colors.black,
265 | );
266 | case PlayMode.single:
267 | return Icon(
268 | Icons.repeat_one,
269 | color: Colors.black,
270 | );
271 | }
272 | return Container();
273 | }
274 |
275 | Widget songProgress(BuildContext context) {
276 | var style = TextStyle(color: Colors.black);
277 | return Row(
278 | children: [
279 | Text(
280 | _formatDuration(_position),
281 | style: style,
282 | ),
283 | Expanded(
284 | child: Padding(
285 | padding: EdgeInsets.symmetric(horizontal: 5),
286 | child: SliderTheme(
287 | data: SliderTheme.of(context).copyWith(
288 | trackHeight: 2,
289 | thumbColor: Colors.blueAccent,
290 | overlayColor: Colors.blue,
291 | thumbShape: RoundSliderThumbShape(
292 | disabledThumbRadius: 5,
293 | enabledThumbRadius: 5,
294 | ),
295 | overlayShape: RoundSliderOverlayShape(
296 | overlayRadius: 10,
297 | ),
298 | activeTrackColor: Colors.blueAccent,
299 | inactiveTrackColor: Colors.grey,
300 | ),
301 | child: Slider(
302 | value: _slider ?? 0,
303 | onChanged: (value) {
304 | setState(() {
305 | _slider = value;
306 | });
307 | },
308 | onChangeEnd: (value) {
309 | if (_duration != null) {
310 | Duration msec = Duration(
311 | milliseconds:
312 | (_duration.inMilliseconds * value).round());
313 | AudioManager.instance.seekTo(msec);
314 | }
315 | },
316 | )),
317 | ),
318 | ),
319 | Text(
320 | _formatDuration(_duration),
321 | style: style,
322 | ),
323 | ],
324 | );
325 | }
326 |
327 | String _formatDuration(Duration d) {
328 | if (d == null) return "--:--";
329 | int minute = d.inMinutes;
330 | int second = (d.inSeconds > 60) ? (d.inSeconds % 60) : d.inSeconds;
331 | String format = ((minute < 10) ? "0$minute" : "$minute") +
332 | ":" +
333 | ((second < 10) ? "0$second" : "$second");
334 | return format;
335 | }
336 |
337 | Widget volumeFrame() {
338 | return Row(children: [
339 | IconButton(
340 | padding: EdgeInsets.all(0),
341 | icon: Icon(
342 | Icons.audiotrack,
343 | color: Colors.black,
344 | ),
345 | onPressed: () {
346 | AudioManager.instance.setVolume(0);
347 | }),
348 | Expanded(
349 | child: Padding(
350 | padding: EdgeInsets.symmetric(horizontal: 0),
351 | child: Slider(
352 | value: _sliderVolume ?? 0,
353 | onChanged: (value) {
354 | setState(() {
355 | _sliderVolume = value;
356 | AudioManager.instance.setVolume(value, showVolume: true);
357 | });
358 | },
359 | )))
360 | ]);
361 | }
362 | }
363 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: audio_manager_example
2 | description: Demonstrates how to use the audio_manager plugin.
3 | publish_to: 'none'
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.1.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 |
13 | # The following adds the Cupertino Icons font to your application.
14 | # Use with the CupertinoIcons class for iOS style icons.
15 | cupertino_icons: ^0.1.2
16 |
17 | dev_dependencies:
18 | flutter_test:
19 | sdk: flutter
20 |
21 | path_provider: ^1.6.5
22 | audio_manager:
23 | path: ../
24 |
25 | # For information on the generic Dart part of this file, see the
26 | # following page: https://dart.dev/tools/pub/pubspec
27 |
28 | # The following section is specific to Flutter.
29 | flutter:
30 |
31 | # The following line ensures that the Material Icons font is
32 | # included with your application, so that you can use the icons in
33 | # the material Icons class.
34 | uses-material-design: true
35 |
36 | # To add assets to your application, add an assets section, like this:
37 | assets:
38 | - assets/
39 |
40 | # An image asset can refer to one or more resolution-specific "variants", see
41 | # https://flutter.dev/assets-and-images/#resolution-aware.
42 |
43 | # For details regarding adding assets from package dependencies, see
44 | # https://flutter.dev/assets-and-images/#from-packages
45 |
46 | # To add custom fonts to your application, add a fonts section here,
47 | # in this "flutter" section. Each entry in this list should have a
48 | # "family" key with the font family name, and a "fonts" key with a
49 | # list giving the asset and other descriptors for the font. For
50 | # example:
51 | # fonts:
52 | # - family: Schyler
53 | # fonts:
54 | # - asset: fonts/Schyler-Regular.ttf
55 | # - asset: fonts/Schyler-Italic.ttf
56 | # style: italic
57 | # - family: Trajan Pro
58 | # fonts:
59 | # - asset: fonts/TrajanPro.ttf
60 | # - asset: fonts/TrajanPro_Bold.ttf
61 | # weight: 700
62 | #
63 | # For details regarding fonts from package dependencies,
64 | # see https://flutter.dev/custom-fonts/#from-packages
65 |
--------------------------------------------------------------------------------
/example/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:audio_manager_example/main.dart';
12 |
13 | void main() {
14 | testWidgets('Verify Platform version', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(MyApp());
17 |
18 | // Verify that platform version is retrieved.
19 | expect(
20 | find.byWidgetPredicate(
21 | (Widget widget) => widget is Text &&
22 | widget.data.startsWith('Running on:'),
23 | ),
24 | findsOneWidget,
25 | );
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/example/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/web/favicon.png
--------------------------------------------------------------------------------
/example/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/example/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/example/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/example/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | audio_manager_example
18 |
19 |
20 |
21 |
24 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/example/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "audio_manager_example",
3 | "short_name": "audio_manager_example",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "Demonstrates how to use the audio_manager plugin.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | .generated/
16 |
17 | *.pbxuser
18 | *.mode1v3
19 | *.mode2v3
20 | *.perspectivev3
21 |
22 | !default.pbxuser
23 | !default.mode1v3
24 | !default.mode2v3
25 | !default.perspectivev3
26 |
27 | xcuserdata
28 |
29 | *.moved-aside
30 |
31 | *.pyc
32 | *sync/
33 | Icon?
34 | .tags*
35 |
36 | /Flutter/Generated.xcconfig
37 | /Flutter/flutter_export_environment.sh
--------------------------------------------------------------------------------
/ios/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/ios/Assets/.gitkeep
--------------------------------------------------------------------------------
/ios/Classes/AudioManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AudioManager.swift
3 | //
4 | // Created by Jerome Xiong on 2020/1/13.
5 | // Copyright © 2020 JeromeXiong. All rights reserved.
6 | //
7 |
8 | import UIKit
9 | import AVFoundation
10 | import MediaPlayer
11 |
12 | open class AudioManager: NSObject {
13 | public enum Events {
14 | case ready(_ duration: Int), seekComplete(_ position: Int), stop, playing, buffering(Bool, Double), pause, ended, next, previous, timeupdate(_ position: Double, _ duration: Double), error(NSError), volumeChange(Float)
15 | }
16 |
17 | public static let `default`: AudioManager = {
18 | return AudioManager()
19 | }()
20 |
21 | private override init() {
22 | super.init()
23 | setRemoteControl()
24 | NotificationCenter.default.addObserver(self, selector: #selector(volumeChange(n:)), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
25 | }
26 | deinit {
27 | NotificationCenter.default.removeObserver(self)
28 | }
29 | /// 事件回调 ⚠️使用weak防止内存泄露
30 | open var onEvents: ((Events)->Void)?
31 | /// 是否缓存中
32 | open fileprivate(set) var buffering = true {
33 | didSet {
34 | onEvents?(.buffering(buffering, buffer))
35 | }
36 | }
37 | /// 缓存进度
38 | open fileprivate(set) var buffer: Double = 0 {
39 | didSet {
40 | onEvents?(.buffering(buffering, buffer))
41 | }
42 | }
43 | /// 是否正在播放
44 | open fileprivate(set) var playing: Bool = false
45 | /// 最近播放的URL
46 | open fileprivate(set) var url: String?
47 | /// 标题
48 | open var title: String?
49 | /// 描述
50 | open var desc: String?
51 | /// 封面图
52 | open var cover: UIImageView?
53 | /// 播放速率
54 | open var rate: Float = 1 {
55 | didSet {
56 | queue.rate = rate
57 | }
58 | }
59 | /// 是否自动播放
60 | open var isAuto: Bool = true
61 |
62 | /// get total duration /milisecond
63 | open var duration: Int {
64 | let duration = queue.currentItem?.duration ?? CMTime.zero
65 | if CMTimeGetSeconds(duration).isNaN {
66 | return 0
67 | }
68 | return Int(CMTimeGetSeconds(duration)) * 1000
69 | }
70 | /// get current position /milisecond
71 | open var currentTime: Int {
72 | guard let currentTime = queue.currentItem?.currentTime() else {
73 | return 0
74 | }
75 |
76 | if CMTimeGetSeconds(currentTime).isNaN || CMTimeGetSeconds(currentTime).isInfinite{
77 | return 0
78 | }else{
79 | return Int(CMTimeGetSeconds(currentTime)) * 1000
80 | }
81 | }
82 | fileprivate var queue = AVQueuePlayer()
83 | fileprivate var _playingMusic = Dictionary()
84 | fileprivate var timeObserver: Any?
85 | fileprivate var observeStatus: NSKeyValueObservation?
86 | fileprivate var observeLoaded: NSKeyValueObservation?
87 | fileprivate var observeBufferEmpty: NSKeyValueObservation?
88 | fileprivate var observeCanPlay: NSKeyValueObservation?
89 |
90 | fileprivate let session = AVAudioSession.sharedInstance()
91 | fileprivate var interrupterStatus = false
92 |
93 | fileprivate lazy var volumeView: MPVolumeView = {
94 | let volumeView = MPVolumeView()
95 | volumeView.frame = CGRect(x: -100, y: -100, width: 40, height: 40)
96 | return volumeView
97 | }()
98 |
99 | /// 是否显示音量视图
100 | open var showVolumeView: Bool = false {
101 | didSet {
102 | if showVolumeView {
103 | volumeView.removeFromSuperview()
104 | }else {
105 | UIApplication.shared.keyWindow?.addSubview(volumeView)
106 | }
107 | }
108 | }
109 | /// 当前音量
110 | open var currentVolume: Float {
111 | return session.outputVolume
112 | }
113 | }
114 | public extension AudioManager {
115 | /// 必须要调用 start method 才能进行其他操作
116 | func start(_ link: String, isLocal: Bool = false) {
117 | var playerItem: AVPlayerItem? = _playingMusic[link] as? AVPlayerItem
118 | if playerItem == nil {
119 | stop(url)
120 | if isLocal {
121 | guard let path = Bundle.main.path(forResource: link, ofType: "") else {
122 | onError(.custom(-1, "link [\(link)] is invalid"))
123 | return
124 | }
125 | playerItem = AVPlayerItem(url: URL(fileURLWithPath: path))
126 | }else {
127 | guard let path = transformURLString(link)?.url else {
128 | onError(.custom(-1, "link [\(link)] is invalid"))
129 | return
130 | }
131 | playerItem = AVPlayerItem(url: path)
132 | }
133 | _playingMusic[link] = playerItem
134 | queue.replaceCurrentItem(with: playerItem)
135 | queue.actionAtItemEnd = .none
136 | queue.rate = rate
137 | if #available(iOS 10.0, *) {
138 | queue.automaticallyWaitsToMinimizeStalling = false
139 | }
140 | url = link
141 | }else {
142 | play(link)
143 | }
144 |
145 | observingProps()
146 | observingTimeChanges()
147 | setRemoteInfo()
148 | activateSession()
149 | UIApplication.shared.beginReceivingRemoteControlEvents()
150 | NotificationCenter.default.addObserver(self, selector: #selector(playerFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: queue.currentItem)
151 | }
152 |
153 | func seek(to position: Double, link: String? = nil) {
154 | guard let _url = link ?? url, let playerItem = _playingMusic[_url] as? AVPlayerItem else {
155 | onError(.notReady)
156 | return
157 | }
158 | if queue.currentItem?.status != .readyToPlay { return }
159 | let timescale = queue.currentItem?.asset.duration.timescale ?? 0
160 | playerItem.seek(to: CMTime(seconds: position, preferredTimescale: timescale)) {[weak self] (flag) in
161 | if flag {
162 | self?.onEvents?(.seekComplete(Int(position * 1000)))
163 | }
164 | }
165 | }
166 |
167 | /// 设置音量大小 0~1
168 | func setVolume(_ value: Float, show volume: Bool = true) {
169 | var value = min(value, 1)
170 | value = max(value, 0)
171 | let volumeView = MPVolumeView()
172 | var slider = UISlider()
173 | for view in volumeView.subviews {
174 | if NSStringFromClass(view.classForCoder) == "MPVolumeSlider" {
175 | slider = view as! UISlider
176 | break
177 | }
178 | }
179 | DispatchQueue.main.asyncAfter(deadline: .now()+0.01) {
180 | slider.value = value
181 | }
182 | if volume {
183 | if !showVolumeView {
184 | showVolumeView = true
185 | }
186 | }else {
187 | showVolumeView = false
188 | }
189 | }
190 |
191 | /// 播放▶️音乐🎵
192 | func play(_ link: String? = nil) {
193 | if playing { return }
194 | guard let playerItem = _playingMusic[link ?? url ?? ""] as? AVPlayerItem, playerItem.status == .readyToPlay else {
195 | onError(.notReady)
196 | return
197 | }
198 | if #available(iOS 10.0, *) {
199 | queue.playImmediately(atRate: rate)
200 | } else {
201 | queue.play()
202 | queue.rate = rate
203 | }
204 | playing = true
205 | onEvents?(.playing)
206 | }
207 |
208 | /// 暂停⏸音乐🎵
209 | func pause(_ link: String? = nil) {
210 | if !playing { return }
211 | guard let _ = _playingMusic[link ?? url ?? ""] as? AVPlayerItem else {
212 | onError(.notReady)
213 | return
214 | }
215 | queue.pause()
216 | playing = false
217 | onEvents?(.pause)
218 | }
219 |
220 | /// 停止⏹音乐🎵
221 | func stop(_ link: String? = nil) {
222 | if let observer = timeObserver {
223 | timeObserver = nil
224 | queue.removeTimeObserver(observer)
225 | NotificationCenter.default.removeObserver(self)
226 | }
227 | let playerItem = _playingMusic[link ?? url ?? ""] as? AVPlayerItem
228 | if let playerItem = playerItem {
229 | seek(to: 0, link: link ?? url ?? "")
230 | queue.remove(playerItem)
231 | _playingMusic.removeValue(forKey: link ?? url ?? "")
232 | }
233 | playing = false
234 | onEvents?(.stop)
235 | }
236 |
237 | /// 清除所有播放信息
238 | func clean() {
239 | stop()
240 | queue.removeAllItems()
241 | _playingMusic.removeAll()
242 | UIApplication.shared.endReceivingRemoteControlEvents()
243 | }
244 | }
245 | private enum AudioError {
246 | case notReady
247 | case custom(Int, String)
248 |
249 | var description: (Int, String) {
250 | switch self {
251 | case .notReady:
252 | return (0, "not ready to play")
253 | case let .custom(code, msg):
254 | return (code, msg)
255 | }
256 | }
257 | }
258 | fileprivate extension AudioManager {
259 | func onError(_ error: AudioError) {
260 | onEvents?(.error(NSError(domain: domain, code: error.description.0, userInfo: ["msg": error.description.1])))
261 | }
262 | var domain: String {
263 | return "\((#file as NSString).lastPathComponent)[\(#line)])"
264 | }
265 | func transformURLString(_ string: String) -> URLComponents? {
266 | guard let urlPath = string.components(separatedBy: "?").first else {
267 | return nil
268 | }
269 |
270 | if urlPath.contains("file:") {
271 | return URLComponents(url: URL(fileURLWithPath: urlPath), resolvingAgainstBaseURL: false)
272 | }
273 |
274 | var components = URLComponents(string: urlPath)
275 | if let queryString = string.components(separatedBy: "?").last {
276 | components?.queryItems = []
277 | let queryItems = queryString.components(separatedBy: "&")
278 | for queryItem in queryItems {
279 | guard let itemName = queryItem.components(separatedBy: "=").first,
280 | let itemValue = queryItem.components(separatedBy: "=").last else {
281 | continue
282 | }
283 | components?.queryItems?.append(URLQueryItem(name: itemName, value: itemValue))
284 | }
285 | }
286 | return components!
287 | }
288 | @objc func playerFinishPlaying(_ n: Notification) {
289 | queue.seek(to: CMTime.zero)
290 | stop()
291 | onEvents?(.ended)
292 | }
293 | /// 监听属性变化
294 | func observingProps() {
295 | observeStatus = queue.currentItem?.observe(\.status) {
296 | [weak self] _playerItem, change in
297 | guard let `self` = self else { return }
298 | if _playerItem.status == .readyToPlay {
299 | self.playing = self.isAuto
300 | if self.isAuto {
301 | self.onEvents?(.playing)
302 | }else {
303 | self.queue.pause()
304 | }
305 | self.onEvents?(.ready(self.duration))
306 | }else {
307 | self.playing = false
308 | }
309 | }
310 |
311 | observeLoaded = queue.currentItem?.observe(\.loadedTimeRanges) {
312 | [weak self] _playerItem, change in
313 | guard let `self` = self else { return }
314 | let ranges = _playerItem.loadedTimeRanges
315 | guard let timeRange = ranges.first as? CMTimeRange else { return }
316 | let start = timeRange.start.seconds
317 | let duration = timeRange.duration.seconds
318 | let cached = start + duration
319 |
320 | let total = _playerItem.duration.seconds
321 | self.buffer = cached / total * 100
322 | }
323 |
324 | observeBufferEmpty = queue.currentItem?.observe(\.isPlaybackBufferEmpty) {
325 | [weak self] _playerItem, change in
326 | self?.buffering = true
327 | }
328 |
329 | observeCanPlay = queue.currentItem?.observe(\.isPlaybackLikelyToKeepUp) {
330 | [weak self] _playerItem, change in
331 | self?.buffering = false
332 | }
333 | }
334 | /// 监听时间变化
335 | func observingTimeChanges() {
336 | if let observer = timeObserver {
337 | timeObserver = nil
338 | queue.removeTimeObserver(observer)
339 | }
340 | let time = CMTimeMake(value: 1, timescale: 1)
341 | timeObserver = queue.addPeriodicTimeObserver(forInterval: time, queue: DispatchQueue.main, using: {[weak self] (currentPlayerTime) in
342 | self?.updateLockInfo()
343 | })
344 | }
345 | }
346 | // MARK: system
347 | public extension AudioManager {
348 | /// 注册后台播放
349 | /// register in application didFinishLaunchingWithOptions method
350 | func registerBackground(){
351 | NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
352 | NotificationCenter.default.addObserver(self, selector: #selector(audioSessionInterrupted(_:)), name: AVAudioSession.interruptionNotification, object: nil)
353 | }
354 |
355 | func activateSession() {
356 | do{
357 | try session.setActive(true)
358 | try session.setCategory(.playback, options: [.allowBluetooth, .mixWithOthers])
359 | if #available(iOS 10.0, *) {
360 | try session.setCategory(.playback, options: [.allowAirPlay, .allowBluetoothA2DP, .mixWithOthers])
361 | }
362 | try session.overrideOutputAudioPort(.speaker)
363 |
364 | }catch{}
365 | }
366 |
367 | /// 中断结束后继续播放
368 | /// register in application applicationDidBecomeActive
369 | func interrupterAction(_ isplay: Bool = false) {
370 | if playing {
371 | pause()
372 | interrupterStatus = true
373 | return
374 | }
375 | if interrupterStatus && isplay {
376 | play()
377 | interrupterStatus = false
378 | }
379 | }
380 | }
381 | fileprivate extension AudioManager {
382 | /// 锁屏操作
383 | func setRemoteControl() {
384 | let remote = MPRemoteCommandCenter.shared()
385 | remote.playCommand.removeTarget(self)
386 | remote.pauseCommand.removeTarget(self)
387 | remote.togglePlayPauseCommand.removeTarget(self)
388 | if #available(iOS 9.1, *) {
389 | remote.changePlaybackPositionCommand.removeTarget(self)
390 | }
391 | remote.previousTrackCommand.removeTarget(self)
392 | remote.nextTrackCommand.removeTarget(self)
393 |
394 | remote.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
395 | self.play()
396 | return .success
397 | }
398 | remote.pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
399 | self.pause()
400 | return .success
401 | }
402 | remote.togglePlayPauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
403 | if self.playing {
404 | self.pause()
405 | }else {
406 | self.play()
407 | }
408 | return .success
409 | }
410 | if #available(iOS 9.1, *) {
411 | remote.changePlaybackPositionCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
412 | let playback = event as! MPChangePlaybackPositionCommandEvent
413 | self.seek(to: playback.positionTime)
414 | return .success
415 | }
416 | }
417 | remote.previousTrackCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
418 | self.onEvents?(.previous)
419 | return .success
420 | }
421 | remote.nextTrackCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
422 | self.onEvents?(.next)
423 | return .success
424 | }
425 | }
426 |
427 | /// 锁屏信息
428 | func updateLockInfo() {
429 | guard let _ = url, playing == true else {
430 | return
431 | }
432 | let duration = Double(CMTimeGetSeconds(queue.currentItem?.duration ?? .zero))
433 | let currentTime = Double(CMTimeGetSeconds(queue.currentTime()))
434 | if duration.isNaN || currentTime.isNaN { return }
435 |
436 | setRemoteInfo()
437 | onEvents?(.timeupdate(currentTime, duration))
438 | }
439 | func setRemoteInfo() {
440 | let center = MPNowPlayingInfoCenter.default()
441 | var infos = [String: Any]()
442 |
443 | infos[MPMediaItemPropertyTitle] = title
444 | infos[MPMediaItemPropertyArtist] = desc
445 | infos[MPMediaItemPropertyPlaybackDuration] = Double(duration / 1000)
446 | infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentTime / 1000)
447 | infos[MPNowPlayingInfoPropertyPlaybackRate] = queue.rate
448 | queue.rate = rate
449 |
450 | let image = cover?.image ?? UIImage()
451 | if #available(iOS 11.0, *) {
452 | infos[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image)
453 | } else {
454 | let cover = image.withText(self.desc ?? "")!
455 | if #available(iOS 10.0, *) {
456 | infos[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: CGSize(width: 200,height: 200), requestHandler: { (size) -> UIImage in
457 | return cover
458 | })
459 |
460 | } else {
461 | infos[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image)
462 | }
463 | }
464 |
465 | center.nowPlayingInfo = infos
466 | }
467 | }
468 |
469 | fileprivate extension AudioManager {
470 | @objc func audioSessionInterrupted(_ n: Notification) {
471 | print("\n\n\n > > > > > Error Audio Session Interrupted \n\n\n")
472 | guard let userInfo = n.userInfo,
473 | let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
474 | let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
475 | return
476 | }
477 | if type == .began {
478 | print("Interruption began, take appropriate actions")
479 | interrupterAction()
480 | }else {
481 | interrupterAction(true)
482 | if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
483 | let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
484 | if options.contains(.shouldResume) {
485 | print("Interruption Ended - playback should resume")
486 | } else {
487 | print("Interruption Ended - playback should NOT resume")
488 | }
489 | }
490 | }
491 | }
492 | @objc func handleRouteChange(_ n: Notification) {
493 | print("\n\n\n > > > > > Audio Route Changed ","\n\n\n")
494 | guard let userInfo = n.userInfo,
495 | let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
496 | let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
497 | return
498 | }
499 |
500 | let ports : [AVAudioSession.Port] = [.airPlay,.builtInMic,.bluetoothA2DP,.bluetoothHFP,.builtInReceiver,.bluetoothLE,.builtInReceiver,.headphones,.headsetMic]
501 | switch reason {
502 | case .newDeviceAvailable: //Get Notification When Device Connect
503 | let session = AVAudioSession.sharedInstance()
504 | for output in session.currentRoute.outputs where ports.contains(where: {$0 == output.portType}) {
505 | break
506 | }
507 | case .oldDeviceUnavailable: //Get Notification When Device Disconnect
508 | if let previousRoute =
509 | userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
510 | for output in previousRoute.outputs where ports.contains(where: {$0 == output.portType}) {
511 | //Check Player State
512 |
513 | break
514 | }
515 | }
516 | default: ()
517 | }
518 | }
519 | @objc func volumeChange(n: Notification){
520 | guard let userInfo = n.userInfo, let parameter = userInfo["AVSystemController_AudioCategoryNotificationParameter"] as? String,
521 | let reason = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String,
522 | let _volume = userInfo["AVSystemController_AudioVolumeNotificationParameter"] as? NSNumber else {
523 | return
524 | }
525 | if (parameter == "Audio/Video") {
526 | if (reason == "ExplicitVolumeChange") {
527 | let volume = _volume.floatValue
528 | print("当前音量\(volume)")
529 | self.onEvents?(.volumeChange(volume))
530 | }
531 | }
532 | }
533 | }
534 | extension UIImage {
535 | func withText(_ text: String) -> UIImage? {
536 | UIGraphicsBeginImageContextWithOptions(self.size, false, 0.0)
537 | self.draw(in: CGRect(origin: .zero, size: self.size))
538 |
539 | // 将文字绘制到图片上面
540 | let rect = CGRect(origin: CGPoint(x: 0, y: self.size.height*0.4), size: self.size)
541 |
542 | // 设置文字样式
543 | let style = NSMutableParagraphStyle()
544 | style.alignment = .center
545 |
546 | let dict: [NSAttributedString.Key: Any] = [
547 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
548 | NSAttributedString.Key.foregroundColor: UIColor.green,
549 | NSAttributedString.Key.paragraphStyle: style
550 | ]
551 | (text as NSString).draw(in: rect, withAttributes: dict)
552 |
553 | let resultImage = UIGraphicsGetImageFromCurrentImageContext()
554 | UIGraphicsEndImageContext();
555 | return resultImage
556 | }
557 | }
558 |
559 |
--------------------------------------------------------------------------------
/ios/Classes/AudioManagerPlugin.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface AudioManagerPlugin : NSObject
4 | @end
5 |
--------------------------------------------------------------------------------
/ios/Classes/AudioManagerPlugin.m:
--------------------------------------------------------------------------------
1 | #import "AudioManagerPlugin.h"
2 | #if __has_include()
3 | #import
4 | #else
5 | // Support project import fallback if the generated compatibility header
6 | // is not copied when this plugin is created as a library.
7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
8 | #import "audio_manager-Swift.h"
9 | #endif
10 |
11 | @implementation AudioManagerPlugin
12 | + (void)registerWithRegistrar:(NSObject*)registrar {
13 | [SwiftAudioManagerPlugin registerWithRegistrar:registrar];
14 | }
15 | @end
16 |
--------------------------------------------------------------------------------
/ios/Classes/SwiftAudioManagerPlugin.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | public class SwiftAudioManagerPlugin: NSObject, FlutterPlugin {
5 | fileprivate var registrar: FlutterPluginRegistrar!
6 | fileprivate static let instance: SwiftAudioManagerPlugin = {
7 | return SwiftAudioManagerPlugin()
8 | }()
9 | public static func register(with registrar: FlutterPluginRegistrar) {
10 | let channel = FlutterMethodChannel(name: "audio_manager", binaryMessenger: registrar.messenger())
11 | registrar.addMethodCallDelegate(instance, channel: channel)
12 | registrar.addApplicationDelegate(instance)
13 |
14 | instance.registrar = registrar
15 | AudioManager.default.onEvents = { event in
16 | switch event {
17 | case .ready(let duration):
18 | channel.invokeMethod("ready", arguments: duration)
19 | case .seekComplete(let position):
20 | channel.invokeMethod("seekComplete", arguments: position)
21 | case .buffering(let buffering, let buffer):
22 | channel.invokeMethod("buffering", arguments: ["buffering": buffering, "buffer": buffer])
23 | case .playing, .pause:
24 | channel.invokeMethod("playstatus", arguments: AudioManager.default.playing)
25 | case .timeupdate(let position, let duration):
26 | channel.invokeMethod("timeupdate", arguments: ["position": Int(position*1000), "duration": Int(duration*1000)])
27 | case .error(let e):
28 | DispatchQueue.main.async {
29 | AudioManager.default.clean()
30 | }
31 | channel.invokeMethod("error", arguments: e.description)
32 | case .next:
33 | channel.invokeMethod("next", arguments: nil)
34 | case .previous:
35 | channel.invokeMethod("previous", arguments: nil)
36 | case .ended:
37 | channel.invokeMethod("ended", arguments: nil)
38 | case .stop:
39 | channel.invokeMethod("stop", arguments: nil)
40 | case .volumeChange(let value):
41 | channel.invokeMethod("volumeChange", arguments: value)
42 | }
43 | }
44 | }
45 |
46 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
47 | let arguments = call.arguments as? Dictionary ?? [:]
48 | let url = arguments["url"] as? String
49 | print("arguments: ", arguments)
50 | switch call.method {
51 | case "getPlatformVersion":
52 | result("iOS " + UIDevice.current.systemVersion)
53 | case "start":
54 | guard var url = url else {
55 | result("参数错误")
56 | return
57 | }
58 | AudioManager.default.title = arguments["title"] as? String
59 | AudioManager.default.desc = arguments["desc"] as? String
60 | if let cover = arguments["cover"] as? String, let isLocalCover = arguments["isLocalCover"] as? Bool {
61 | if !isLocalCover, let _cover = URL(string: cover) {
62 | let request = URLRequest(url: _cover)
63 | NSURLConnection.sendAsynchronousRequest(request, queue: OperationQueue.main) { (_, data, error) in
64 | if let data = data {
65 | AudioManager.default.cover = UIImageView(image: UIImage(data: data))
66 | }
67 | if let error = error as NSError? {
68 | result(error.description)
69 | }
70 | }
71 | }else if let path = self.getLocal(SwiftAudioManagerPlugin.instance.registrar, path: cover) {
72 | AudioManager.default.cover = UIImageView(image: UIImage(contentsOfFile: path))
73 | }
74 | }
75 | let isLocal = arguments["isLocal"] as? Bool ?? false
76 | if isLocal {
77 | url = SwiftAudioManagerPlugin.instance.registrar.lookupKey(forAsset: url)
78 | }
79 | let isAuto = arguments["isAuto"] as? Bool ?? true
80 | AudioManager.default.isAuto = isAuto
81 | AudioManager.default.start(url, isLocal: isLocal)
82 | case "playOrPause":
83 | if AudioManager.default.playing {
84 | AudioManager.default.pause(url)
85 | }else {
86 | AudioManager.default.play(url)
87 | }
88 | result(AudioManager.default.playing)
89 | case "play":
90 | AudioManager.default.play(url)
91 | result(AudioManager.default.playing)
92 | case "pause":
93 | AudioManager.default.pause(url)
94 | result(AudioManager.default.playing)
95 | case "stop":
96 | AudioManager.default.stop()
97 | case "release":
98 | AudioManager.default.clean()
99 | case "updateLrc":
100 | AudioManager.default.desc = arguments["lrc"] as? String
101 | case "seekTo":
102 | guard let position = arguments["position"] as? Double else {
103 | result("参数错误")
104 | return
105 | }
106 | AudioManager.default.seek(to: position/1000, link: url)
107 | case "rate":
108 | guard let rate = arguments["rate"] as? Double else {
109 | result("参数错误")
110 | return
111 | }
112 | AudioManager.default.rate = Float(rate)
113 | case "setVolume":
114 | guard let value = arguments["value"] as? Double else {
115 | result("参数错误")
116 | return
117 | }
118 | let showVolume = arguments["showVolume"] as? Bool ?? false
119 | AudioManager.default.setVolume(Float(value), show: showVolume)
120 | case "currentVolume":
121 | result(AudioManager.default.currentVolume)
122 | default:
123 | result(FlutterMethodNotImplemented)
124 | }
125 | }
126 |
127 | func getLocal(_ registrar: FlutterPluginRegistrar, path: String) -> String? {
128 | let key = registrar.lookupKey(forAsset: path)
129 | return Bundle.main.path(forResource: key, ofType: nil)
130 | }
131 |
132 | public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any] = [:]) -> Bool {
133 | AudioManager.default.registerBackground()
134 | return true
135 | }
136 |
137 | // public func applicationWillResignActive(_ application: UIApplication) {
138 | // backTaskId = backgroundPlayerID(backTaskId)
139 | // }
140 |
141 | private var backTaskId: UIBackgroundTaskIdentifier = .invalid
142 | /// 设置后台任务ID
143 | private func backgroundPlayerID(_ backTaskId: UIBackgroundTaskIdentifier) -> UIBackgroundTaskIdentifier {
144 | var taskId = UIBackgroundTaskIdentifier.invalid;
145 | taskId = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
146 | if taskId != .invalid && backTaskId != .invalid {
147 | UIApplication.shared.endBackgroundTask(backTaskId)
148 | }
149 | return taskId
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/ios/audio_manager.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
3 | # Run `pod lib lint audio_manager.podspec' to validate before publishing.
4 | #
5 | Pod::Spec.new do |s|
6 | s.name = 'audio_manager'
7 | s.version = '0.0.1'
8 | s.summary = 'A new flutter plugin project.'
9 | s.description = <<-DESC
10 | A new flutter plugin project.
11 | DESC
12 | s.homepage = 'http://example.com'
13 | s.license = { :file => '../LICENSE' }
14 | s.author = { 'Your Company' => 'email@example.com' }
15 | s.source = { :path => '.' }
16 | s.source_files = 'Classes/**/*'
17 | s.dependency 'Flutter'
18 | s.platform = :ios, '8.0'
19 |
20 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
22 | s.swift_version = '5.0'
23 | end
24 |
--------------------------------------------------------------------------------
/lib/audio_manager.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:io';
3 | import 'dart:math';
4 | import 'package:flutter/services.dart';
5 | import 'package:audio_manager/src/AudioType.dart';
6 | import 'package:audio_manager/src/AudioInfo.dart';
7 |
8 | export 'package:audio_manager/src/AudioInfo.dart';
9 | export 'package:audio_manager/src/AudioType.dart';
10 |
11 | class AudioManager {
12 | static AudioManager? _instance;
13 | static AudioManager get instance => _getInstance();
14 |
15 | static _getInstance() {
16 | if (_instance == null) {
17 | _instance = new AudioManager._();
18 | }
19 | return _instance;
20 | }
21 |
22 | static MethodChannel _channel = const MethodChannel('audio_manager');
23 |
24 | AudioManager._() {
25 | _channel.setMethodCallHandler(_handler);
26 | getCurrentVolume();
27 | }
28 |
29 | /// 是否资源加载中
30 | bool get isLoading => _isLoading;
31 | bool _isLoading = true;
32 |
33 | /// Current playback status
34 | bool get isPlaying => _playing;
35 | bool _playing = false;
36 |
37 | void _setPlaying(bool playing) {
38 | if (_playing == playing) return;
39 | _playing = playing;
40 | _onEvents(AudioManagerEvents.playstatus, _playing);
41 | }
42 |
43 | /// Current playing time (ms
44 | Duration get position => _position;
45 | Duration _position = Duration(milliseconds: 0);
46 |
47 | /// Total current playing time (ms
48 | Duration get duration => _duration;
49 | Duration _duration = Duration(milliseconds: 0);
50 |
51 | /// get current volume 0~1
52 | double get volume => _volume;
53 | double _volume = 0;
54 |
55 | /// If there are errors, return details
56 | String? get error => _error;
57 | String? _error;
58 |
59 | /// list of playback. Used to record playlists
60 | List get audioList => _audioList;
61 | List _audioList = [];
62 |
63 | /// Set up playlists. Use the [play] or [start] method if you want to play
64 | set audioList(List list) {
65 | if (list.length == 0) throw "[list] can not be null or empty";
66 | _audioList = list;
67 | _info = _initRandom();
68 | }
69 |
70 | /// Currently playing subscript of [audioList]
71 | int get curIndex => _curIndex;
72 | int _curIndex = 0;
73 | List _randoms = [];
74 |
75 | /// Play mode [sequence, shuffle, single], default `sequence`
76 | PlayMode get playMode => _playMode;
77 | PlayMode _playMode = PlayMode.sequence;
78 |
79 | /// Whether to internally handle [next] and [previous] events. default true
80 | bool intercepter = true;
81 |
82 | /// Whether to auto play. default true
83 | bool get auto => _auto;
84 | bool _auto = true;
85 |
86 | /// Playback info
87 | AudioInfo? get info => _info;
88 | AudioInfo? _info;
89 |
90 | Future _handler(MethodCall call) {
91 | switch (call.method) {
92 | case "ready":
93 | _isLoading = false;
94 | _duration = Duration(milliseconds: call.arguments ?? 0);
95 | _onEvents(AudioManagerEvents.ready, _duration);
96 | break;
97 | case "seekComplete":
98 | _position = Duration(milliseconds: call.arguments ?? 0);
99 | if (_duration.inMilliseconds != 0)
100 | _onEvents(AudioManagerEvents.seekComplete, _position);
101 | break;
102 | case "buffering":
103 | _onEvents(AudioManagerEvents.buffering, call.arguments);
104 | break;
105 | case "playstatus":
106 | _setPlaying(call.arguments ?? false);
107 | break;
108 | case "timeupdate":
109 | _error = null;
110 | _position = Duration(milliseconds: call.arguments["position"] ?? 0);
111 | _duration = Duration(milliseconds: call.arguments["duration"] ?? 0);
112 | if (!_playing) _setPlaying(true);
113 | if (_position.inMilliseconds < 0 || _duration.inMilliseconds <= 0)
114 | break;
115 | if (_position > _duration) {
116 | _position = _duration;
117 | _setPlaying(false);
118 | }
119 | _onEvents(AudioManagerEvents.timeupdate,
120 | {"position": _position, "duration": _duration});
121 | break;
122 | case "error":
123 | _error = call.arguments;
124 | if (_playing) _setPlaying(false);
125 | _onEvents(AudioManagerEvents.error, _error);
126 | break;
127 | case "next":
128 | if (intercepter) next();
129 | _onEvents(AudioManagerEvents.next, null);
130 | break;
131 | case "previous":
132 | if (intercepter) previous();
133 | _onEvents(AudioManagerEvents.previous, null);
134 | break;
135 | case "ended":
136 | _onEvents(AudioManagerEvents.ended, null);
137 | break;
138 | case "stop":
139 | _onEvents(AudioManagerEvents.stop, null);
140 | _reset();
141 | break;
142 | case "volumeChange":
143 | _volume = call.arguments;
144 | _onEvents(AudioManagerEvents.volumeChange, _volume);
145 | break;
146 | default:
147 | _onEvents(AudioManagerEvents.unknow, call.arguments);
148 | break;
149 | }
150 | return Future.value(true);
151 | }
152 |
153 | String _preprocessing() {
154 | var errMsg = "";
155 | if (_info == null) errMsg = "you must invoke the [start] method first";
156 | if (_error != null) errMsg = _error!;
157 | if (_isLoading) errMsg = "audio resource loading....";
158 |
159 | if (errMsg.isNotEmpty) _onEvents(AudioManagerEvents.error, errMsg);
160 | return errMsg;
161 | }
162 |
163 | Events? _events;
164 | bool _initialize = false;
165 |
166 | /// callback events
167 | void onEvents(Events events) {
168 | _events = events;
169 | }
170 |
171 | void _onEvents(AudioManagerEvents events, args) {
172 | if (_events == null) return;
173 | _events!(events, args);
174 | }
175 |
176 | Future get platformVersion async {
177 | final String version = await _channel.invokeMethod('getPlatformVersion');
178 | return version;
179 | }
180 |
181 | /// Initial playback. Preloaded playback information
182 | ///
183 | /// `url`: Playback address, `network` address or` asset` address.
184 | ///
185 | /// `title`: Notification play title
186 | ///
187 | /// `desc`: Notification details; `cover`: cover image address, `network` address, or `asset` address;
188 | /// `auto`: Whether to play automatically, default is true;
189 | Future start(String url, String title,
190 | {required String desc, required String cover, bool? auto}) async {
191 | if (url.isEmpty) return "[url] can not be null or empty";
192 | if (title.isEmpty) return "[title] can not be null or empty";
193 | cover = cover;
194 | desc = desc;
195 |
196 | _info = AudioInfo(url, title: title, desc: desc, coverUrl: cover);
197 | _audioList.insert(0, _info!);
198 | return await play(index: 0, auto: auto);
199 | }
200 |
201 | /// This will load the file from the file-URI given by:
202 | /// `'file://${file.path}'`.
203 | Future file(File file, String title,
204 | {required String desc, required String cover, required bool auto}) async {
205 | return await start("file://${file.path}", title,
206 | desc: desc, cover: cover, auto: auto);
207 | }
208 |
209 | Future startInfo(AudioInfo audio, {required bool auto}) async {
210 | return await start(audio.url, audio.title,
211 | desc: audio.desc, cover: audio.coverUrl);
212 | }
213 |
214 | /// Play specified subscript audio if you want
215 | Future play({int? index, bool? auto}) async {
216 | if (index != null && (index < 0 || index >= _audioList.length))
217 | throw "invalid index";
218 | _auto = auto ?? true;
219 | _curIndex = index ?? _curIndex;
220 | final random = _initRandom();
221 | // Do not replay the same url
222 | if (_info!.url != random.url) {
223 | stop();
224 | _isLoading = true;
225 | _initialize = true;
226 | }
227 | _info = random;
228 | _onEvents(AudioManagerEvents.start, _info);
229 |
230 | final regx = new RegExp(r'^(http|https|file):\/\/\/?([\w.]+\/?)\S*');
231 | final result = await _channel.invokeMethod('start', {
232 | "url": _info!.url,
233 | "title": _info!.title,
234 | "desc": _info!.desc,
235 | "cover": _info!.coverUrl,
236 | "isAuto": _auto,
237 | "isLocal": !regx.hasMatch(_info!.url),
238 | "isLocalCover": !regx.hasMatch(_info!.coverUrl),
239 | });
240 | return result;
241 | }
242 |
243 | /// Play or pause; that is, pause if currently playing, otherwise play
244 | ///
245 | /// ⚠️ Must be preloaded
246 | ///
247 | /// [return] Returns the current playback status
248 | Future playOrPause() async {
249 | if (_preprocessing().isNotEmpty) return false;
250 |
251 | if (_initialize == false && _playing == false) {
252 | await play(index: _curIndex, auto: true);
253 | return true;
254 | } else {
255 | bool playing = await _channel.invokeMethod("playOrPause");
256 | _setPlaying(playing);
257 | return playing;
258 | }
259 | }
260 |
261 | /// to play status
262 | Future toPlay() async {
263 | if (_preprocessing().isNotEmpty) return false;
264 | bool playing = await _channel.invokeMethod("play");
265 | _setPlaying(playing);
266 | return playing;
267 | }
268 |
269 | /// to pause status
270 | Future toPause() async {
271 | if (_preprocessing().isNotEmpty) return false;
272 | bool playing = await _channel.invokeMethod("pause");
273 | _setPlaying(playing);
274 | return playing;
275 | }
276 |
277 | /// `position` Move location millisecond timestamp.
278 | ///
279 | /// ⚠️ You must after [AudioManagerEvents.ready] event invoked before you can change the playback progress
280 | Future seekTo(Duration position) async {
281 | if (_preprocessing().isNotEmpty) return _preprocessing();
282 | if (position.inMilliseconds < 0 ||
283 | position.inMilliseconds > duration.inMilliseconds)
284 | return "[position] must be greater than 0 and less than the total duration";
285 | return await _channel
286 | .invokeMethod("seekTo", {"position": position.inMilliseconds});
287 | }
288 |
289 | /// `rate` Play rate, default [AudioRate.rate100] is 1.0
290 | Future setRate(AudioRate rate) async {
291 | if (_preprocessing().isNotEmpty) return _preprocessing();
292 | const _rates = [0.5, 0.75, 1, 1.5, 1.75, 2];
293 | rate = rate;
294 | double _rate = _rates[rate.index].toDouble();
295 | return await _channel.invokeMethod("rate", {"rate": _rate});
296 | }
297 |
298 | /// stop play
299 | stop() {
300 | _reset();
301 | _initialize = false;
302 | _channel.invokeMethod("stop");
303 | }
304 |
305 | _reset() {
306 | if (_isLoading) return;
307 | _duration = Duration(milliseconds: 0);
308 | _position = Duration(milliseconds: 0);
309 | _setPlaying(false);
310 | _onEvents(AudioManagerEvents.timeupdate,
311 | {"position": _position, "duration": _duration});
312 | }
313 |
314 | /// release all resource
315 | release() {
316 | _reset();
317 | _channel.invokeListMethod("release");
318 | }
319 |
320 | /// Update play details
321 | updateLrc(String lrc) {
322 | if (_preprocessing().isNotEmpty) return _preprocessing();
323 | _channel.invokeMethod("updateLrc", {"lrc": lrc});
324 | }
325 |
326 | /// Switch playback mode. `Playmode` priority is greater than `index`
327 | PlayMode nextMode({PlayMode? playMode, int? index}) {
328 | int mode = index ?? (_playMode.index + 1) % 3;
329 | if (playMode != null) mode = playMode.index;
330 | switch (mode) {
331 | case 0:
332 | _playMode = PlayMode.sequence;
333 | break;
334 | case 1:
335 | _playMode = PlayMode.shuffle;
336 | break;
337 | case 2:
338 | _playMode = PlayMode.single;
339 | break;
340 | default:
341 | _playMode = PlayMode.sequence;
342 | break;
343 | }
344 | return _playMode;
345 | }
346 |
347 | AudioInfo _initRandom() {
348 | if (playMode == PlayMode.shuffle) {
349 | if (_randoms.length != _audioList.length) {
350 | _randoms = _audioList.asMap().keys.toList();
351 | _randoms.shuffle();
352 | }
353 | _curIndex = _randoms[_curIndex];
354 | }
355 | if (_curIndex >= _audioList.length) {
356 | _curIndex = _audioList.length - 1;
357 | }
358 | if (_curIndex < 0) {
359 | _curIndex = 0;
360 | }
361 | return _audioList[_curIndex];
362 | }
363 |
364 | /// play next audio
365 | Future next() async {
366 | if (playMode != PlayMode.single) {
367 | _curIndex = (_curIndex + 1) % _audioList.length;
368 | }
369 | return await play();
370 | }
371 |
372 | /// play previous audio
373 | Future previous() async {
374 | if (playMode != PlayMode.single) {
375 | int index = _curIndex - 1;
376 | _curIndex = index < 0 ? _audioList.length - 1 : index;
377 | }
378 | return await play();
379 | }
380 |
381 | /// set volume range(0~1). `showVolume`: show volume view or not and this is only in iOS
382 | /// ⚠️ IOS simulator is invalid, please use real machine
383 | Future setVolume(double value, {bool showVolume = false}) async {
384 | var volume = min(value, 1);
385 | value = max(value, 0);
386 | final result = await _channel
387 | .invokeMethod("setVolume", {"value": volume, "showVolume": showVolume});
388 | return result;
389 | }
390 |
391 | /// get current volume
392 | Future getCurrentVolume() async {
393 | _volume = await _channel.invokeMethod("currentVolume");
394 | return _volume;
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/lib/audio_manager_web.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:html';
3 |
4 | import 'package:flutter/services.dart';
5 | import 'package:flutter_web_plugins/flutter_web_plugins.dart';
6 | import 'audio_manager.dart';
7 |
8 | class WrappedPlayer {
9 | double pausedAt = 0;
10 | double currentVolume = 1;
11 | double currentRate = 1;
12 | PlayMode playMode = PlayMode.sequence;
13 | String currentUrl = "";
14 | bool isPlaying = false;
15 |
16 | AudioElement? player;
17 |
18 | void start(String url) {
19 | currentUrl = url;
20 |
21 | stop();
22 | recreateNode();
23 | if (isPlaying) {
24 | resume();
25 | }
26 | }
27 |
28 | void setVolume(double volume) {
29 | currentVolume = volume;
30 | player?.volume = volume;
31 | }
32 |
33 | void setRate(double rate) {
34 | currentRate = rate;
35 | player?.playbackRate = rate;
36 | }
37 |
38 | void recreateNode() {
39 | if (currentUrl.isEmpty) {
40 | return;
41 | }
42 | player = AudioElement(currentUrl);
43 | player?.loop = playMode == PlayMode.single;
44 | player?.volume = currentVolume;
45 | player?.playbackRate = currentRate;
46 | }
47 |
48 | void seekTo(double position) {
49 | isPlaying = true;
50 | if (currentUrl.isEmpty) {
51 | return;
52 | }
53 | if (player == null) {
54 | recreateNode();
55 | }
56 | player?.play();
57 | player?.currentTime = position;
58 | }
59 |
60 | void resume() {
61 | seekTo(pausedAt);
62 | }
63 |
64 | void pause() {
65 | pausedAt = (player?.currentTime ?? 0) as double;
66 | _cancel();
67 | }
68 |
69 | void stop() {
70 | pausedAt = 0;
71 | _cancel();
72 | }
73 |
74 | void release() {
75 | _cancel();
76 | player = null;
77 | }
78 |
79 | void _cancel() {
80 | isPlaying = false;
81 | player?.pause();
82 | player = null;
83 | }
84 | }
85 |
86 | class AudioManagerPlugin {
87 | // players by playerId
88 | Map players = {};
89 |
90 | static void registerWith(Registrar registrar) {
91 | final MethodChannel channel = MethodChannel(
92 | 'audio_manager',
93 | const StandardMethodCodec(),
94 | // ignore: deprecated_member_use
95 | registrar.messenger);
96 |
97 | final AudioManagerPlugin instance = AudioManagerPlugin();
98 | channel.setMethodCallHandler(instance.handleMethodCall);
99 | }
100 |
101 | WrappedPlayer getOrCreatePlayer(String playerId) {
102 | return players.putIfAbsent(playerId, () => WrappedPlayer());
103 | }
104 |
105 | Future start(String playerId, String url) async {
106 | final WrappedPlayer player = getOrCreatePlayer(playerId);
107 |
108 | if (player.currentUrl == url) {
109 | return player;
110 | }
111 |
112 | player.start(url);
113 | return player;
114 | }
115 |
116 | Future handleMethodCall(MethodCall call) async {
117 | Map arguments = call.arguments ?? {};
118 | final playerId = arguments['playerId'] ?? "0";
119 | var player = getOrCreatePlayer(playerId);
120 | print("------arguments: ${call.method}, player: $arguments");
121 | switch (call.method) {
122 | case "getPlatformVersion":
123 | return "Browser ";
124 | case "start":
125 | final String url = arguments["url"];
126 | // String title = arguments["title"];
127 | // String desc = arguments["desc"];
128 | // String cover = arguments["cover"];
129 | bool isAuto = arguments["isAuto"] ?? false;
130 | // bool isLocal = arguments["isLocal"] ?? false;
131 | // bool isLocalCover = arguments["isLocalCover"] ?? false;
132 | player = await start(playerId, url);
133 | player.player?.autoplay = isAuto;
134 | return 1;
135 | case "playOrPause":
136 | if (player.isPlaying) {
137 | player.pause();
138 | } else {
139 | player.resume();
140 | }
141 | return player.isPlaying;
142 | case "play":
143 | player.resume();
144 | return player.isPlaying;
145 | case "pause":
146 | player.pause();
147 | return player.isPlaying;
148 | case "stop":
149 | player.stop();
150 | break;
151 | case 'release':
152 | player.release();
153 | return 1;
154 | case "seekTo":
155 | double position = call.arguments['position'] ?? 0;
156 | player.seekTo(position);
157 | break;
158 | case 'rate':
159 | double rate = call.arguments['rate'] ?? 1.0;
160 | player.setRate(rate);
161 | return 1;
162 | case "setVolume":
163 | double volume = call.arguments['volume'] ?? 1.0;
164 | player.setVolume(volume);
165 | break;
166 | case "currentVolume":
167 | return player.currentVolume;
168 | case 'updateLrc':
169 | default:
170 | throw PlatformException(
171 | code: 'Unimplemented',
172 | details:
173 | "The plugin for web doesn't implement the method '${call.method}'",
174 | );
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/lib/src/AudioInfo.dart:
--------------------------------------------------------------------------------
1 | class AudioInfo {
2 | String url;
3 | String title;
4 | String desc;
5 | String coverUrl;
6 |
7 | AudioInfo(this.url,
8 | {required this.title, required this.desc, required this.coverUrl});
9 |
10 | AudioInfo.fromJson(Map json)
11 | : url = json['url'],
12 | title = json['title'],
13 | desc = json['desc'],
14 | coverUrl = json['coverUrl'];
15 |
16 | Map toJson() => {
17 | 'url': url,
18 | 'title': title,
19 | 'desc': desc,
20 | 'coverUrl': coverUrl,
21 | };
22 |
23 | @override
24 | String toString() {
25 | return 'AudioInfo{url: $url, title: $title, desc: $desc, coverUrl: $coverUrl}';
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/src/AudioType.dart:
--------------------------------------------------------------------------------
1 | /// Play callback event enumeration
2 | enum AudioManagerEvents {
3 | /// start load data
4 | start,
5 |
6 | /// ready to play. If you want to invoke [seekTo], you must follow this callback
7 | ready,
8 |
9 | /// seek completed
10 | seekComplete,
11 |
12 | /// buffering size
13 | buffering,
14 |
15 | /// [isPlaying] status
16 | playstatus,
17 | timeupdate,
18 | error,
19 | next,
20 | previous,
21 | ended,
22 |
23 | /// Android notification bar click Close
24 | stop,
25 |
26 | /// ⚠️ IOS simulator is invalid, please use real machine
27 | volumeChange,
28 | unknow
29 | }
30 | typedef void Events(AudioManagerEvents events, args);
31 |
32 | /// Play rate enumeration [0.5, 0.75, 1, 1.5, 1.75, 2]
33 | enum AudioRate { rate50, rate75, rate100, rate150, rate175, rate200 }
34 |
35 | /// play mode
36 | enum PlayMode { sequence, shuffle, single }
37 |
38 | class PlaybackState {
39 | final AudioState state;
40 |
41 | final Duration position;
42 |
43 | final Duration bufferedSize;
44 |
45 | final AudioRate? speed;
46 |
47 | final error;
48 |
49 | const PlaybackState(
50 | this.state, {
51 | required this.position,
52 | required this.bufferedSize,
53 | this.speed,
54 | this.error,
55 | });
56 |
57 | const PlaybackState.none()
58 | : this(
59 | AudioState.none,
60 | position: const Duration(seconds: 0),
61 | bufferedSize: const Duration(seconds: 0),
62 | speed: AudioRate.rate100,
63 | );
64 | }
65 |
66 | /// play state
67 | enum AudioState { none, paused, playing, buffering, error }
68 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: audio_manager
2 | description: A flutter plugin for music playback, including notification handling.
3 | version: 0.8.2
4 | homepage: https://github.com/jeromexiong/audio_manager
5 |
6 | environment:
7 | sdk: ">=2.12.0 <3.0.0"
8 | flutter: ">=1.20.0"
9 |
10 | dependencies:
11 | flutter:
12 | sdk: flutter
13 | flutter_web_plugins:
14 | sdk: flutter
15 |
16 | dev_dependencies:
17 | flutter_test:
18 | sdk: flutter
19 |
20 | flutter:
21 | plugin:
22 | platforms:
23 | android:
24 | package: cc.dync.audio_manager
25 | pluginClass: AudioManagerPlugin
26 | ios:
27 | pluginClass: AudioManagerPlugin
28 | web:
29 | pluginClass: AudioManagerPlugin
30 | fileName: audio_manager_web.dart
31 |
--------------------------------------------------------------------------------
/screenshots/android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/screenshots/android.png
--------------------------------------------------------------------------------
/screenshots/android2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/screenshots/android2.png
--------------------------------------------------------------------------------
/screenshots/iOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/screenshots/iOS.png
--------------------------------------------------------------------------------
/screenshots/iOS2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeromexiong/audio_manager/8019029e391b53f8bbfca0e448c9db48e8eb5168/screenshots/iOS2.jpeg
--------------------------------------------------------------------------------
/test/audio_manager_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/services.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:audio_manager/audio_manager.dart';
4 |
5 | void main() {
6 | const MethodChannel channel = MethodChannel('audio_manager');
7 |
8 | TestWidgetsFlutterBinding.ensureInitialized();
9 |
10 | setUp(() {
11 | channel.setMockMethodCallHandler((MethodCall methodCall) async {
12 | return '42';
13 | });
14 | });
15 |
16 | tearDown(() {
17 | channel.setMockMethodCallHandler(null);
18 | });
19 |
20 | test('getPlatformVersion', () async {
21 | expect(await AudioManager.instance.platformVersion, '42');
22 | });
23 | }
24 |
--------------------------------------------------------------------------------