├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib │ └── flutter.jar ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── idofilus │ └── audio │ ├── AudioPlayer.java │ └── AudioPlugin.java ├── audio.iml ├── doc └── flutter_audio.gif ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── idofilus │ │ │ │ ├── audioexample │ │ │ │ └── MainActivity.java │ │ │ │ └── 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 │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── lib │ └── main.dart └── pubspec.yaml ├── flutter_audio_plugin.iml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── AudioPlayer.h │ ├── AudioPlayer.m │ ├── AudioPlugin.h │ └── AudioPlugin.m └── audio.podspec ├── lib └── audio.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | pubspec.lock 7 | 8 | build/ 9 | 10 | .vs/ 11 | .idea/ -------------------------------------------------------------------------------- /.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: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.5 2 | * Updated Uuid version to latest 3 | 4 | ## 0.0.4 5 | * Fixed gif 6 | 7 | ## 0.0.3 8 | * Added gif 9 | 10 | ## 0.0.2 11 | * Added LICENSE 12 | * Added example snippet 13 | 14 | ## 0.0.1 15 | * Multiplayer audio support -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ido Filus 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-player audio 2 | 3 | **This plugin is under development, this already being used in production. Just make sure you understand what's missing** 4 | 5 | ![Example running on Android](https://github.com/idofilus/flutter_audio/blob/master/doc/flutter_audio.gif?raw=true) 6 | 7 | 8 | Flutter Audio which will both play and ~~record audio~~ _(upcoming feature)_. 9 | 10 | _(after failed tries trying to work and combine few existing plugins, due the continues issues I had I decided to create this plugin)_ 11 | 12 | The aim of this plugin is really to give a solid solution for audio, 13 | feel free to open issues with new requests or pull requests. 14 | This is extremely important to have such a plugin that is very activate with updates. 15 | 16 | And of course, performance. 17 | Even thought this package is offering lots of features. 18 | I really care about the performance, so I took a close care of it. 19 | 20 | ## Features 21 | - Audio player with support to multiple players simultaneously 22 | - Buffer updates on the player _(percent of the loading whether it's local or remote file)_ 23 | - Preload the audio 24 | - ~~Audio Recorder~~ _(upcoming feature)_ 25 | 26 | I'll be adding soon more support, my goal is to make this plugin a complete solution. -------------------------------------------------------------------------------- /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/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.idofilus.audio' 2 | version '1.0-SNAPSHOT' 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.2.1' 11 | } 12 | } 13 | rootProject.allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | apply plugin: 'com.android.library' 20 | android { 21 | compileSdkVersion 27 22 | 23 | defaultConfig { 24 | minSdkVersion 16 25 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 26 | } 27 | lintOptions { 28 | disable 'InvalidPackage' 29 | } 30 | } 31 | 32 | dependencies { 33 | androidTestImplementation files('lib/flutter.jar') 34 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/lib/flutter.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/android/lib/flutter.jar -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'audio' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/idofilus/audio/AudioPlayer.java: -------------------------------------------------------------------------------- 1 | package com.idofilus.audio; 2 | 3 | import android.media.AudioAttributes; 4 | import android.media.AudioManager; 5 | import android.media.MediaPlayer; 6 | import android.os.Build; 7 | import android.os.Handler; 8 | import android.provider.MediaStore; 9 | import android.util.Log; 10 | 11 | import java.io.IOException; 12 | import java.util.HashMap; 13 | 14 | import io.flutter.plugin.common.MethodCall; 15 | import io.flutter.plugin.common.MethodChannel; 16 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 17 | import io.flutter.plugin.common.MethodChannel.Result; 18 | import io.flutter.plugin.common.PluginRegistry.Registrar; 19 | 20 | /** 21 | * AudioPlugin 22 | */ 23 | public class AudioPlayer 24 | { 25 | private static final String TAG = AudioPlayer.class.getName(); 26 | 27 | private MethodChannel channel; 28 | private String uid; 29 | private MediaPlayer player; 30 | private int handleInterval; 31 | private Handler handler = new Handler(); 32 | private boolean preloaded = false; 33 | private boolean loaded = false; 34 | private String lastUrl; 35 | 36 | public AudioPlayer(MethodChannel channel, String uid) 37 | { 38 | this.channel = channel; 39 | this.uid = uid; 40 | } 41 | 42 | MediaPlayer getPlayer() 43 | { 44 | return player; 45 | } 46 | 47 | /// Initialize the media player 48 | private void initialize() 49 | { 50 | if (player != null) 51 | return; 52 | 53 | player = new MediaPlayer(); 54 | player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() 55 | { 56 | @Override 57 | public void onBufferingUpdate(MediaPlayer mp, final int percent) 58 | { 59 | // TODO: TEST onBufferingUpdate on release 60 | 61 | Log.v(TAG, String.format("[onBufferingUpdate] percent=%d", percent)); 62 | invoke("player.onBuffering", percent); 63 | 64 | if (percent == 100 && !loaded) 65 | { 66 | loaded = true; 67 | 68 | if (preloaded) 69 | invoke("player.onReady", player.getDuration()); 70 | else 71 | playAudio(); 72 | } 73 | } 74 | }); 75 | 76 | player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() 77 | { 78 | @Override 79 | public void onCompletion(MediaPlayer mp) 80 | { 81 | stop(true); 82 | } 83 | }); 84 | 85 | player.setOnErrorListener(new MediaPlayer.OnErrorListener() 86 | { 87 | @Override 88 | public boolean onError(MediaPlayer mp, int what, int extra) 89 | { 90 | switch (what) 91 | { 92 | case MediaPlayer.MEDIA_ERROR_IO: 93 | AudioPlayer.this.onError("IO"); 94 | break; 95 | 96 | case MediaPlayer.MEDIA_ERROR_SERVER_DIED: 97 | AudioPlayer.this.onError("SERVER_DIED"); 98 | break; 99 | 100 | case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: 101 | AudioPlayer.this.onError("NOT_VALID_FOR_PROGRESSIVE_PLAYBACK"); 102 | break; 103 | 104 | case MediaPlayer.MEDIA_ERROR_MALFORMED: 105 | AudioPlayer.this.onError("MALFORMED"); 106 | break; 107 | 108 | case MediaPlayer.MEDIA_ERROR_UNSUPPORTED: 109 | AudioPlayer.this.onError("UNSUPPORTED"); 110 | break; 111 | 112 | case MediaPlayer.MEDIA_ERROR_TIMED_OUT: 113 | AudioPlayer.this.onError("TIMED_OUT"); 114 | break; 115 | 116 | case MediaPlayer.MEDIA_ERROR_UNKNOWN: 117 | default: 118 | AudioPlayer.this.onError("UNKNOWN"); 119 | break; 120 | } 121 | 122 | return true; 123 | } 124 | }); 125 | 126 | // TODO: Volume ? 127 | } 128 | 129 | private void playAudio() 130 | { 131 | initialize(); 132 | player.start(); 133 | handler.post(sendPayload); 134 | invoke("player.onPlay", player.getDuration()); 135 | } 136 | 137 | /// Release the media player 138 | void release() 139 | { 140 | if (player != null) 141 | { 142 | player.stop(); 143 | player.release(); 144 | } 145 | } 146 | 147 | void play(String url, int positionInterval) 148 | { 149 | Log.v(TAG, "playing: " + url); 150 | 151 | if (lastUrl == null || !lastUrl.equals(url)) 152 | { 153 | preload(url, positionInterval); 154 | preloaded = false; 155 | } 156 | else 157 | playAudio(); 158 | } 159 | 160 | void preload(String url, int positionInterval) 161 | { 162 | try 163 | { 164 | lastUrl = url; 165 | 166 | invoke("player.onBuffering", 0); 167 | 168 | loaded = false; 169 | initialize(); 170 | 171 | player.reset(); 172 | 173 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 174 | { 175 | player.setAudioAttributes(new AudioAttributes.Builder() 176 | .setUsage(AudioAttributes.USAGE_MEDIA) 177 | .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) 178 | .build()); 179 | } 180 | else 181 | { 182 | player.setAudioStreamType(AudioManager.STREAM_MUSIC); 183 | } 184 | 185 | player.setDataSource(url); 186 | player.prepareAsync(); 187 | 188 | handleInterval = positionInterval; 189 | preloaded = true; 190 | } 191 | catch (IOException e) 192 | { 193 | onError(e, "player.error.datasource", String.format("Failed to play audio, invalid data source: %s", e.getMessage())); 194 | } 195 | } 196 | 197 | void pause() 198 | { 199 | handler.removeCallbacks(sendPayload); 200 | 201 | if (player != null && player.isPlaying()) 202 | player.pause(); 203 | 204 | invoke("player.onPause", null); 205 | } 206 | 207 | /// completed flag will determine if the audio stopped by completion 208 | void stop(boolean completed) 209 | { 210 | handler.removeCallbacks(sendPayload); 211 | 212 | if (player != null && player.isPlaying()) 213 | player.stop(); 214 | 215 | invoke("player.onStop", completed); 216 | } 217 | 218 | void seek(double position) 219 | { 220 | if (player == null || position == player.getCurrentPosition()) 221 | return; 222 | 223 | player.seekTo((int)position); 224 | invoke("player.onCurrentPosition", position); 225 | } 226 | 227 | private Runnable sendPayload = new Runnable() 228 | { 229 | @Override 230 | public void run() 231 | { 232 | if (player.isPlaying()) 233 | { 234 | int position = player.getCurrentPosition(); 235 | Log.v(TAG, String.format("[position update of %d] uid=%s position=%d", handleInterval, uid, position)); 236 | invoke("player.onCurrentPosition", position); 237 | } 238 | 239 | handler.postDelayed(this, handleInterval); 240 | } 241 | }; 242 | 243 | private void onError(IOException e, final String code, final String message) 244 | { 245 | Log.e(TAG, message); 246 | channel.invokeMethod("player.onError", new HashMap() {{ 247 | put("uid", uid); 248 | put("code", code); 249 | put("message", message); 250 | }}); 251 | } 252 | 253 | private void onError(final String code) 254 | { 255 | Log.e(TAG, String.format("onError::code %s", code)); 256 | channel.invokeMethod("player.onError.code", new HashMap() {{ 257 | put("uid", uid); 258 | put("code", code); 259 | }}); 260 | } 261 | 262 | private void invoke(String name, Object argument) 263 | { 264 | HashMap data = new HashMap<>(); 265 | data.put("uid", uid); 266 | data.put("argument", argument); 267 | 268 | Log.v(TAG, String.format("[invoke] %s %s => ", name, uid) + argument); 269 | channel.invokeMethod(name, data); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /android/src/main/java/com/idofilus/audio/AudioPlugin.java: -------------------------------------------------------------------------------- 1 | package com.idofilus.audio; 2 | 3 | import java.util.WeakHashMap; 4 | 5 | import io.flutter.plugin.common.MethodCall; 6 | import io.flutter.plugin.common.MethodChannel; 7 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 8 | import io.flutter.plugin.common.MethodChannel.Result; 9 | import io.flutter.plugin.common.PluginRegistry.Registrar; 10 | 11 | /** 12 | * AudioPlugin 13 | */ 14 | public class AudioPlugin implements MethodCallHandler 15 | { 16 | private static final String TAG = AudioPlugin.class.getName(); 17 | 18 | private MethodChannel channel; 19 | private static WeakHashMap players = new WeakHashMap<>(); 20 | 21 | /** 22 | * Plugin registration. 23 | */ 24 | public static void registerWith(Registrar registrar) 25 | { 26 | final MethodChannel channel = new MethodChannel(registrar.messenger(), "audio"); 27 | channel.setMethodCallHandler(new AudioPlugin(registrar, channel)); 28 | } 29 | 30 | public AudioPlugin(Registrar registrar, MethodChannel channel) 31 | { 32 | this.channel = channel; 33 | } 34 | 35 | @Override 36 | public void onMethodCall(MethodCall call, Result result) 37 | { 38 | String uid = call.argument("uid"); 39 | 40 | System.out.println(String.format("onMethodCall method=%s uid=%s", call.method, uid)); 41 | 42 | // Make sure we have the audio player ready 43 | initialize(uid); 44 | 45 | switch (call.method) 46 | { 47 | case "player.play": 48 | players.get(uid).play((String)call.argument("url"), (int)call.argument("positionInterval")); 49 | result.success(null); 50 | break; 51 | 52 | case "player.preload": 53 | players.get(uid).preload((String)call.argument("url"), (int)call.argument("positionInterval")); 54 | result.success(null); 55 | break; 56 | 57 | case "player.pause": 58 | players.get(uid).pause(); 59 | result.success(null); 60 | break; 61 | 62 | case "player.stop": 63 | players.get(uid).stop(false); 64 | result.success(null); 65 | break; 66 | 67 | case "player.seek": 68 | players.get(uid).seek((double) call.argument("position")); 69 | result.success(null); 70 | break; 71 | 72 | case "player.release": 73 | players.get(uid).release(); 74 | result.success(null); 75 | break; 76 | 77 | default: 78 | result.notImplemented(); 79 | break; 80 | } 81 | } 82 | 83 | /// Initialize the media player 84 | private void initialize(final String uid) 85 | { 86 | if (players.containsKey(uid)) 87 | return; 88 | 89 | players.put(uid, new AudioPlayer(channel, uid)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /audio.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /doc/flutter_audio.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/doc/flutter_audio.gif -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /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: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # audio_example 2 | 3 | ```dart 4 | 5 | @immutable 6 | class AudioPlayerDemo extends StatefulWidget 7 | { 8 | final String url; 9 | 10 | AudioPlayerDemo(this.url); 11 | 12 | @override 13 | State createState() => AudioPlayerDemoState(); 14 | } 15 | 16 | class AudioPlayerDemoState extends State 17 | { 18 | Audio audioPlayer = new Audio(single: true); 19 | AudioPlayerState state = AudioPlayerState.STOPPED; 20 | double position = 0; 21 | StreamSubscription _playerStateSubscription; 22 | StreamSubscription _playerPositionController; 23 | StreamSubscription _playerBufferingSubscription; 24 | StreamSubscription _playerErrorSubscription; 25 | 26 | @override 27 | void initState() 28 | { 29 | _playerStateSubscription = audioPlayer.onPlayerStateChanged.listen((AudioPlayerState state) 30 | { 31 | print("onPlayerStateChanged: ${audioPlayer.uid} $state"); 32 | 33 | if (mounted) 34 | setState(() => this.state = state); 35 | }); 36 | 37 | _playerPositionController = audioPlayer.onPlayerPositionChanged.listen((double position) 38 | { 39 | print("onPlayerPositionChanged: ${audioPlayer.uid} $position ${audioPlayer.duration}"); 40 | 41 | if (mounted) 42 | setState(() => this.position = position); 43 | }); 44 | 45 | _playerBufferingSubscription = audioPlayer.onPlayerBufferingChanged.listen((int percent) 46 | { 47 | print("onPlayerBufferingChanged: ${audioPlayer.uid} $percent"); 48 | }); 49 | 50 | _playerErrorSubscription = audioPlayer.onPlayerError.listen((AudioPlayerError error) 51 | { 52 | throw("onPlayerError: ${error.code} ${error.message}"); 53 | }); 54 | 55 | audioPlayer.preload(widget.url); 56 | 57 | super.initState(); 58 | } 59 | 60 | @override 61 | Widget build(BuildContext context) 62 | { 63 | Widget status = Container(); 64 | 65 | print("[build] uid=${audioPlayer.uid} duration=${audioPlayer.duration} state=$state"); 66 | 67 | switch (state) 68 | { 69 | case AudioPlayerState.LOADING: 70 | { 71 | status = Container( 72 | padding: const EdgeInsets.all(12.0), 73 | child: Container( 74 | child: Center( 75 | child: CircularProgressIndicator(strokeWidth: 2.0)), 76 | width: 24.0, 77 | height: 24.0 78 | ) 79 | ); 80 | break; 81 | } 82 | 83 | case AudioPlayerState.PLAYING: 84 | { 85 | status = IconButton(icon: Icon(Icons.pause, size: 28.0), onPressed: onPause); 86 | break; 87 | } 88 | 89 | case AudioPlayerState.READY: 90 | case AudioPlayerState.PAUSED: 91 | case AudioPlayerState.STOPPED: 92 | { 93 | status = IconButton(icon: Icon(Icons.play_arrow, size: 28.0), onPressed: onPlay); 94 | 95 | if (state == AudioPlayerState.STOPPED) 96 | audioPlayer.seek(0.0); 97 | 98 | break; 99 | } 100 | } 101 | 102 | return Container( 103 | padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0), 104 | child: Column( 105 | children: [ 106 | Text(audioPlayer.uid), 107 | Row( 108 | children: [ 109 | status, 110 | Slider( 111 | max: audioPlayer.duration.toDouble(), 112 | value: position.toDouble(), 113 | onChanged: onSeek, 114 | ), 115 | Text("${audioPlayer.duration.toDouble()}ms") 116 | ], 117 | ) 118 | ], 119 | ), 120 | ); 121 | } 122 | 123 | @override 124 | void dispose() 125 | { 126 | _playerStateSubscription.cancel(); 127 | _playerPositionController.cancel(); 128 | _playerBufferingSubscription.cancel(); 129 | _playerErrorSubscription.cancel(); 130 | audioPlayer.release(); 131 | super.dispose(); 132 | } 133 | 134 | onPlay() 135 | { 136 | audioPlayer.play(widget.url); 137 | } 138 | 139 | onPause() 140 | { 141 | audioPlayer.pause(); 142 | } 143 | 144 | onSeek(double value) 145 | { 146 | // Note: We can only seek if the audio is ready 147 | audioPlayer.seek(value); 148 | } 149 | } 150 | 151 | ``` -------------------------------------------------------------------------------- /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 27 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 "com.idofilus.audioexample" 37 | minSdkVersion 16 38 | targetSdkVersion 27 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.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 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/idofilus/audioexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.idofilus.audioexample; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/idofilus/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.idofilus.example; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 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/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/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/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/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/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/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/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/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/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/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/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 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 | -------------------------------------------------------------------------------- /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-4.10.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/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:audio/audio.dart'; 4 | 5 | void main() => runApp(AudioApp()); 6 | 7 | class AudioRecorder extends StatefulWidget 8 | { 9 | @override 10 | State createState() => AudioRecorderState(); 11 | } 12 | 13 | class AudioRecorderState extends State 14 | { 15 | @override 16 | Widget build(BuildContext context) 17 | { 18 | return Container( 19 | padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0), 20 | child: Column( 21 | children: [ 22 | Text("Audio Recorder") 23 | ], 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | @immutable 30 | class AudioPlayerDemo extends StatefulWidget 31 | { 32 | final String url; 33 | 34 | AudioPlayerDemo(this.url); 35 | 36 | @override 37 | State createState() => AudioPlayerDemoState(); 38 | } 39 | 40 | class AudioPlayerDemoState extends State 41 | { 42 | Audio audioPlayer = new Audio(single: true); 43 | AudioPlayerState state = AudioPlayerState.STOPPED; 44 | double position = 0; 45 | int buffering = 0; 46 | StreamSubscription _playerStateSubscription; 47 | StreamSubscription _playerPositionController; 48 | StreamSubscription _playerBufferingSubscription; 49 | StreamSubscription _playerErrorSubscription; 50 | 51 | @override 52 | void initState() 53 | { 54 | _playerStateSubscription = audioPlayer.onPlayerStateChanged.listen((AudioPlayerState state) 55 | { 56 | print("onPlayerStateChanged: ${audioPlayer.uid} $state"); 57 | 58 | if (mounted) 59 | setState(() => this.state = state); 60 | }); 61 | 62 | _playerPositionController = audioPlayer.onPlayerPositionChanged.listen((double position) 63 | { 64 | print("onPlayerPositionChanged: ${audioPlayer.uid} $position ${audioPlayer.duration}"); 65 | 66 | if (mounted) 67 | setState(() => this.position = position); 68 | }); 69 | 70 | _playerBufferingSubscription = audioPlayer.onPlayerBufferingChanged.listen((int percent) 71 | { 72 | print("onPlayerBufferingChanged: ${audioPlayer.uid} $percent"); 73 | 74 | if (mounted && buffering != percent) 75 | setState(() => buffering = percent); 76 | }); 77 | 78 | _playerErrorSubscription = audioPlayer.onPlayerError.listen((AudioPlayerError error) 79 | { 80 | throw("onPlayerError: ${error.code} ${error.message}"); 81 | }); 82 | 83 | audioPlayer.preload(widget.url); 84 | 85 | super.initState(); 86 | } 87 | 88 | @override 89 | Widget build(BuildContext context) 90 | { 91 | Widget status = Container(); 92 | 93 | print("[build] uid=${audioPlayer.uid} duration=${audioPlayer.duration} state=$state"); 94 | 95 | switch (state) 96 | { 97 | case AudioPlayerState.LOADING: 98 | { 99 | status = Container( 100 | padding: const EdgeInsets.all(12.0), 101 | child: Container( 102 | width: 24.0, 103 | height: 24.0, 104 | child: Center( 105 | child: Stack( 106 | alignment: AlignmentDirectional.center, 107 | children: [ 108 | CircularProgressIndicator(strokeWidth: 2.0), 109 | Text("${buffering}%", style: TextStyle(fontSize: 8.0), textAlign: TextAlign.center) 110 | ], 111 | )), 112 | ) 113 | ); 114 | break; 115 | } 116 | 117 | case AudioPlayerState.PLAYING: 118 | { 119 | status = IconButton(icon: Icon(Icons.pause, size: 28.0), onPressed: onPause); 120 | break; 121 | } 122 | 123 | case AudioPlayerState.READY: 124 | case AudioPlayerState.PAUSED: 125 | case AudioPlayerState.STOPPED: 126 | { 127 | status = IconButton(icon: Icon(Icons.play_arrow, size: 28.0), onPressed: onPlay); 128 | 129 | if (state == AudioPlayerState.STOPPED) 130 | audioPlayer.seek(0.0); 131 | 132 | break; 133 | } 134 | } 135 | 136 | return Container( 137 | padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0), 138 | child: Column( 139 | children: [ 140 | Text(audioPlayer.uid), 141 | Row( 142 | children: [ 143 | status, 144 | Slider( 145 | max: audioPlayer.duration.toDouble(), 146 | value: position.toDouble(), 147 | onChanged: onSeek, 148 | ), 149 | Text("${audioPlayer.duration.toDouble()}ms") 150 | ], 151 | ) 152 | ], 153 | ), 154 | ); 155 | } 156 | 157 | @override 158 | void dispose() 159 | { 160 | _playerStateSubscription.cancel(); 161 | _playerPositionController.cancel(); 162 | _playerBufferingSubscription.cancel(); 163 | _playerErrorSubscription.cancel(); 164 | audioPlayer.release(); 165 | super.dispose(); 166 | } 167 | 168 | onPlay() 169 | { 170 | audioPlayer.play(widget.url); 171 | } 172 | 173 | onPause() 174 | { 175 | audioPlayer.pause(); 176 | } 177 | 178 | onSeek(double value) 179 | { 180 | // Note: We can only seek if the audio is ready 181 | audioPlayer.seek(value); 182 | } 183 | } 184 | 185 | class AudioApp extends StatefulWidget 186 | { 187 | 188 | @override 189 | _AudioAppState createState() => _AudioAppState(); 190 | } 191 | 192 | class _AudioAppState extends State 193 | { 194 | @override 195 | void initState() 196 | { 197 | super.initState(); 198 | } 199 | 200 | @override 201 | Widget build(BuildContext context) 202 | { 203 | return MaterialApp( 204 | debugShowCheckedModeBanner: false, 205 | home: Scaffold( 206 | appBar: AppBar( 207 | title: const Text("Audio"), 208 | ), 209 | body: ListView( 210 | children: [ 211 | //AudioPlayerDemo("https://firebasestorage.googleapis.com/v0/b/openso"), // Test the error handling 212 | AudioPlayerDemo("https://firebasestorage.googleapis.com/v0/b/opensource-11ed5.appspot.com/o/flutter_audio_plugin%2FSampleAudio_0.4mb.mp3?alt=media&token=a6334d66-dc48-4562-b126-ed7004b18e5c"), 213 | AudioPlayerDemo("https://firebasestorage.googleapis.com/v0/b/opensource-11ed5.appspot.com/o/flutter_audio_plugin%2F456235__greek555__loop-mix-128-bpm.mp3?alt=media&token=3d4f4357-a143-46bd-89c1-0b3fbaa9a9e9"), 214 | ], 215 | ), 216 | ), 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: audio_example 2 | description: Demonstrates how to use the audio plugin. 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | # The following adds the Cupertino Icons font to your application. 13 | # Use with the CupertinoIcons class for iOS style icons. 14 | cupertino_icons: ^0.1.2 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | audio: 21 | path: ../ 22 | 23 | # For information on the generic Dart part of this file, see the 24 | # following page: https://www.dartlang.org/tools/pub/pubspec 25 | 26 | # The following section is specific to Flutter. 27 | flutter: 28 | 29 | # The following line ensures that the Material Icons font is 30 | # included with your application, so that you can use the icons in 31 | # the material Icons class. 32 | uses-material-design: true 33 | 34 | # To add assets to your application, add an assets section, like this: 35 | # assets: 36 | # - images/a_dot_burr.jpeg 37 | # - images/a_dot_ham.jpeg 38 | 39 | # An image asset can refer to one or more resolution-specific "variants", see 40 | # https://flutter.io/assets-and-images/#resolution-aware. 41 | 42 | # For details regarding adding assets from package dependencies, see 43 | # https://flutter.io/assets-and-images/#from-packages 44 | 45 | # To add custom fonts to your application, add a fonts section here, 46 | # in this "flutter" section. Each entry in this list should have a 47 | # "family" key with the font family name, and a "fonts" key with a 48 | # list giving the asset and other descriptors for the font. For 49 | # example: 50 | # fonts: 51 | # - family: Schyler 52 | # fonts: 53 | # - asset: fonts/Schyler-Regular.ttf 54 | # - asset: fonts/Schyler-Italic.ttf 55 | # style: italic 56 | # - family: Trajan Pro 57 | # fonts: 58 | # - asset: fonts/TrajanPro.ttf 59 | # - asset: fonts/TrajanPro_Bold.ttf 60 | # weight: 700 61 | # 62 | # For details regarding fonts from package dependencies, 63 | # see https://flutter.io/custom-fonts/#from-packages 64 | -------------------------------------------------------------------------------- /flutter_audio_plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idofilus/flutter_audio/d1e68673e3181fe315a6661e73063e808dee8765/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/AudioPlayer.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AudioPlayer : NSObject 5 | 6 | @property NSString* uid; 7 | @property FlutterMethodChannel* channel; 8 | @property NSString* lastUrl; 9 | @property AVPlayer* player; 10 | @property AVPlayerItem* playerItem; 11 | @property bool preloaded; 12 | 13 | - (id)initWithUid: (NSString*) uid channel:(FlutterMethodChannel*)channel; 14 | - (void)play: (NSString*) url; 15 | - (void)preload: (NSString*) url; 16 | - (void)pause; 17 | - (void)stop: (bool) completed; 18 | - (void)seek: (double) position; 19 | - (void)releasePlayer; 20 | 21 | - (int)getPlayerDuration; 22 | 23 | @end -------------------------------------------------------------------------------- /ios/Classes/AudioPlayer.m: -------------------------------------------------------------------------------- 1 | #import "AudioPlayer.h" 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | @implementation AudioPlayer 9 | 10 | - (id)initWithUid: (NSString*) uid channel:(FlutterMethodChannel*)channel 11 | { 12 | self = [super init]; 13 | 14 | if (self) 15 | { 16 | self.uid = uid; 17 | self.channel = channel; 18 | self.preloaded = false; 19 | } 20 | 21 | if (self.channel) 22 | NSLog(@"Valid channel!"); 23 | 24 | return self; 25 | } 26 | 27 | - (void)play: (NSString*) url 28 | { 29 | NSLog(@"playing [%@]= %@", _uid, url); 30 | 31 | if (_lastUrl == nil || ![url isEqualToString:_lastUrl]) 32 | { 33 | [self preload:url]; 34 | _preloaded = false; 35 | } 36 | else 37 | { 38 | [self playAudio]; 39 | } 40 | } 41 | 42 | -(void)playAudio 43 | { 44 | [_player play]; 45 | [_channel invokeMethod:@"player.onPlay" arguments:@{@"uid": _uid, @"argument": @([self getPlayerDuration])}]; 46 | } 47 | 48 | - (void)preload: (NSString*) url 49 | { 50 | NSLog(@"preload [%@]= %@", _uid, url); 51 | 52 | _lastUrl = url; 53 | 54 | [_channel invokeMethod:@"player.onBuffering" arguments:@{@"uid": _uid, @"argument": @(0)}]; 55 | 56 | // Create AVAsset using URL 57 | AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:url]]; 58 | 59 | // Create AVPlayerItem using AVAsset 60 | _playerItem = [[AVPlayerItem alloc] initWithAsset:asset]; 61 | 62 | // Initialise AVPlayer 63 | _player = [AVPlayer playerWithPlayerItem:_playerItem]; 64 | 65 | // Register for playback end notification 66 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) 67 | name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem]; 68 | 69 | // Register observer for events of AVPlayer status 70 | [_player addObserver:self forKeyPath:@"status" options:0 context:nil]; 71 | [_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:0 context:nil]; 72 | 73 | id _observer = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 2) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) 74 | { 75 | int _progress = CMTimeGetSeconds(time); 76 | NSLog(@"_progress=%d", _progress); 77 | }]; 78 | 79 | CMTime interval = CMTimeMakeWithSeconds(0.2, NSEC_PER_SEC); 80 | id timeObserver = [_player addPeriodicTimeObserverForInterval:interval queue:nil usingBlock:^(CMTime time) 81 | { 82 | //[self onTimeInterval:time]; 83 | int _progress = CMTimeGetSeconds(_playerItem.currentTime) * 1000; 84 | 85 | if (_progress >= 0) 86 | { 87 | NSLog(@"_progress=%d", _progress); 88 | [_channel invokeMethod:@"player.onCurrentPosition" arguments:@{@"uid": _uid, @"argument": @(_progress)}]; 89 | } 90 | }]; 91 | 92 | _preloaded = true; 93 | } 94 | 95 | - (void)playerItemDidReachEnd:(NSNotification *)notification 96 | { 97 | NSLog(@"playerItemDidReachEnd"); 98 | [self stop:true]; 99 | } 100 | 101 | - (void)pause 102 | { 103 | NSLog(@"pause [%@]", _uid); 104 | [_player pause]; 105 | [_channel invokeMethod:@"player.onPause" arguments:@{@"uid": _uid}]; 106 | } 107 | 108 | - (void)stop: (bool) completed 109 | { 110 | NSLog(@"stop [%@] %d", _uid, completed); 111 | [_player pause]; 112 | [_player seekToTime:CMTimeMake(0, 1)]; 113 | [_player replaceCurrentItemWithPlayerItem:_playerItem]; 114 | [_channel invokeMethod:@"player.onStop" arguments:@{@"uid": _uid, @"argument": @(completed)}]; 115 | } 116 | 117 | - (void)seek: (double) position 118 | { 119 | NSLog(@"seek [%@] %.20f", _uid, position); 120 | [_player seekToTime:CMTimeMakeWithSeconds(position / 1000, 1)]; 121 | [_channel invokeMethod:@"player.onCurrentPosition" arguments:@{@"uid": _uid, @"argument": @(position)}]; 122 | } 123 | 124 | - (void)releasePlayer 125 | { 126 | NSLog(@"release [%@]", _uid); 127 | } 128 | 129 | - (int)getPlayerDuration 130 | { 131 | return CMTimeGetSeconds(_playerItem.duration) * 1000; 132 | } 133 | 134 | - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context 135 | { 136 | 137 | if (object == _player && [keyPath isEqualToString:@"status"]) 138 | { 139 | if (_player.status == AVPlayerStatusFailed) 140 | { 141 | NSLog(@"AVPlayer Failed"); 142 | 143 | } 144 | else if (_player.status == AVPlayerStatusReadyToPlay) 145 | { 146 | NSLog(@"AVPlayerStatusReadyToPlay [onReady:duration=%d or %d]", [self getPlayerDuration], CMTimeGetSeconds(_playerItem.asset.duration) * 1000); 147 | 148 | if (_preloaded) 149 | { 150 | [_channel invokeMethod:@"player.onReady" arguments:@{@"uid": _uid, @"argument": @([self getPlayerDuration])}]; 151 | } 152 | else 153 | { 154 | [self playAudio]; 155 | } 156 | } 157 | else if (_player.status == AVPlayerItemStatusUnknown) 158 | { 159 | NSLog(@"AVPlayer Unknown"); 160 | } 161 | } 162 | else if (object == _player && [keyPath isEqualToString:@"loadedTimeRanges"]) 163 | { 164 | NSArray* timeRanges = (NSArray*)[change objectForKey:NSKeyValueChangeNewKey]; 165 | 166 | NSLog(@"timerange 2"); 167 | 168 | if (timeRanges && [timeRanges count]) 169 | { 170 | CMTimeRange timerange = [[timeRanges objectAtIndex:0]CMTimeRangeValue]; 171 | NSLog(@"timerange 3"); 172 | } 173 | } 174 | else 175 | { 176 | //[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 177 | } 178 | } 179 | 180 | - (void)dealloc 181 | { 182 | /*[[NSNotificationCenter defaultCenter] removeObserver:self]; 183 | 184 | // Register observer for events of AVPlayer status 185 | [_player removeObserver:self forKeyPath:@"status" options:0 context:nil]; 186 | [_playerItem removeObserver:self forKeyPath:@"loadedTimeRanges" options:0 context:nil];*/ 187 | } 188 | 189 | @end -------------------------------------------------------------------------------- /ios/Classes/AudioPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AudioPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/AudioPlugin.m: -------------------------------------------------------------------------------- 1 | #import "AudioPlugin.h" 2 | #import "AudioPlayer.h" 3 | 4 | #import 5 | #import 6 | #import 7 | #import 8 | 9 | static FlutterMethodChannel* channel; 10 | static NSMutableDictionary* players; 11 | 12 | @implementation AudioPlugin 13 | + (void)registerWithRegistrar:(NSObject*)registrar 14 | { 15 | channel = [FlutterMethodChannel methodChannelWithName:@"audio" binaryMessenger:[registrar messenger]]; 16 | AudioPlugin* instance = [[AudioPlugin alloc] init]; 17 | [registrar addMethodCallDelegate:instance channel:channel]; 18 | } 19 | 20 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result 21 | { 22 | NSString* uid = call.arguments[@"uid"]; 23 | 24 | NSLog(@"handleMethodCall: [%@] %@", call.method, uid); 25 | 26 | for(NSString *key in [call.arguments allKeys]) { 27 | NSLog(@"handleMethodCall ARG=%@, VALUE=%@", key, [call.arguments objectForKey:key]); 28 | } 29 | 30 | if (![players objectForKey:uid]) 31 | players[uid] = [[AudioPlayer alloc] initWithUid:uid channel:channel]; 32 | 33 | if ([@"player.play" isEqualToString:call.method]) 34 | { 35 | [players[uid] play:call.arguments[@"url"]]; 36 | result(nil); 37 | } 38 | else if ([@"player.preload" isEqualToString:call.method]) 39 | { 40 | [players[uid] preload:call.arguments[@"url"]]; 41 | result(nil); 42 | } 43 | else if ([@"player.pause" isEqualToString:call.method]) 44 | { 45 | [players[uid] pause]; 46 | result(nil); 47 | } 48 | else if ([@"player.stop" isEqualToString:call.method]) 49 | { 50 | [players[uid] stop:call.arguments[@"completed"]]; 51 | result(nil); 52 | } 53 | else if ([@"player.seek" isEqualToString:call.method]) 54 | { 55 | NSLog(@"position = %.20f", [call.arguments[@"position"] doubleValue]); 56 | [players[uid] seek:[call.arguments[@"position"] doubleValue]]; 57 | result(nil); 58 | } 59 | else if ([@"player.release" isEqualToString:call.method]) 60 | { 61 | [players[uid] releasePlayer]; 62 | result(nil); 63 | } 64 | else 65 | { 66 | result(FlutterMethodNotImplemented); 67 | } 68 | } 69 | 70 | - (id)init 71 | { 72 | self = [super init]; 73 | 74 | if (self) 75 | { 76 | players = [[NSMutableDictionary alloc] init]; 77 | } 78 | 79 | return self; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /ios/audio.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'audio' 6 | s.version = '0.0.1' 7 | s.summary = 'A new Flutter project.' 8 | s.description = <<-DESC 9 | A new Flutter project. 10 | DESC 11 | s.homepage = 'http://example.com' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/audio.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | enum AudioPlayerState 7 | { 8 | /// Audio file is preparing for playback 9 | /// This can be by fetching the file or reading it 10 | LOADING, 11 | 12 | /// When the file is fully loaded 13 | READY, 14 | 15 | PLAYING, 16 | 17 | PAUSED, 18 | 19 | STOPPED 20 | } 21 | 22 | enum AudioPlayerErrorCode 23 | { 24 | MISSING_URL, 25 | 26 | /// File or network related operation errors 27 | IO, 28 | 29 | /// Media server died 30 | SERVER_DIED, 31 | 32 | /// Video is streamed but no valid progressive playback 33 | NOT_VALID_FOR_PROGRESSIVE_PLAYBACK, 34 | 35 | /// Bitstream is not conforming to the related coding standard or file spec 36 | MALFORMED, 37 | 38 | /// Bitstream is conforming to the related coding standard or file spec, but 39 | /// the media framework does not support the feature 40 | UNSUPPORTED, 41 | 42 | /// Some operation takes too long to complete 43 | TIMED_OUT, 44 | 45 | UNKNOWN 46 | } 47 | 48 | class AudioPlayerError 49 | { 50 | AudioPlayerErrorCode code; 51 | String message; 52 | 53 | AudioPlayerError(this.code, this.message); 54 | } 55 | 56 | class AudioPlayerErrorCodeType 57 | { 58 | String code; 59 | 60 | AudioPlayerErrorCodeType(this.code); 61 | } 62 | 63 | class AudioPlayer 64 | { 65 | static const MethodChannel _channel = const MethodChannel("audio"); 66 | 67 | final StreamController _playerStateController = StreamController.broadcast(); 68 | final StreamController _playerPositionController = StreamController.broadcast(); 69 | final StreamController _playerBufferingController = StreamController.broadcast(); 70 | final StreamController _playerErrorController = StreamController.broadcast(); 71 | 72 | String uid; 73 | AudioPlayerState _state = AudioPlayerState.STOPPED; 74 | int _duration = 0; 75 | bool _completed = false; 76 | 77 | /// Interval ms of how often the position stream will be called 78 | int _positionUpdateInterval; 79 | bool _ready = false; 80 | 81 | Stream get onPlayerStateChanged => _playerStateController.stream; 82 | Stream get onPlayerPositionChanged => _playerPositionController.stream; 83 | Stream get onPlayerBufferingChanged => _playerBufferingController.stream; 84 | Stream get onPlayerError => _playerErrorController.stream; 85 | 86 | AudioPlayer({positionInterval = 200}) 87 | { 88 | uid = new Uuid().v4(); 89 | _positionUpdateInterval = positionInterval; 90 | } 91 | 92 | AudioPlayerState get state => _state; 93 | 94 | /// Duration of the audio, will be updated once the audio state is PLAYING 95 | int get duration => _duration; 96 | 97 | /// You can know when audio is [stop]ed if it's stopped by reaching the end of the audio or not 98 | bool get isCompleted => _completed; 99 | 100 | Future play(String url) async 101 | { 102 | if (url == null) 103 | return _onError(new AudioPlayerError(AudioPlayerErrorCode.MISSING_URL, "Missing url when trying to play. Please provide it with audioPlayer.url = your_url")); 104 | 105 | await invoke("player.play", {"url": url, "positionInterval": _positionUpdateInterval}); 106 | } 107 | 108 | Future preload(String url) async 109 | { 110 | if (url == null) 111 | return _onError(new AudioPlayerError(AudioPlayerErrorCode.MISSING_URL, "Missing url when trying to preload. Please provide it with audioPlayer.url = your_url")); 112 | 113 | await invoke("player.preload", {"url": url, "positionInterval": _positionUpdateInterval}); 114 | } 115 | 116 | Future pause() async 117 | { 118 | await invoke("player.pause", null); 119 | } 120 | 121 | Future stop() async 122 | { 123 | await invoke("player.stop", null); 124 | } 125 | 126 | Future seek(double position) async 127 | { 128 | await invoke("player.seek", {"position": position}); 129 | } 130 | 131 | Future release() async 132 | { 133 | await invoke("player.release", null); 134 | } 135 | 136 | Future invoke(String name, Map data) async 137 | { 138 | if (data == null) 139 | data = {}; 140 | 141 | data["uid"] = uid; 142 | await _channel.invokeMethod(name, data); 143 | } 144 | 145 | void onBuffering(int percent) 146 | { 147 | if (!_ready && _state != AudioPlayerState.LOADING) 148 | { 149 | _state = AudioPlayerState.LOADING; 150 | _playerStateController.add(_state); 151 | 152 | if (percent == 100) 153 | { 154 | _ready = true; 155 | _state = AudioPlayerState.READY; 156 | } 157 | } 158 | 159 | _playerBufferingController.add(percent); 160 | } 161 | 162 | void onCurrentPosition(dynamic position) 163 | { 164 | if (position is int) 165 | position = (position as int).toDouble(); 166 | 167 | _playerPositionController.add(position); 168 | } 169 | 170 | void onPlay(int duration) 171 | { 172 | _ready = true; 173 | _duration = duration; 174 | _state = AudioPlayerState.PLAYING; 175 | _playerStateController.add(_state); 176 | } 177 | 178 | void onReady(int duration) 179 | { 180 | _duration = duration; 181 | _state = AudioPlayerState.READY; 182 | _playerStateController.add(_state); 183 | } 184 | 185 | void onPause() 186 | { 187 | _state = AudioPlayerState.PAUSED; 188 | _playerStateController.add(_state); 189 | } 190 | 191 | void onStop(bool completed) 192 | { 193 | _completed = completed; 194 | _state = AudioPlayerState.STOPPED; 195 | _playerStateController.add(_state); 196 | } 197 | 198 | AudioPlayerError _getErrorInstance(final String errorCode) 199 | { 200 | AudioPlayerErrorCode code; 201 | String message; 202 | 203 | switch (errorCode) 204 | { 205 | case "IO": 206 | code = AudioPlayerErrorCode.IO; 207 | message = "File or network related operation errors"; 208 | break; 209 | 210 | case "SERVER_DIED": 211 | code = AudioPlayerErrorCode.SERVER_DIED; 212 | message = "Media server died"; 213 | break; 214 | 215 | case "NOT_VALID_FOR_PROGRESSIVE_PLAYBACK": 216 | code = AudioPlayerErrorCode.NOT_VALID_FOR_PROGRESSIVE_PLAYBACK; 217 | message = "Video is streamed but no valid progressive playback"; 218 | break; 219 | 220 | case "MALFORMED": 221 | code = AudioPlayerErrorCode.MALFORMED; 222 | message = "Bitstream is not conforming to the related coding standard or file spec"; 223 | break; 224 | 225 | case "UNSUPPORTED": 226 | code = AudioPlayerErrorCode.UNSUPPORTED; 227 | message = "Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature"; 228 | break; 229 | 230 | case "TIMED_OUT": 231 | code = AudioPlayerErrorCode.TIMED_OUT; 232 | message = "Some operation takes too long to complete"; 233 | break; 234 | 235 | default: 236 | code = AudioPlayerErrorCode.UNKNOWN; 237 | message = "Unknown error"; 238 | break; 239 | } 240 | 241 | return AudioPlayerError(code, message); 242 | } 243 | 244 | void _onError(AudioPlayerError error) 245 | { 246 | stop(); 247 | _playerErrorController.add(error); 248 | } 249 | 250 | void _onErrorCode(AudioPlayerErrorCodeType error) 251 | { 252 | stop(); 253 | _playerErrorController.add(_getErrorInstance(error.code)); 254 | } 255 | } 256 | 257 | class Audio 258 | { 259 | static const MethodChannel _channel = const MethodChannel("audio"); 260 | static Map players = {}; 261 | AudioPlayer player; 262 | bool single; 263 | 264 | Stream get onPlayerStateChanged => player._playerStateController.stream; 265 | Stream get onPlayerPositionChanged => player._playerPositionController.stream; 266 | Stream get onPlayerBufferingChanged => player._playerBufferingController.stream; 267 | Stream get onPlayerError => player._playerErrorController.stream; 268 | 269 | String get uid => player.uid; 270 | AudioPlayerState get state => player.state; 271 | int get duration => player.duration; 272 | bool get isCompleted => player.isCompleted; 273 | 274 | /// Create [Audio] reference 275 | /// 276 | /// [single] will pause all the other players and make sure only 1 player play at a time 277 | /// [positionInterval] is the delay between each stream update on the player position 278 | Audio({ 279 | this.single = false, 280 | positionInterval = 200 281 | }) 282 | { 283 | _channel.setMethodCallHandler(_onChannelMethod); 284 | 285 | player = new AudioPlayer(positionInterval: positionInterval); 286 | players[player.uid] = player; 287 | } 288 | 289 | Future _onChannelMethod(MethodCall call) async 290 | { 291 | Map data = call.arguments; 292 | 293 | AudioPlayer player = players[data["uid"]]; 294 | 295 | if (player == null) 296 | { 297 | return; 298 | } 299 | 300 | dynamic argument = data["argument"]; 301 | 302 | switch (call.method) 303 | { 304 | case "player.onBuffering": 305 | player.onBuffering(argument); 306 | break; 307 | 308 | case "player.onCurrentPosition": 309 | player.onCurrentPosition(argument); 310 | break; 311 | 312 | case "player.onPlay": 313 | player.onPlay(argument); 314 | break; 315 | 316 | case "player.onReady": 317 | player.onReady(argument); 318 | break; 319 | 320 | case "player.onPause": 321 | player.onPause(); 322 | break; 323 | 324 | case "player.onStop": 325 | player.onStop(argument); 326 | break; 327 | 328 | case "player.onError": 329 | player._onError(argument); 330 | break; 331 | 332 | case "player.onError.code": 333 | player._onErrorCode(argument); 334 | break; 335 | 336 | default: 337 | throw new ArgumentError("Unknown channel method ${call.method}"); 338 | } 339 | } 340 | 341 | Future play(String url) async 342 | { 343 | // Make sure all the other players is paused 344 | if (single) 345 | { 346 | players.forEach((uid, player) 347 | { 348 | player.pause(); 349 | }); 350 | } 351 | 352 | await player.play(url); 353 | } 354 | 355 | Future preload(String url) async 356 | { 357 | await player.preload(url); 358 | } 359 | 360 | Future pause() async 361 | { 362 | await player.pause(); 363 | } 364 | 365 | Future stop() async 366 | { 367 | await player.stop(); 368 | } 369 | 370 | Future seek(double position) async 371 | { 372 | await player.seek(position); 373 | } 374 | 375 | Future release() async 376 | { 377 | await player.release(); 378 | } 379 | 380 | static void stopAll() 381 | { 382 | players.forEach((uid, player) 383 | { 384 | player.stop(); 385 | }); 386 | } 387 | 388 | void _onError(AudioPlayerError error) 389 | { 390 | player._onError(error); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: audio 2 | description: Audio recorder, streamable and multi-player. 3 | version: 0.0.5 4 | author: Ido Filus 5 | homepage: https://github.com/idofilus/flutter_audio 6 | 7 | environment: 8 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 9 | flutter: ">=0.2.5 <2.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | uuid: ^2.0.1 16 | 17 | flutter: 18 | plugin: 19 | androidPackage: com.idofilus.audio 20 | pluginClass: AudioPlugin --------------------------------------------------------------------------------